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