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