Initial self-hosting for tests.
[online-glom:gwt-glom.git] / src / test / java / org / glom / web / server / SelfHoster.java
1 /*
2  * Copyright (C) 2012 Openismus GmbH
3  *
4  * This file is part of GWT-Glom.
5  *
6  * GWT-Glom is free software: you can redistribute it and/or modify it
7  * under the terms of the GNU Lesser General Public License as published by the
8  * Free Software Foundation, either version 3 of the License, or (at your
9  * option) any later version.
10  *
11  * GWT-Glom is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
14  * for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with GWT-Glom.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package org.glom.web.server;
21
22 import java.io.BufferedReader;
23 import java.io.File;
24 import java.io.FileNotFoundException;
25 import java.io.FileOutputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.InputStreamReader;
29 import java.net.ServerSocket;
30 import java.sql.Connection;
31 import java.sql.DriverManager;
32 import java.sql.SQLException;
33 import java.util.List;
34 import java.util.Locale;
35 import java.util.Map;
36 import java.util.Map.Entry;
37 import java.util.Properties;
38
39 import org.apache.commons.lang3.StringUtils;
40 import org.glom.web.server.libglom.Document;
41 import org.glom.web.shared.DataItem;
42 import org.glom.web.shared.libglom.Field;
43 import org.jooq.InsertResultStep;
44 import org.jooq.InsertSetStep;
45 import org.jooq.Record;
46 import org.jooq.SQLDialect;
47 import org.jooq.Table;
48 import org.jooq.exception.DataAccessException;
49 import org.jooq.impl.Factory;
50
51 import com.google.common.io.Files;
52 import com.ibm.icu.text.NumberFormat;
53
54 /**
55  * @author Murray Cumming <murrayc@murrayc.com>
56  * 
57  */
58 public class SelfHoster {
59         // private String tempFilepathDir = "";
60         private boolean selfHostingActive = false;
61         private Document document = null;
62         private String username = "";
63         private String password = "";
64
65         SelfHoster(final Document document) {
66                 this.document = document;
67         }
68
69         private static final int PORT_POSTGRESQL_SELF_HOSTED_START = 5433;
70         private static final int PORT_POSTGRESQL_SELF_HOSTED_END = 5500;
71
72         private static final String DEFAULT_CONFIG_PG_HBA_LOCAL_8p4 = "# TYPE  DATABASE    USER        CIDR-ADDRESS          METHOD\n"
73                         + "\n"
74                         + "# local is for Unix domain socket connections only\n"
75                         + "# trust allows connection from the current PC without a password:\n"
76                         + "local   all         all                               trust\n"
77                         + "local   all         all                               ident\n"
78                         + "local   all         all                               md5\n"
79                         + "\n"
80                         + "# TCP connections from the same computer, with a password:\n"
81                         + "host    all         all         127.0.0.1    255.255.255.255    md5\n"
82                         + "# IPv6 local connections:\n"
83                         + "host    all         all         ::1/128               md5\n";
84
85         private static final String DEFAULT_CONFIG_PG_IDENT = "";
86         private static final String FILENAME_DATA = "data";
87
88         public boolean createAndSelfHostFromExample(final Document.HostingMode hostingMode) {
89
90                 if (!createAndSelfHostNewEmpty(hostingMode)) {
91                         // std::cerr << G_STRFUNC << ": test_create_and_selfhost_new_empty() failed." << std::endl;
92                         return false;
93                 }
94
95                 final boolean recreated = recreateDatabaseFromDocument(); /* TODO: Progress callback */
96                 if (!recreated) {
97                         cleanup();
98                 }
99
100                 return recreated;
101         }
102
103         /**
104          * @param document
105          * @param
106          * @param subDirectoryPath
107          * @return
108          */
109         private boolean createAndSelfHostNewEmpty(Document.HostingMode hostingMode) {
110                 if (hostingMode != Document.HostingMode.HOSTING_MODE_POSTGRES_SELF) {
111                         // TODO: std::cerr << G_STRFUNC << ": This test function does not support the specified hosting_mode: " <<
112                         // hosting_mode << std::endl;
113                         return false;
114                 }
115
116                 // Save a copy, specifying the path to file in a directory:
117                 // For instance, /tmp/testglom/testglom.glom");
118                 final String tempFilename = "testglom";
119                 File tempFolder = Files.createTempDir();
120                 final File tempDir = new File(tempFolder, tempFilename);
121
122                 final String tempDirPath = tempDir.getPath();
123                 final String tempFilePath = tempDirPath + File.separator + tempFilename;
124                 final File file = new File(tempFilePath);
125
126                 // Make sure that the file does not exist yet:
127                 {
128                         tempDir.delete();
129                 }
130
131                 // Save the example as a real file:
132                 document.setFileURI(file.getPath());
133
134                 document.setHostingMode(hostingMode);
135                 document.setIsExampleFile(false);
136                 final boolean saved = document.save();
137                 if (!saved) {
138                         // TODO: Warn
139                         return false; // TODO: Delete the directory.
140                 }
141
142                 // We must specify a default username and password:
143                 final String user = "glom_default_developer_user";
144                 final String password = "glom_default_developer_password";
145
146                 // Create the self-hosting files:
147                 if (!initialize(user, password)) {
148                         // TODO: Warn.
149                         // TODO: Delete directory.
150                 }
151
152                 // Check that it really created some files:
153                 if (!tempDir.exists()) {
154                         // TODO: Warn
155                         // TODO: Delete directory.
156                 }
157
158                 return selfHost(user, password);
159         }
160
161         /**
162          * @param document
163          * @param user
164          * @param password
165          * @return
166          */
167         private boolean selfHost(final String user, final String password) {
168                 // TODO: m_network_shared = network_shared;
169
170                 if (getSelfHostingActive()) {
171                         // TODO: std::cerr << G_STRFUNC << ": Already started." << std::endl;
172                         return false; // STARTUPERROR_NONE; //Just do it once.
173                 }
174
175                 final String dbDirData = getSelfHostingDataPath(false);
176                 if (StringUtils.isEmpty(dbDirData) || !fileExists(dbDirData)) {
177                         /*
178                          * final String dbDirBackup = dbDir + File.separator + FILENAME_BACKUP;
179                          * 
180                          * if(fileExists(dbDirBackup)) { //TODO: std::cerr << G_STRFUNC <<
181                          * ": There is no data, but there is backup data." << std::endl; //Let the caller convert the backup to real
182                          * data and then try again: return false; // STARTUPERROR_FAILED_NO_DATA_HAS_BACKUP_DATA; } else {
183                          */
184                         // TODO: std::cerr << "ConnectionPool::create_self_hosting(): The data sub-directory could not be found." <<
185                         // dbdir_data_uri << std::endl;
186                         return false; // STARTUPERROR_FAILED_NO_DATA;
187                         // }
188                 }
189
190                 final int availablePort = discoverFirstFreePort(PORT_POSTGRESQL_SELF_HOSTED_START,
191                                 PORT_POSTGRESQL_SELF_HOSTED_END);
192                 // std::cout << "debug: " << G_STRFUNC << ":() : debug: Available port for self-hosting: " << available_port <<
193                 // std::endl;
194                 if (availablePort == 0) {
195                         // TODO: Use a return enum or exception so we can tell the user about this:
196                         // TODO: std::cerr << G_STRFUNC << ": No port was available between " << PORT_POSTGRESQL_SELF_HOSTED_START
197                         // << " and " << PORT_POSTGRESQL_SELF_HOSTED_END << std::endl;
198                         return false; // STARTUPERROR_FAILED_UNKNOWN_REASON;
199                 }
200
201                 final NumberFormat format = NumberFormat.getInstance(Locale.US);
202                 format.setGroupingUsed(false); // TODO: Does this change it system-wide?
203                 final String portAsText = format.format(availablePort);
204
205                 // -D specifies the data directory.
206                 // -c config_file= specifies the configuration file
207                 // -k specifies a directory to use for the socket. This must be writable by us.
208                 // Make sure to use double quotes for the executable path, because the
209                 // CreateProcess() API used on Windows does not support single quotes.
210                 final String dbDir = getSelfHostingPath("", false);
211                 final String dbDirConfig = getSelfHostingPath("config", false);
212                 final String dbDirHba = dbDirConfig + File.separator + "pg_hba.conf";
213                 final String dbDirIdent = dbDirConfig + File.separator + "pg_ident.conf";
214                 final String dbDirPid = getSelfHostingPath("pid", false);
215
216                 // Note that postgres returns this error if we split the arguments more,
217                 // for instance splitting -D and dbDirData into separate strings:
218                 // too many command-line arguments (first is "(null)")
219                 // Note: If we use "-D " instead of "-D" then the initdb seems to make the space part of the filepath,
220                 // though that does not happen with the normal command line.
221                 // However, we must have a space after -k.
222                 // Also, the c hba_file=path argument must be split after -c, or postgres will get a " hba_file" configuration
223                 // parameter instead of "hba_file".
224                 final ProcessBuilder commandPostgresStart = new ProcessBuilder(getPathToPostgresExecutable("postgres"), "-D"
225                                 + shellQuote(dbDirData), "-p", portAsText, "-i", // Equivalent to -h "*", which in turn is equivalent
226                                                                                                                                         // to
227                                 // listen_addresses in postgresql.conf. Listen to all IP addresses,
228                                 // so any client can connect (with a username+password)
229                                 "-c", "hba_file=" + shellQuote(dbDirHba), "-c", "ident_file=" + shellQuote(dbDirIdent), "-k"
230                                                 + shellQuote(dbDir), "--external_pid_file=" + shellQuote(dbDirPid));
231                 // std::cout << G_STRFUNC << ": debug: " << command_postgres_start << std::endl;
232
233                 // Make sure to use double quotes for the executable path, because the
234                 // CreateProcess() API used on Windows does not support single quotes.
235                 //
236                 // Note that postgres returns this error if we split the arguments more,
237                 // for instance splitting -D and dbDirData into separate strings:
238                 // too many command-line arguments (first is "(null)")
239                 // Note: If we use "-D " instead of "-D" then the initdb seems to make the space part of the filepath,
240                 // though that does not happen with the normal command line.
241                 final ProcessBuilder commandCheckPostgresHasStarted = new ProcessBuilder(getPathToPostgresExecutable("pg_ctl"),
242                                 "status", "-D" + shellQuote(dbDirData));
243
244                 // For postgres 8.1, this is "postmaster is running".
245                 // For postgres 8.2, this is "server is running".
246                 // This is a big hack that we should avoid. murrayc.
247                 //
248                 // pg_ctl actually seems to return a 0 result code for "is running" and a 1 for not running, at least with
249                 // Postgres 8.2,
250                 // so maybe we can avoid this in future.
251                 // Please do test it with your postgres version, using "echo $?" to see the result code of the last command.
252                 final String secondCommandSuccessText = "is running"; // TODO: This is not a stable API. Also, watch out for
253                                                                                                                                 // localisation.
254
255                 // The first command does not return, but the second command can check whether it succeeded:
256                 // TODO: Progress
257                 final boolean result = executeCommandLineAndWaitUntilSecondCommandReturnsSuccess(commandPostgresStart,
258                                 commandCheckPostgresHasStarted, secondCommandSuccessText);
259                 if (!result) {
260                         // TODO: std::cerr << "Error while attempting to self-host a database." << std::endl;
261                         return false; // STARTUPERROR_FAILED_UNKNOWN_REASON;
262                 }
263
264                 // Remember the port for later:
265                 document.setConnectionPort(availablePort);
266
267                 return true; // STARTUPERROR_NONE;
268         }
269
270         /**
271          * @param dbDirData
272          * @return
273          */
274         private String shellQuote(String str) {
275                 // TODO: If we add the quotes then they seem to be used as part of the path, though that is not a problem with
276                 // the normal command line.
277                 return str;
278
279                 // TODO: Escape.
280                 // return "'" + str + "'";
281         }
282
283         private String getSelfHostingPath(final String subpath, boolean create) {
284                 final String dbDir = document.getSelfHostedDirectoryPath();
285                 if (StringUtils.isEmpty(subpath)) {
286                         return dbDir;
287                 }
288
289                 final String dbDirData = dbDir + File.separator + subpath;
290                 final File file = new File(dbDirData);
291
292                 // Return the path regardless of whether it exists:
293                 if (!create) {
294                         return dbDirData;
295                 }
296
297                 if (!file.exists()) {
298                         try {
299                                 Files.createParentDirs(file);
300                         } catch (IOException e) {
301                                 // TODO Auto-generated catch block
302                                 e.printStackTrace();
303                                 return "";
304                         }
305
306                         if (!file.mkdir()) {
307                                 return "";
308                         }
309                 }
310
311                 return dbDirData;
312         }
313
314         private String getSelfHostingDataPath(boolean create) {
315                 return getSelfHostingPath(FILENAME_DATA, create);
316         }
317
318         private boolean executeCommandLineAndWait(ProcessBuilder command) {
319
320                 command.redirectErrorStream(true);
321
322                 // Run the first command, and wait for it to return:
323                 Process process = null;
324                 try {
325                         process = command.start();
326                 } catch (IOException e) {
327                         // TODO Auto-generated catch block
328                         e.printStackTrace();
329                         return false;
330                 }
331
332                 final InputStream stderr = process.getInputStream();
333                 final InputStreamReader isr = new InputStreamReader(stderr);
334                 final BufferedReader br = new BufferedReader(isr);
335                 String output = "";
336                 String line;
337                 try {
338                         while ((line = br.readLine()) != null) {
339                                 output += line + "\n";
340                         }
341                 } catch (IOException e1) {
342                         e1.printStackTrace();
343                         return false;
344                 }
345
346                 int result = 0;
347                 try {
348                         result = process.waitFor();
349                 } catch (InterruptedException e) {
350                         // TODO Auto-generated catch block
351                         e.printStackTrace();
352                         return false;
353                 }
354
355                 if (result != 0) {
356                         System.out.println("Command failed: " + command.toString());
357                         System.out.print("Output: " + output);
358                         return false;
359                 }
360
361                 return true;
362         }
363
364         private boolean executeCommandLineAndWaitUntilSecondCommandReturnsSuccess(ProcessBuilder command,
365                         ProcessBuilder commandSecond, String secondCommandSuccessText) {
366                 command.redirectErrorStream(true);
367
368                 // Run the first command, and do not wait for it to return:
369                 Process process = null;
370                 try {
371                         process = command.start();
372                 } catch (IOException e) {
373                         // TODO Auto-generated catch block
374                         e.printStackTrace();
375                         return false;
376                 }
377
378                 final InputStream stderr = process.getInputStream();
379                 final InputStreamReader isr = new InputStreamReader(stderr);
380                 final BufferedReader br = new BufferedReader(isr);
381
382                 /*
383                  * We do not wait, because this (postgres, for instance), does not return: final int result = process.waitFor();
384                  * if (result != 0) { // TODO: Warn. return false; }
385                  */
386
387                 // Now run the second command, usually to verify that the first command has really done its work:
388                 final boolean result = executeCommandLineAndWait(commandSecond);
389
390                 // Try to get the output:
391                 if (!result) {
392                         String output = "";
393                         String line;
394                         try {
395                                 while ((line = br.readLine()) != null) {
396                                         output += line + "\n";
397                                         System.out.println(line);
398                                 }
399                         } catch (IOException e1) {
400                                 // TODO Auto-generated catch block
401                                 e1.printStackTrace();
402                                 return false;
403                         }
404
405                         System.out.print("Output of first command: " + output);
406                 }
407
408                 return result;
409         }
410
411         /**
412          * @param string
413          * @return
414          */
415         private static String getPathToPostgresExecutable(String string) {
416                 // TODO: Test other locations.
417                 return "/usr/bin/" + string;
418         }
419
420         /**
421          * @param start
422          * @param end
423          * @return
424          */
425         private static int discoverFirstFreePort(int start, int end) {
426                 for (int port = start; port <= end; ++port) {
427                         try {
428                                 ServerSocket socket = new ServerSocket(port);
429
430                                 // If the instantiation succeeded then the port was free:
431                                 return socket.getLocalPort(); // This must equal port.
432                         } catch (IOException ex) {
433                                 continue; // try next port
434                         }
435                 }
436
437                 return 0;
438         }
439
440         /**
441          * @param dbDir
442          * @return
443          */
444         private static boolean fileExists(String filePath) {
445                 final File file = new File(filePath);
446                 return file.exists();
447         }
448
449         private void setSelfHostingActive(boolean selfHostingActive) {
450                 this.selfHostingActive = selfHostingActive;
451         }
452
453         /**
454          * @return
455          */
456         private boolean getSelfHostingActive() {
457                 return selfHostingActive;
458         }
459
460         /**
461          * @param cpds
462          * @return
463          */
464         private boolean initialize(final String initialUsername, final String initialPassword) {
465                 if (!initializeConfFiles()) {
466                         // TODO: Warn
467                         return false;
468                 }
469
470                 // initdb creates a new postgres database cluster:
471
472                 // Get file:// URI for the tmp/ directory:
473                 File filePwFile = null;
474                 try {
475                         filePwFile = File.createTempFile("glom_initdb_pwfile", "");
476                 } catch (IOException e) {
477                         // TODO Auto-generated catch block
478                         e.printStackTrace();
479                 }
480                 final String tempPwFile = filePwFile.getPath();
481
482                 final boolean pwfileCreationSucceeded = createTextFile(tempPwFile, initialPassword);
483                 if (!pwfileCreationSucceeded) {
484                         // TODO: Warn.
485                         return false;
486                 }
487
488                 // Make sure to use double quotes for the executable path, because the
489                 // CreateProcess() API used on Windows does not support single quotes.
490                 final String dbDirData = getSelfHostingDataPath(false /* create */);
491
492                 // Note that initdb returns this error if we split the arguments more,
493                 // for instance splitting -D and dbDirData into separate strings:
494                 // too many command-line arguments (first is "(null)")
495                 // TODO: If we quote tempPwFile then initdb says that it cannot find it.
496                 // Note: If we use "-D " instead of "-D" then the initdb seems to make the space part of the filepath,
497                 // though that does not happen with the normal command line.
498                 final ProcessBuilder commandInitdb = new ProcessBuilder(getPathToPostgresExecutable("initdb"), "-D"
499                                 + shellQuote(dbDirData), "-U", initialUsername, "--pwfile=" + tempPwFile);
500
501                 // Note that --pwfile takes the password from the first line of a file. It's an alternative to supplying it when
502                 // prompted on stdin.
503                 final boolean result = executeCommandLineAndWait(commandInitdb);
504                 if (!result) {
505                         // TODO: std::cerr << "Error while attempting to create self-hosting database." << std::endl;
506                         return false;
507                 }
508
509                 // Of course, we don't want this to stay around. It would be a security risk.
510                 File fileTempPwFile = new File(tempPwFile);
511                 if (!fileTempPwFile.delete()) {
512                         // TODO: Warn.
513                 }
514
515                 // Save the username and password for later;
516                 this.username = initialUsername;
517                 this.password = initialPassword;
518
519                 return result; // ? INITERROR_NONE : INITERROR_COULD_NOT_START_SERVER;
520
521         }
522
523         private boolean initializeConfFiles() {
524                 final String dataDirPath = document.getSelfHostedDirectoryPath();
525
526                 final String dbDirConfig = dataDirPath + File.separator + "config";
527                 // String defaultConfContents = "";
528
529                 // Choose the configuration contents based on the postgresql version
530                 // and whether we want to be network-shared:
531                 // final float postgresqlVersion = 9.0f; //TODO: get_postgresql_utils_version_as_number(slot_progress);
532                 // final boolean networkShared = true;
533                 // std::cout << "DEBUG: postgresql_version=" << postgresql_version << std::endl;
534
535                 // TODO: Support the other configurations, as in libglom.
536                 final String defaultConfContents = DEFAULT_CONFIG_PG_HBA_LOCAL_8p4;
537
538                 // std::cout << "DEBUG: default_conf_contents=" << default_conf_contents << std::endl;
539
540                 final boolean hbaConfCreationSucceeded = createTextFile(dbDirConfig + File.separator + "pg_hba.conf",
541                                 defaultConfContents);
542                 if (!hbaConfCreationSucceeded) {
543                         // TODO: Warn
544                         return false;
545                 }
546
547                 final boolean identConfCreationSucceeded = createTextFile(dbDirConfig + File.separator + "pg_ident.conf",
548                                 DEFAULT_CONFIG_PG_IDENT);
549                 if (!identConfCreationSucceeded) {
550                         // TODO: Warn
551                         return false;
552                 }
553
554                 return true;
555         }
556
557         /**
558          * @param path
559          * @param contents
560          * @return
561          */
562         private static boolean createTextFile(final String path, final String contents) {
563                 final File file = new File(path);
564                 final File parent = file.getParentFile();
565                 if (parent == null) {
566                         // TODO: Warn.
567                         return false;
568                 }
569
570                 parent.mkdirs();
571                 try {
572                         file.createNewFile();
573                 } catch (IOException e) {
574                         // TODO Auto-generated catch block
575                         e.printStackTrace();
576                         return false;
577                 }
578
579                 FileOutputStream output = null;
580                 try {
581                         output = new FileOutputStream(file);
582                 } catch (FileNotFoundException e) {
583                         // TODO Auto-generated catch block
584                         e.printStackTrace();
585                         return false;
586                 }
587
588                 try {
589                         output.write(contents.getBytes());
590                 } catch (IOException e) {
591                         // TODO Auto-generated catch block
592                         e.printStackTrace();
593                         return false;
594                 }
595
596                 return true;
597         }
598
599         /**
600          * @param document
601          * @return
602          */
603         private boolean recreateDatabaseFromDocument() {
604                 // Check whether the database exists already.
605                 final String dbName = document.getConnectionDatabase();
606                 if (StringUtils.isEmpty(dbName)) {
607                         return false;
608                 }
609
610                 document.setConnectionDatabase(dbName);
611                 Connection connection = createConnection();
612                 if (connection != null) {
613                         // Connection to the database succeeded, so the database
614                         // exists already.
615                         try {
616                                 connection.close();
617                         } catch (SQLException e) {
618                                 // TODO Auto-generated catch block
619                                 e.printStackTrace();
620                         }
621                         return false;
622                 }
623
624                 // Create the database:
625                 progress();
626                 document.setConnectionDatabase("");
627
628                 connection = createConnection();
629                 if (connection == null) {
630                         // TODO: Warn.
631                         return false;
632                 }
633
634                 final boolean dbCreated = createDatabase(connection, dbName);
635
636                 if (!dbCreated) {
637                         return false;
638                 }
639
640                 progress();
641
642                 // Check that we can connect:
643                 try {
644                         connection.close();
645                 } catch (SQLException e) {
646                         // TODO Auto-generated catch block
647                         e.printStackTrace();
648                 }
649                 connection = null;
650
651                 document.setConnectionDatabase(dbName);
652                 connection = createConnection();
653                 if (connection == null) {
654                         // TODO: Warn
655                         return false;
656                 }
657
658                 progress();
659
660                 // Create each table:
661                 final List<String> tables = document.getTableNames();
662                 for (String tableName : tables) {
663
664                         // Create SQL to describe all fields in this table:
665                         final List<Field> fields = document.getTableFields(tableName);
666
667                         progress();
668                         final boolean tableCreationSucceeded = createTable(connection, document, tableName, fields);
669                         progress();
670                         if (!tableCreationSucceeded) {
671                                 // TODO: std::cerr << G_STRFUNC << ": CREATE TABLE failed with the newly-created database." <<
672                                 // std::endl;
673                                 return false;
674                         }
675                 }
676
677                 // Note that create_database() has already called add_standard_tables() and add_standard_groups(document).
678
679                 // Add groups from the document:
680                 progress();
681                 if (!addGroupsFromDocument(document)) {
682                         // TODO: std::cerr << G_STRFUNC << ": add_groups_from_document() failed." << std::endl;
683                         return false;
684                 }
685
686                 // Set table privileges, using the groups we just added:
687                 progress();
688                 if (!setTablePrivilegesGroupsFromDocument(document)) {
689                         // TODO: std::cerr << G_STRFUNC << ": set_table_privileges_groups_from_document() failed." << std::endl;
690                         return false;
691                 }
692
693                 for (String tableName : tables) {
694                         // Add any example data to the table:
695                         progress();
696
697                         // try
698                         // {
699                         progress();
700                         final boolean tableInsertSucceeded = insertExampleData(connection, document, tableName);
701
702                         if (!tableInsertSucceeded) {
703                                 // TODO: std::cerr << G_STRFUNC << ": INSERT of example data failed with the newly-created database." <<
704                                 // std::endl;
705                                 return false;
706                         }
707                         // }
708                         // catch(final std::exception& ex)
709                         // {
710                         // std::cerr << G_STRFUNC << ": exception: " << ex.what() << std::endl;
711                         // HandleError(ex);
712                         // }
713
714                 } // for(tables)
715
716                 return true; // All tables created successfully.
717         }
718
719         /**
720          * @return
721          * @throws SQLException
722          */
723         private Connection createConnection() {
724                 Properties connectionProps = new Properties();
725                 connectionProps.put("user", this.username);
726                 connectionProps.put("password", this.password);
727
728                 String jdbcURL = "jdbc:postgresql://" + document.getConnectionServer() + ":" + document.getConnectionPort();
729                 String db = document.getConnectionDatabase();
730                 if (StringUtils.isEmpty(db)) {
731                         // Use the default PostgreSQL database, because ComboPooledDataSource.connect() fails otherwise.
732                         db = "template1";
733                 }
734                 jdbcURL += "/" + db; // TODO: Quote the database name?
735
736                 Connection conn = null;
737                 try {
738                         conn = DriverManager.getConnection(jdbcURL + "/", connectionProps);
739                 } catch (SQLException e) {
740                         // TODO Auto-generated catch block
741                         // e.printStackTrace();
742                         return null;
743                 }
744
745                 return conn;
746         }
747
748         /**
749          *
750          */
751         private void progress() {
752                 // TODO Auto-generated method stub
753
754         }
755
756         /**
757          * @param document
758          * @param tableName
759          * @return
760          */
761         private boolean insertExampleData(final Connection connection, final Document document, final String tableName) {
762
763                 final Factory factory = new Factory(connection, SQLDialect.POSTGRES);
764                 final Table<Record> table = Factory.tableByName(tableName);
765
766                 final List<Map<String, DataItem>> exampleRows = document.getExampleRows(tableName);
767                 for (final Map<String, DataItem> row : exampleRows) {
768                         InsertSetStep<Record> insertStep = factory.insertInto(table);
769
770                         for (final Entry<String, DataItem> entry : row.entrySet()) {
771                                 final String fieldName = entry.getKey();
772                                 final DataItem value = entry.getValue();
773                                 if (value == null) {
774                                         continue;
775                                 }
776
777                                 final Field field = document.getField(tableName, fieldName);
778                                 if (field == null) {
779                                         continue;
780                                 }
781
782                                 final org.jooq.Field<Object> jooqField = Factory.fieldByName(field.getName());
783                                 if (jooqField == null) {
784                                         continue;
785                                 }
786
787                                 final Object fieldValue = value.getValue(field.getGlomType());
788                                 insertStep = insertStep.set(jooqField, fieldValue);
789                         }
790
791                         if (!(insertStep instanceof InsertResultStep<?>)) {
792                                 continue;
793                         }
794
795                         // We suppress the warning because we _do_ check the cast above.
796                         @SuppressWarnings("unchecked")
797                         final InsertResultStep<Record> insertResultStep = (InsertResultStep<Record>) insertStep;
798
799                         try {
800                                 final Record record = insertResultStep.fetchOne();
801                         } catch (DataAccessException e) {
802                                 // e.printStackTrace();
803                                 return false;
804                         }
805                         // TODO: Check that it worked.
806                 }
807
808                 return true;
809         }
810
811         /**
812          * @param document2
813          * @return
814          */
815         private boolean setTablePrivilegesGroupsFromDocument(Document document2) {
816                 // TODO Auto-generated method stub
817                 return true;
818         }
819
820         /**
821          * @param document2
822          * @return
823          */
824         private boolean addGroupsFromDocument(Document document2) {
825                 // TODO Auto-generated method stub
826                 return true;
827         }
828
829         /**
830          * @param document
831          * @param tableName
832          * @param fields
833          * @return
834          */
835         private boolean createTable(final Connection connection, final Document document, final String tableName,
836                         final List<Field> fields) {
837                 boolean tableCreationSucceeded = false;
838
839                 /*
840                  * TODO: //Create the standard field too: //(We don't actually use this yet) if(std::find_if(fields.begin(),
841                  * fields.end(), predicate_FieldHasName<Field>(GLOM_STANDARD_FIELD_LOCK)) == fields.end()) { sharedptr<Field>
842                  * field = sharedptr<Field>::create(); field->set_name(GLOM_STANDARD_FIELD_LOCK);
843                  * field->set_glom_type(Field::TYPE_TEXT); fields.push_back(field); }
844                  */
845
846                 // Create SQL to describe all fields in this table:
847                 String sqlFields = "";
848                 for (Field field : fields) {
849                         // Create SQL to describe this field:
850                         String sqlFieldDescription = escapeSqlId(field.getName()) + " " + field.getSqlType();
851
852                         if (field.getPrimaryKey())
853                                 sqlFieldDescription += " NOT NULL  PRIMARY KEY";
854
855                         // Append it:
856                         if (!StringUtils.isEmpty(sqlFields)) {
857                                 sqlFields += ", ";
858                         }
859
860                         sqlFields += sqlFieldDescription;
861                 }
862
863                 if (StringUtils.isEmpty(sqlFields)) {
864                         // TODO: std::cerr << G_STRFUNC << ": sql_fields is empty." << std::endl;
865                 }
866
867                 // Actually create the table
868                 final String query = "CREATE TABLE " + escapeSqlId(tableName) + " (" + sqlFields + ");";
869                 Factory factory = new Factory(connection, SQLDialect.POSTGRES);
870                 final int result = factory.execute(query);
871                 tableCreationSucceeded = true;
872                 if (!tableCreationSucceeded) {
873                         // TODO: Warn: std::cerr << G_STRFUNC << ": CREATE TABLE failed." << std::endl;
874                 }
875
876                 return tableCreationSucceeded;
877         }
878
879         /**
880          * @param name
881          * @return
882          */
883         private String escapeSqlId(String name) {
884                 // TODO:
885                 return "\"" + name + "\"";
886         }
887
888         /**
889          * @return
890          */
891         private static boolean createDatabase(final Connection connection, final String databaseName) {
892
893                 final String query = "CREATE DATABASE \"" + databaseName + "\""; // TODO: Escaping.
894                 Factory factory = new Factory(connection, SQLDialect.POSTGRES);
895
896                 final int result = factory.execute(query);
897
898                 return true;
899         }
900
901         /**
902          *
903          */
904         public void cleanup() {
905
906                 // Stop the server:
907                 if ((document != null) && (document.getConnectionPort() != 0)) {
908                         final String dbDirData = getSelfHostingDataPath(false);
909
910                         // -D specifies the data directory.
911                         // -c config_file= specifies the configuration file
912                         // -k specifies a directory to use for the socket. This must be writable by us.
913                         // We use "-m fast" instead of the default "-m smart" because that waits for clients to disconnect (and
914                         // sometimes never succeeds).
915                         // TODO: Warn about connected clients on other computers? Warn those other users?
916                         // Make sure to use double quotes for the executable path, because the
917                         // CreateProcess() API used on Windows does not support single quotes.
918                         final ProcessBuilder commandPostgresStop = new ProcessBuilder(getPathToPostgresExecutable("pg_ctl"), "-D"
919                                         + shellQuote(dbDirData), "stop", "-m", "fast");
920                         final boolean result = executeCommandLineAndWait(commandPostgresStop);
921                         if (!result) {
922                                 // TODO: Warn
923                                 // return;
924                         }
925
926                         document.setConnectionPort(0);
927                 }
928
929                 // Delete the files:
930                 final String selfhostingPath = getSelfHostingPath("", false);
931                 final File fileSelfHosting = new File(selfhostingPath);
932                 fileSelfHosting.delete();
933
934                 final String docPath = document.getFileURI();
935                 final File fileDoc = new File(docPath);
936                 fileDoc.delete();
937         }
938 }