Replace deprecated DOM.setElementAttribute().
[online-glom:gwt-glom.git] / src / test / java / org / glom / web / server / SelfHosterMySQL.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.File;
23 import java.io.IOException;
24 import java.sql.Connection;
25 import java.sql.DriverManager;
26 import java.sql.SQLException;
27 import java.util.ArrayList;
28 import java.util.List;
29 import java.util.Properties;
30
31 import org.apache.commons.lang3.StringUtils;
32 import org.glom.web.server.libglom.Document;
33 import org.glom.web.shared.libglom.Field;
34 import org.jooq.SQLDialect;
35 import org.jooq.exception.DataAccessException;
36 import org.jooq.impl.Factory;
37
38 import com.google.common.io.Files;
39
40 /**
41  * @author Murray Cumming <murrayc@murrayc.com>
42  * 
43  */
44 public class SelfHosterMySQL extends SelfHoster {
45         SelfHosterMySQL(final Document document) {
46                 super(document);
47         }
48
49         private static final int PORT_MYSQL_SELF_HOSTED_START = 3306;
50         private static final int PORT_MYSQL_SELF_HOSTED_END = 3350;
51         
52         private static final String FILENAME_DATA = "data";
53         
54         private static final String DEFAULT_DATABASE_NAME = "INFORMATION_SCHEMA";
55         
56         private int port;
57           
58         //These are remembered in order to use them to issue the shutdown command via mysqladmin:
59         private String savedUsername;
60         private String savedPassword;
61         
62         boolean temporaryPasswordActive; //Whether the password is an initial temporary one.
63         private String initialPasswordToSet;
64         private String initialUsernameToSet;
65         private String temporaryPassword;
66
67         /**
68          * @param document
69          * @param
70          * @param subDirectoryPath
71          * @return
72          * @Override
73          */
74         protected boolean createAndSelfHostNewEmpty() { 
75                 final File tempDir = saveDocumentCopy(Document.HostingMode.HOSTING_MODE_MYSQL_SELF);
76
77                 // We must specify a default username and password:
78                 final String user = "glom_dev_user"; //Shorter than what we use for MYSQL, to satisfy MYSQL.
79                 final String password = "glom_default_developer_password";
80
81                 // Create the self-hosting files:
82                 if (!initialize(user, password)) {
83                         System.out.println("createAndSelfHostNewEmpty(): initialize failed.");
84                         // TODO: Delete directory.
85                 }
86
87                 // Check that it really created some files:
88                 if (!tempDir.exists()) {
89                         System.out.println("createAndSelfHostNewEmpty(): tempDir does not exist.");
90                         // TODO: Delete directory.
91                 }
92
93                 return selfHost(user, password);
94         }
95
96         /**
97          * @param document
98          * @param user
99          * @param password
100          * @return
101          * @Override
102          */
103         private boolean selfHost(final String user, final String password) {
104                 if (getSelfHostingActive()) {
105                         // TODO: std::cerr << G_STRFUNC << ": Already started." << std::endl;
106                         return false; // STARTUPERROR_NONE; //Just do it once.
107                 }
108
109                 final String dbDirData = getSelfHostingDataPath(false);
110                 if (StringUtils.isEmpty(dbDirData) || !SelfHoster.fileExists(dbDirData)) {
111                         /*
112                          * final String dbDirBackup = dbDir + File.separator + FILENAME_BACKUP;
113                          * 
114                          * if(fileExists(dbDirBackup)) { //TODO: std::cerr << G_STRFUNC <<
115                          * ": There is no data, but there is backup data." << std::endl; //Let the caller convert the backup to real
116                          * data and then try again: return false; // STARTUPERROR_FAILED_NO_DATA_HAS_BACKUP_DATA; } else {
117                          */
118                         // TODO: std::cerr << "ConnectionPool::create_self_hosting(): The data sub-directory could not be found." <<
119                         // dbdir_data_uri << std::endl;
120                         return false; // STARTUPERROR_FAILED_NO_DATA;
121                         // }
122                 }
123
124                 final int availablePort = SelfHoster.discoverFirstFreePort(PORT_MYSQL_SELF_HOSTED_START,
125                                 PORT_MYSQL_SELF_HOSTED_END);
126
127                 if (availablePort == 0) {
128                         // TODO: Use a return enum or exception so we can tell the user about this:
129                         // TODO: std::cerr << G_STRFUNC << ": No port was available between " << PORT_MYSQL_SELF_HOSTED_START
130                         // << " and " << PORT_MYSQL_SELF_HOSTED_END << std::endl;
131                         return false; // STARTUPERROR_FAILED_UNKNOWN_REASON;
132                 }
133
134                 final String portAsText = portNumberAsText(availablePort);
135                 final String dbDirPid = getSelfHostingPath("pid", false);
136                 final String dbDirSocket = getSelfHostingPath("mysqld.sock", false);
137
138                 final String commandPathStart = getPathToMysqlExecutable("mysqld_safe");
139                 if (StringUtils.isEmpty(commandPathStart)) {
140                         System.out.println("selfHost(): getPathToMysqlExecutable(mysqld_safe) failed.");
141                         return false;
142                 }
143
144                 final ProcessBuilder commandMysqlStart = new ProcessBuilder(commandPathStart,
145                                 "--no-defaults",
146                                 "--port=" + portAsText,
147                                 "--datadir=" + shellQuote(dbDirData),
148                                 "--socket=" + shellQuote(dbDirSocket),
149                                 "--pid-file=" + shellQuote(dbDirPid));
150
151
152                 port = availablePort; //Needed by getMysqlAdminCommand().
153                  
154                 final List<String> progAndArgsCheck = getMysqlAdminCommand(savedUsername, savedPassword);
155                 progAndArgsCheck.add("ping");
156                 final ProcessBuilder commandCheckMysqlHasStarted = new ProcessBuilder(progAndArgsCheck);
157
158                 final String secondCommandSuccessText = "mysqld is alive"; // TODO: This is not a stable API. Also, watch out for
159                                                                                                                                 // localisation.
160
161                 // The first command does not return, but the second command can check whether it succeeded:
162                 // TODO: Progress
163                 final boolean result = executeCommandLineAndWaitUntilSecondCommandReturnsSuccess(commandMysqlStart,
164                                 commandCheckMysqlHasStarted, secondCommandSuccessText);
165                 if (!result) {
166                         System.out.println("selfHost(): Error while attempting to self-host a database.");
167                         return false; // STARTUPERROR_FAILED_UNKNOWN_REASON;
168                 }
169
170                 // Remember the port for later:
171                 port = availablePort; //Remember it for later.
172                 document.setConnectionPort(availablePort);
173
174                 // Check that we can really connect:
175                 
176                 //Sleep for a fairly long time initially to avoid distracting error messages when trying to connect,
177                 //while the database server is still starting up.
178                 try {
179                         Thread.sleep(5000);
180                 } catch (InterruptedException e) {
181                         // TODO Auto-generated catch block
182                         e.printStackTrace();
183                 }
184
185                 // mysqladmin could report success before it is really ready to let us connect,
186                 // so in this case we can just keep trying until it works, for a while:
187                 for (int i = 0; i < 10; i++) {
188
189                         try {
190                                 Thread.sleep(1000);
191                         } catch (InterruptedException e) {
192                                 // TODO Auto-generated catch block
193                                 e.printStackTrace();
194                         }
195
196                         final String dbName = document.getConnectionDatabase();
197                         document.setConnectionDatabase(""); // We have not created the database yet.
198
199                         //Check that we can connect:
200                         final Connection connection = createConnection(false);
201                         document.setConnectionDatabase(dbName);
202                         if (connection != null) {
203                                 //Close the connection:
204                                 try {
205                                         connection.close();
206                                 } catch (SQLException e) {
207                                         // TODO Auto-generated catch block
208                                         e.printStackTrace();
209                                 }
210
211                                 System.out.println("selfHost(): Connection succeeded after retries=" + i);
212                                 
213                                 if (temporaryPasswordActive) {
214                                         return setInitialUsernameAndPassword();
215                                 }
216
217                                 return true; // STARTUPERROR_NONE;
218                         }
219
220                         /*
221                         System.out
222                                         .println("selfHost(): Waiting and retrying the connection due to suspected too-early success of pg_ctl. retries="
223                                                         + i);
224                         */
225                 }
226
227                 System.out.println("selfHost(): Test connection failed after multiple retries.");
228                 return false;
229         }
230
231         /**
232          * @return 
233          * 
234          */
235         private boolean setInitialUsernameAndPassword() {
236                 //If necessary, set the initial root password and rename the root user:
237                 if(!temporaryPasswordActive)
238                         return true;
239                 
240                 if (StringUtils.isEmpty(initialUsernameToSet)) {
241                         System.out.println("setInitialUsernameAndPassword(): initialUsernameToSet is empty.");
242                         return false;
243                 }
244
245                 if (StringUtils.isEmpty(initialPasswordToSet)) {
246                         System.out.println("setInitialUsernameAndPassword(): initialPasswordToSet is empty.");
247                         return false;
248                 }
249                 
250                 //Set the root password:
251                 final List<String> progAndArgs = getMysqlAdminCommand("root", temporaryPassword);
252                 progAndArgs.add("password");
253                 progAndArgs.add(shellQuote(initialPasswordToSet));
254
255                 final ProcessBuilder commandInitdbSetInitialPassword = new ProcessBuilder(progAndArgs);
256                 final boolean result = executeCommandLineAndWait(commandInitdbSetInitialPassword);
257                 
258                 if(!result) {
259                         System.out.println("setInitialUsernameAndPassword(): commandInitdbSetInitialPassword failed.");
260                         return false;
261                 }
262
263                 temporaryPasswordActive = false;
264                 temporaryPassword = null;
265
266                 //Rename the root user,
267                 //so we can connnect as the expected username:
268                 //We connect to the INFORMATION_SCHEMA database, because libgda needs us to specify some database.
269                 final Connection connection = createConnection(DEFAULT_DATABASE_NAME, "root", initialPasswordToSet, false);
270                 if (connection == null) {
271                         //std::cerr << G_STRFUNC << "Error while attempting to start self-hosting MYSQL database, when setting the initial username: connection failed." << std::endl;
272                         return false;
273                 }
274
275                 savedPassword = initialPasswordToSet;
276
277                 final String query = buildQueryChangeUsername(connection, "root", initialUsernameToSet);
278
279                 try {
280                         SqlUtils.executeUpdate(connection, query);
281                 } catch (final SQLException e) {
282                         System.out.println("setInitialUsernameAndPassword(): change username query failed: " + query.toString());
283                         e.printStackTrace();
284                         return false;
285                 } finally {
286                         // cleanup everything that has been used
287                         try {
288                                 // TODO: If we use the ResultSet.
289                         } catch (final Exception e) {
290                                 return false;
291                         }
292                 }
293
294                 savedUsername = initialUsernameToSet;
295                 initialUsernameToSet = null;
296
297                 return true;
298         }
299
300         /**
301          * @param connection
302          * @param string
303          * @param initialUsernameToSet2
304          * @return
305          */
306         private String buildQueryChangeUsername(Connection connection, String oldUsername, String newUsername) {
307                 if (StringUtils.isEmpty(oldUsername)) {
308                         //std::cerr << G_STRFUNC << ": old_username is empty." << std::endl;
309                         return "";
310                 }
311
312                 if (StringUtils.isEmpty(newUsername)) {
313                         //std::cerr << G_STRFUNC << ": new_username is empty." << std::endl;
314                         return "";
315                 }
316
317                 //TODO: Try to avoid specifing @localhost.
318                 //We do this to avoid this error:
319                 //mysql> RENAME USER root TO glom_dev_user;
320                 //ERROR 1396 (HY000): Operation RENAME USER failed for 'root'@'%'
321                 //mysql> RENAME USER root@localhost TO glom_dev_user;
322                 //Query OK, 0 rows affected (0.00 sec)
323                 final String user = quoteAndEscapeSqlId(oldUsername) + "@localhost";
324
325                 //Login will fail after restart if we don't specify @localhost here too:
326                 final String newUser = quoteAndEscapeSqlId(newUsername) + "@localhost";
327
328                 return "RENAME USER " + user + " TO " + newUser;
329         }
330
331         private String getSelfHostingPath(final String subpath, final boolean create) {
332                 final String dbDir = document.getSelfHostedDirectoryPath();
333                 if (StringUtils.isEmpty(dbDir)) {
334                         System.out.println("getSelfHostingPath(): getSelfHostedDirectoryPath returned no path.");
335                         return null;
336                 }
337
338                 if (StringUtils.isEmpty(subpath)) {
339                         return dbDir;
340                 }
341
342                 final String dbDirData = dbDir + File.separator + subpath;
343                 final File file = new File(dbDirData);
344
345                 // Return the path regardless of whether it exists:
346                 if (!create) {
347                         return dbDirData;
348                 }
349
350                 if (!file.exists()) {
351                         try {
352                                 Files.createParentDirs(file);
353                         } catch (final IOException e) {
354                                 // TODO Auto-generated catch block
355                                 e.printStackTrace();
356                                 return "";
357                         }
358
359                         if (!file.mkdir()) {
360                                 return "";
361                         }
362                 }
363
364                 return dbDirData;
365         }
366
367         private String getSelfHostingDataPath(final boolean create) {
368                 return getSelfHostingPath(FILENAME_DATA, create);
369         }
370
371         /**
372          * @param string
373          * @return
374          */
375         private static String getPathToMysqlExecutable(final String string) {
376                 final List<String> dirPaths = new ArrayList<String>();
377                 dirPaths.add("/usr/bin");
378
379                 for (String dir : dirPaths) {
380                         final String path = dir + File.separator + string;
381                         if (fileExistsAndIsExecutable(path)) {
382                                 return path;
383                         }
384                 }
385
386                 return "";
387         }
388         
389         private List<String> getMysqlAdminCommand(final String username, final String password) {
390                 if (StringUtils.isEmpty(username)) {
391                         return null;
392                 }
393                 
394                 final String portAsText = portNumberAsText(port);
395
396                 final List<String> progAndArgs = new ArrayList<String>();
397                 progAndArgs.add(getPathToMysqlExecutable("mysqladmin"));
398                 progAndArgs.add("--no-defaults");
399                 progAndArgs.add("--port=" + portAsText);
400                 progAndArgs.add("--protocol=tcp"); //Otherwise we cannot connect as root. TODO: However, maybe we could use --skip-networking if network sharing is not enabled.
401                 progAndArgs.add("--user=" + shellQuote(username));
402
403                 //--password='' is not always interpreted the same as specifying no --password.
404                 if(!StringUtils.isEmpty(password)) {
405                         progAndArgs.add("--password=" + shellQuote(password));
406                 }
407
408                 return progAndArgs;
409         }
410
411         /**
412          * @param cpds
413          * @return
414          */
415         private boolean initialize(final String initialUsername, final String initialPassword) {
416                 if (StringUtils.isEmpty(initialUsername)) {
417                         System.out.println("initialize(): initialUsername is empty.");
418                         return false;
419                 }
420
421                 if (StringUtils.isEmpty(initialPassword)) {
422                         System.out.println("initialize(): initialPassword is empty.");
423                         return false;
424                 }
425                 
426                 // mysql_install_db creates a new mysql database cluster:
427
428                 // Get file:// URI for the tmp/ directory:
429
430                 // Make sure to use double quotes for the executable path, because the
431                 // CreateProcess() API used on Windows does not support single quotes.
432                 final String dbDirData = getSelfHostingDataPath(false /* create */);
433                 if(dbDirData.isEmpty()) {
434                         System.out.println("initialize(): getSelfHostingDataPath returned no path.");
435                         return false;
436                 }
437
438                 // Note that initdb returns this error if we split the arguments more,
439                 // for instance splitting -D and dbDirData into separate strings:
440                 // too many command-line arguments (first is "(null)")
441                 // TODO: If we quote tempPwFile then initdb says that it cannot find it.
442                 // Note: If we use "-D " instead of "-D" then the initdb seems to make the space part of the filepath,
443                 // though that does not happen with the normal command line.
444                 boolean result = false;
445                 final String commandPath = getPathToMysqlExecutable("mysql_install_db");
446                 if (StringUtils.isEmpty(commandPath)) {
447                         System.out.println("initialize(): getPathToMysqlExecutable(mysql_install_db) failed.");
448                 } else {
449                         final ProcessBuilder commandInitdb = new ProcessBuilder(commandPath, "--no-defaults", "--datadir=" + shellQuote(dbDirData));
450
451                         // Note that --pwfile takes the password from the first line of a file. It's an alternative to supplying it
452                         // when
453                         // prompted on stdin.
454                         result = executeCommandLineAndWait(commandInitdb);
455                 }
456
457                 if (!result) {
458                         System.out.println("initialize(): Error while attempting to create self-hosting database.");
459                         return false;
460                 }
461
462                 //This is used during the first start:
463                 initialPasswordToSet = initialPassword;
464             initialUsernameToSet = initialUsername;
465
466             //TODO: With MYSQL 5.6, use the new --random-passwords option (see above)
467             temporaryPassword = "";
468             temporaryPasswordActive = true;
469             savedUsername = "root";
470             savedPassword = "";
471             username = "root";
472
473                 return result; // ? INITERROR_NONE : INITERROR_COULD_NOT_START_SERVER;
474         }
475
476         /**
477          * @param document
478          * @return
479          */
480         protected boolean recreateDatabaseFromDocument() {
481                 // Check whether the database exists already.
482                 final String dbName = document.getConnectionDatabase();
483                 if (StringUtils.isEmpty(dbName)) {
484                         return false;
485                 }
486
487                 document.setConnectionDatabase(dbName);
488                 Connection connection = createConnection(true);
489                 if (connection != null) {
490                         // Connection to the database succeeded, so the database
491                         // exists already.
492                         try {
493                                 connection.close();
494                         } catch (final SQLException e) {
495                                 // TODO Auto-generated catch block
496                                 e.printStackTrace();
497                         }
498                         return false;
499                 }
500
501                 // Create the database:
502                 progress();
503                 document.setConnectionDatabase("");
504
505                 connection = createConnection(false);
506                 if (connection == null) {
507                         System.out.println("recreatedDatabase(): createConnection() failed, before creating the database.");
508                         return false;
509                 }
510
511                 final boolean dbCreated = createDatabase(connection, dbName);
512
513                 if (!dbCreated) {
514                         return false;
515                 }
516
517                 progress();
518
519                 // Check that we can connect:
520                 try {
521                         connection.close();
522                 } catch (final SQLException e) {
523                         // TODO Auto-generated catch block
524                         e.printStackTrace();
525                 }
526                 connection = null;
527
528                 document.setConnectionDatabase(dbName);
529                 connection = createConnection(false);
530                 if (connection == null) {
531                         System.out.println("recreatedDatabase(): createConnection() failed, after creating the database.");
532                         return false;
533                 }
534
535                 progress();
536
537                 // Create each table:
538                 final List<String> tables = document.getTableNames();
539                 for (final String tableName : tables) {
540
541                         // Create SQL to describe all fields in this table:
542                         final List<Field> fields = document.getTableFields(tableName);
543
544                         progress();
545                         final boolean tableCreationSucceeded = createTable(connection, document, tableName, fields);
546                         progress();
547                         if (!tableCreationSucceeded) {
548                                 // TODO: std::cerr << G_STRFUNC << ": CREATE TABLE failed with the newly-created database." <<
549                                 // std::endl;
550                                 return false;
551                         }
552                 }
553
554                 // Note that create_database() has already called add_standard_tables() and add_standard_groups(document).
555
556                 // Add groups from the document:
557                 progress();
558                 if (!addGroupsFromDocument(document)) {
559                         // TODO: std::cerr << G_STRFUNC << ": add_groups_from_document() failed." << std::endl;
560                         return false;
561                 }
562
563                 // Set table privileges, using the groups we just added:
564                 progress();
565                 if (!setTablePrivilegesGroupsFromDocument(document)) {
566                         // TODO: std::cerr << G_STRFUNC << ": set_table_privileges_groups_from_document() failed." << std::endl;
567                         return false;
568                 }
569
570                 for (final String tableName : tables) {
571                         // Add any example data to the table:
572                         progress();
573
574                         // try
575                         // {
576                         progress();
577                         final boolean tableInsertSucceeded = insertExampleData(connection, document, tableName);
578
579                         if (!tableInsertSucceeded) {
580                                 // TODO: std::cerr << G_STRFUNC << ": INSERT of example data failed with the newly-created database." <<
581                                 // std::endl;
582                                 return false;
583                         }
584                         // }
585                         // catch(final std::exception& ex)
586                         // {
587                         // std::cerr << G_STRFUNC << ": exception: " << ex.what() << std::endl;
588                         // HandleError(ex);
589                         // }
590
591                 } // for(tables)
592
593                 return true; // All tables created successfully.
594         }
595
596         public Connection createConnection(final String database, final String username, final String password, boolean failureExpected) {
597                 //We don't just use SqlUtils.tryUsernameAndPassword() because it uses ComboPooledDataSource,
598                 //which does not automatically close its connections,
599                 //leading to errors because connections are already open.
600                 
601                 if(StringUtils.isEmpty(username)) {
602                         System.out.println("setInitialUsernameAndPassword(): username is empty.");
603                         return null;
604                 }
605
606                 final SqlUtils.JdbcConnectionDetails details = SqlUtils.getJdbcConnectionDetails(document.getHostingMode(), document.getConnectionServer(), document.getConnectionPort(), database);
607                 if (details == null) {
608                         System.out.println("setInitialUsernameAndPassword(): getJdbcConnectionDetails() returned null.");
609                         return null;
610                 }
611                 
612                 final Properties connectionProps = new Properties();
613                 connectionProps.put("user", username);
614                 connectionProps.put("password", password);
615
616                 Connection conn = null;
617                 try {
618                         //TODO: Remove these debug prints when we figure out why getConnection sometimes hangs. 
619                         //System.out.println("debug: SelfHosterMySQL.createConnection(): before createConnection()");
620                         DriverManager.setLoginTimeout(10);
621                         conn = DriverManager.getConnection(details.jdbcURL, connectionProps);
622                         //System.out.println("debug: SelfHosterMySQL.createConnection(): before createConnection()");
623                 } catch (final SQLException e) {
624                         if(!failureExpected) {
625                                 e.printStackTrace();
626                         }
627                         return null;
628                 }
629
630                 return conn;
631         }
632         
633         /**
634          */
635         public Connection createConnection(boolean failureExpected) {
636                 final String db = document.getConnectionDatabase();
637                 return createConnection(db, this.username, this.password, failureExpected);
638         }
639
640         /**
641          *
642          */
643         private void progress() {
644                 // TODO Auto-generated method stub
645
646         }
647
648         /**
649          * @param document
650          * @param tableName
651          * @param fields
652          * @return
653          */
654         private boolean createTable(final Connection connection, final Document document, final String tableName,
655                         final List<Field> fields) {
656                 boolean tableCreationSucceeded = false;
657
658                 /*
659                  * TODO: //Create the standard field too: //(We don't actually use this yet) if(std::find_if(fields.begin(),
660                  * fields.end(), predicate_FieldHasName<Field>(GLOM_STANDARD_FIELD_LOCK)) == fields.end()) { sharedptr<Field>
661                  * field = sharedptr<Field>::create(); field->set_name(GLOM_STANDARD_FIELD_LOCK);
662                  * field->set_glom_type(Field::TYPE_TEXT); fields.push_back(field); }
663                  */
664
665                 // Create SQL to describe all fields in this table:
666                 String sqlFields = "";
667                 for (final Field field : fields) {
668                         // Create SQL to describe this field:
669                         String sqlFieldDescription = quoteAndEscapeSqlId(field.getName()) + " " + field.getSqlType(Field.SqlDialect.MYSQL);
670
671                         if (field.getPrimaryKey()) {
672                                 sqlFieldDescription += " NOT NULL  PRIMARY KEY";
673                         }
674
675                         // Append it:
676                         if (!StringUtils.isEmpty(sqlFields)) {
677                                 sqlFields += ", ";
678                         }
679
680                         sqlFields += sqlFieldDescription;
681                 }
682
683                 if (StringUtils.isEmpty(sqlFields)) {
684                         // TODO: std::cerr << G_STRFUNC << ": sql_fields is empty." << std::endl;
685                 }
686
687                 // Actually create the table
688                 final String query = "CREATE TABLE " + quoteAndEscapeSqlId(tableName) + " (" + sqlFields + ");";
689                 final Factory factory = new Factory(connection, getSqlDialect());
690                 try {
691                         factory.execute(query);
692                 } catch (DataAccessException e) {
693                         System.out.println("createDatabase(): query failed: " + query);
694                         e.printStackTrace();
695                         return false;
696                 }
697
698                 tableCreationSucceeded = true;
699                 if (!tableCreationSucceeded) {
700                         System.out.println("recreatedDatabase(): CREATE TABLE() failed.");
701                 }
702
703                 return tableCreationSucceeded;
704         }
705
706         /**
707          * @param name
708          * @return
709          */
710         private static String quoteAndEscapeSqlId(final String name) {
711                 return quoteAndEscapeSqlId(name, SQLDialect.MYSQL);
712         }
713
714         /**
715          * @return
716          */
717         private static boolean createDatabase(final Connection connection, final String databaseName) {
718                 final String query = "CREATE DATABASE  " + quoteAndEscapeSqlId(databaseName);
719                 final Factory factory = new Factory(connection, SQLDialect.MYSQL);
720
721                 try {
722                         factory.execute(query);
723                 } catch (DataAccessException e) {
724                         System.out.println("createDatabase(): query failed: " + query);
725                         e.printStackTrace();
726                         return false;
727                 }
728
729                 return true;
730         }
731
732         /**
733          *
734          */
735         public boolean cleanup() {
736                 boolean result = true;
737
738                 // Stop the server:
739                 if ((document != null) && (document.getConnectionPort() != 0)) {
740                         final List<String> progAndArgs = getMysqlAdminCommand(savedUsername, savedPassword);
741                         progAndArgs.add("shutdown");
742
743                         final ProcessBuilder commandMySQLStop = new ProcessBuilder(progAndArgs);
744                         result = executeCommandLineAndWait(commandMySQLStop);
745                         if (!result) {
746                                 System.out.println("cleanup(): Failed to stop the MYSQL server.");
747                         }
748
749                         document.setConnectionPort(0);
750                 }
751
752                 // Delete the files:
753                 final String selfhostingPath = getSelfHostingPath("", false);
754                 final File fileSelfHosting = new File(selfhostingPath);
755                 fileSelfHosting.delete();
756
757                 final String docPath = document.getFileURI();
758                 final File fileDoc = new File(docPath);
759                 fileDoc.delete();
760
761                 return result;
762         }
763         
764         @Override
765         public SQLDialect getSqlDialect() {
766                 return SQLDialect.MYSQL;
767         }
768 }