Remove some duplicate code.
[online-glom:gwt-glom.git] / src / main / java / org / glom / web / server / OnlineGlomServiceImpl.java
1 /*
2  * Copyright (C) 2010, 2011 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.FilenameFilter;
24 import java.io.InputStream;
25 import java.sql.Connection;
26 import java.sql.SQLException;
27 import java.util.ArrayList;
28 import java.util.Hashtable;
29 import java.util.List;
30 import java.util.Properties;
31
32 import javax.servlet.ServletException;
33
34 import org.apache.commons.io.FileUtils;
35 import org.apache.commons.io.FilenameUtils;
36 import org.apache.commons.lang.StringUtils;
37 import org.glom.libglom.BakeryDocument.LoadFailureCodes;
38 import org.glom.libglom.Document;
39 import org.glom.libglom.Glom;
40 import org.glom.libglom.Report;
41 import org.glom.web.client.OnlineGlomService;
42 import org.glom.web.shared.DataItem;
43 import org.glom.web.shared.DetailsLayoutAndData;
44 import org.glom.web.shared.DocumentInfo;
45 import org.glom.web.shared.Documents;
46 import org.glom.web.shared.NavigationRecord;
47 import org.glom.web.shared.Reports;
48 import org.glom.web.shared.TypedDataItem;
49 import org.glom.web.shared.layout.LayoutGroup;
50
51 import com.google.gwt.user.server.rpc.RemoteServiceServlet;
52 import com.mchange.v2.c3p0.DataSources;
53
54 /**
55  * This is the servlet class for setting up the server side of Online Glom. The client side can call the public methods
56  * in this class via OnlineGlom
57  * 
58  * For instance, it loads all the available documents and provide a list - see getDocuments(). It then provides
59  * information from each document. For instance, see getListViewLayout().
60  * 
61  * TODO: Watch for changes to the .glom files, to reload new versions and to load newly-added files. TODO: Watch for
62  * changes to the properties (configuration)?
63  */
64 @SuppressWarnings("serial")
65 public class OnlineGlomServiceImpl extends RemoteServiceServlet implements OnlineGlomService {
66
67         private static final String GLOM_FILE_EXTENSION = "glom";
68
69         // convenience class for dealing with the Online Glom configuration file
70         private class OnlineGlomProperties extends Properties {
71                 public String getKey(final String value) {
72                         for (final String key : stringPropertyNames()) {
73                                 if (getProperty(key).trim().equals(value))
74                                         return key;
75                         }
76                         return null;
77                 }
78         }
79
80         private final Hashtable<String, ConfiguredDocument> documentMapping = new Hashtable<String, ConfiguredDocument>();
81         private Exception configurationException = null;
82
83         /*
84          * This is called when the servlet is started or restarted.
85          * 
86          * (non-Javadoc)
87          * 
88          * @see javax.servlet.GenericServlet#init()
89          */
90         @Override
91         public void init() throws ServletException {
92
93                 // All of the initialisation code is surrounded by a try/catch block so that the servlet can be in an
94                 // initialised state and the error message can be retrieved by the client code.
95                 try {
96                         // Find the configuration file. See this thread for background info:
97                         // http://stackoverflow.com/questions/2161054/where-to-place-properties-files-in-a-jsp-servlet-web-application
98                         // FIXME move onlineglom.properties to the WEB-INF folder (option number 2 from the stackoverflow question)
99                         final OnlineGlomProperties config = new OnlineGlomProperties();
100                         final InputStream is = Thread.currentThread().getContextClassLoader()
101                                         .getResourceAsStream("onlineglom.properties");
102                         if (is == null) {
103                                 final String errorMessage = "onlineglom.properties not found.";
104                                 Log.fatal(errorMessage);
105                                 throw new Exception(errorMessage);
106                         }
107                         config.load(is); // can throw an IOException
108
109                         // check if we can read the configured glom file directory
110                         final String documentDirName = config.getProperty("glom.document.directory");
111                         final File documentDir = new File(documentDirName);
112                         if (!documentDir.isDirectory()) {
113                                 final String errorMessage = documentDirName + " is not a directory.";
114                                 Log.fatal(errorMessage);
115                                 throw new Exception(errorMessage);
116                         }
117                         if (!documentDir.canRead()) {
118                                 final String errorMessage = "Can't read the files in directory " + documentDirName + " .";
119                                 Log.fatal(errorMessage);
120                                 throw new Exception(errorMessage);
121                         }
122
123                         // get and check the glom files in the specified directory
124                         // TODO: Test this:
125                         final String[] extensions = { GLOM_FILE_EXTENSION };
126                         final List<File> glomFiles = (List<File>) FileUtils
127                                         .listFiles(documentDir, extensions, true /* recursive */);
128
129                         // don't continue if there aren't any Glom files to configure
130                         if (glomFiles.size() <= 0) {
131                                 final String errorMessage = "Unable to find any Glom documents in the configured directory "
132                                                 + documentDirName
133                                                 + " . Check the onlineglom.properties file to ensure that 'glom.document.directory' is set to the correct directory.";
134                                 Log.error(errorMessage);
135                                 throw new Exception(errorMessage);
136                         }
137
138                         // Check to see if the native library of java libglom is visible to the JVM
139                         if (!isNativeLibraryVisibleToJVM()) {
140                                 final String errorMessage = "The java-libglom shared library is not visible to the JVM."
141                                                 + " Ensure that 'java.library.path' is set with the path to the java-libglom shared library."
142                                                 + "\n: java.library.path: " + System.getProperty("java.library.path");
143                                 Log.error(errorMessage);
144                                 throw new Exception(errorMessage);
145                         }
146
147                         // Check for a specified default locale,
148                         // for table titles, field titles, etc:
149                         final String globalLocaleID = StringUtils.defaultString(config.getProperty("glom.document.locale"));
150
151                         // This initialisation method can throw an UnsatisfiedLinkError if the java-libglom native library isn't
152                         // available. But since we're checking for the error condition above, the UnsatisfiedLinkError will never be
153                         // thrown.
154                         Glom.libglom_init();
155
156                         // Allow a fake connection, so sqlbuilder_get_full_query() can work:
157                         Glom.set_fake_connection();
158
159                         for (final File glomFile : glomFiles) {
160                                 final Document document = new Document();
161                                 document.set_file_uri("file://" + glomFile.getAbsolutePath());
162                                 final int error = 0;
163                                 final boolean retval = document.load(error);
164                                 if (retval == false) {
165                                         String message;
166                                         if (LoadFailureCodes.LOAD_FAILURE_CODE_NOT_FOUND == LoadFailureCodes.swigToEnum(error)) {
167                                                 message = "Could not find file: " + glomFile.getAbsolutePath();
168                                         } else {
169                                                 message = "An unknown error occurred when trying to load file: " + glomFile.getAbsolutePath();
170                                         }
171                                         Log.error(message);
172                                         // continue with for loop because there may be other documents in the directory
173                                         continue;
174                                 }
175
176                                 final ConfiguredDocument configuredDocument = new ConfiguredDocument(document); // can throw a
177                                 // PropertyVetoException
178
179                                 // check if a username and password have been set and work for the current document
180                                 final String filename = glomFile.getName();
181                                 final String key = config.getKey(filename);
182                                 if (key != null) {
183                                         final String[] keyArray = key.split("\\.");
184                                         if (keyArray.length == 3 && "filename".equals(keyArray[2])) {
185                                                 // username/password could be set, let's check to see if it works
186                                                 final String usernameKey = key.replaceAll(keyArray[2], "username");
187                                                 final String passwordKey = key.replaceAll(keyArray[2], "password");
188                                                 configuredDocument.setUsernameAndPassword(config.getProperty(usernameKey).trim(),
189                                                                 config.getProperty(passwordKey)); // can throw an SQLException
190                                         }
191                                 }
192
193                                 // check the if the global username and password have been set and work with this document
194                                 if (!configuredDocument.isAuthenticated()) {
195                                         configuredDocument.setUsernameAndPassword(config.getProperty("glom.document.username").trim(),
196                                                         config.getProperty("glom.document.password")); // can throw an SQLException
197                                 }
198
199                                 if (!StringUtils.isEmpty(globalLocaleID)) {
200                                         configuredDocument.setDefaultLocaleID(globalLocaleID.trim());
201                                 }
202
203                                 // The key for the hash table is the file name without the .glom extension and with spaces ( ) replaced
204                                 // with pluses (+). The space/plus replacement makes the key more friendly for URLs.
205                                 final String documentID = FilenameUtils.removeExtension(filename).replace(' ', '+');
206                                 configuredDocument.setDocumentID(documentID);
207                                 documentMapping.put(documentID, configuredDocument);
208                         }
209
210                 } catch (final Exception e) {
211                         // Don't throw the Exception so that servlet will be initialised and the error message can be retrieved.
212                         configurationException = e;
213                 }
214
215         }
216
217         /**
218          * Checks if the java-libglom native library is visible to the JVM.
219          * 
220          * @return true if the java-libglom native library is visible to the JVM, false if it's not
221          */
222         private boolean isNativeLibraryVisibleToJVM() {
223                 final String javaLibraryPath = System.getProperty("java.library.path");
224
225                 // Go through all the library paths and check for the java_libglom .so.
226                 for (final String libDirName : javaLibraryPath.split(":")) {
227                         final File libDir = new File(libDirName);
228
229                         if (!libDir.isDirectory())
230                                 continue;
231                         if (!libDir.canRead())
232                                 continue;
233
234                         final File[] libs = libDir.listFiles(new FilenameFilter() {
235                                 @Override
236                                 public boolean accept(final File dir, final String name) {
237                                         return name.matches("libjava_libglom-[0-9]\\.[0-9].+\\.so");
238                                 }
239                         });
240
241                         // if at least one directory had the .so, we're done
242                         if (libs.length > 0)
243                                 return true;
244                 }
245
246                 return false;
247         }
248
249         /*
250          * This is called when the servlet is stopped or restarted.
251          * 
252          * @see javax.servlet.GenericServlet#destroy()
253          */
254         @Override
255         public void destroy() {
256                 Glom.libglom_deinit();
257
258                 for (final String documenTitle : documentMapping.keySet()) {
259                         final ConfiguredDocument configuredDoc = documentMapping.get(documenTitle);
260                         if (configuredDoc == null)
261                                 continue;
262
263                         try {
264                                 DataSources.destroy(configuredDoc.getCpds());
265                         } catch (final SQLException e) {
266                                 Log.error(documenTitle, "Error cleaning up the ComboPooledDataSource.", e);
267                         }
268                 }
269         }
270
271         /*
272          * (non-Javadoc)
273          * 
274          * @see org.glom.web.client.OnlineGlomService#getConfigurationErrorMessage()
275          */
276         @Override
277         public String getConfigurationErrorMessage() {
278                 if (configurationException == null)
279                         return "No configuration errors to report.";
280                 else if (configurationException.getMessage() == null)
281                         return configurationException.toString();
282                 else
283                         return configurationException.getMessage();
284         }
285
286         /*
287          * (non-Javadoc)
288          * 
289          * @see org.glom.web.client.OnlineGlomService#getDocumentInfo(java.lang.String)
290          */
291         @Override
292         public DocumentInfo getDocumentInfo(final String documentID, final String localeID) {
293
294                 ConfiguredDocument configuredDoc = documentMapping.get(documentID);
295
296                 // Avoid dereferencing a null object:
297                 if (configuredDoc == null)
298                         return new DocumentInfo();
299
300                 // FIXME check for authentication
301
302                 return configuredDoc.getDocumentInfo(StringUtils.defaultString(localeID));
303
304         }
305
306         /*
307          * (non-Javadoc)
308          * 
309          * @see org.glom.web.client.OnlineGlomService#getListViewLayout(java.lang.String, java.lang.String)
310          */
311         @Override
312         public LayoutGroup getListViewLayout(final String documentID, final String tableName, final String localeID) {
313                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
314                 if (configuredDoc == null)
315                         return new LayoutGroup();
316
317                 // FIXME check for authentication
318
319                 return configuredDoc.getListViewLayoutGroup(tableName, StringUtils.defaultString(localeID));
320         }
321
322         // TODO: Specify the foundset (via a where clause) and maybe a default sort order.
323         /*
324          * (non-Javadoc)
325          * 
326          * @see org.glom.web.client.OnlineGlomService#getReportLayout(java.lang.String, java.lang.String, java.lang.String,
327          * java.lang.String)
328          */
329         @Override
330         public String getReportHTML(final String documentID, final String tableName, final String reportName, final String quickFind, 
331                         final String localeID) {
332                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
333                 if (configuredDoc == null)
334                         return "";
335
336                 final Document glomDocument = configuredDoc.getDocument();
337                 if (glomDocument == null) {
338                         final String errorMessage = "getReportHTML(): getDocument() failed.";
339                         Log.fatal(errorMessage);
340                         // TODO: throw new Exception(errorMessage);
341                         return "";
342                 }
343
344                 // FIXME check for authentication
345
346                 final Report report = glomDocument.get_report(tableName, reportName);
347                 if (report == null) {
348                         Log.info(documentID, tableName, "The report layout is not defined for this table:" + reportName);
349                         return "";
350                 }
351
352                 Connection connection;
353                 try {
354                         connection = configuredDoc.getCpds().getConnection();
355                 } catch (final SQLException e2) {
356                         // TODO Auto-generated catch block
357                         e2.printStackTrace();
358                         return "Connection Failed";
359                 }
360
361                 // TODO: Use quickFind
362                 final ReportGenerator generator = new ReportGenerator(StringUtils.defaultString(localeID));
363                 return generator.generateReport(glomDocument, tableName, report, connection, quickFind);
364         }
365
366         /*
367          * (non-Javadoc)
368          * 
369          * @see org.glom.web.client.OnlineGlomService#getListViewData(java.lang.String, java.lang.String, int, int)
370          */
371         @Override
372         public ArrayList<DataItem[]> getListViewData(final String documentID, final String tableName,
373                         final String quickFind, final int start, final int length) {
374                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
375                 if (!configuredDoc.isAuthenticated()) {
376                         return new ArrayList<DataItem[]>();
377                 }
378                 return configuredDoc.getListViewData(tableName, quickFind, start, length, false, 0, false);
379         }
380
381         /*
382          * (non-Javadoc)
383          * 
384          * @see org.glom.web.client.OnlineGlomService#getSortedListViewData(java.lang.String, java.lang.String, int, int,
385          * int, boolean)
386          */
387         @Override
388         public ArrayList<DataItem[]> getSortedListViewData(final String documentID, final String tableName,
389                         final String quickFind, final int start, final int length, final int sortColumnIndex,
390                         final boolean isAscending) {
391                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
392                 if (configuredDoc == null)
393                         return new ArrayList<DataItem[]>();
394
395                 if (!configuredDoc.isAuthenticated()) {
396                         return new ArrayList<DataItem[]>();
397                 }
398                 return configuredDoc.getListViewData(tableName, quickFind, start, length, true, sortColumnIndex, isAscending);
399         }
400
401         /*
402          * (non-Javadoc)
403          * 
404          * @see org.glom.web.client.OnlineGlomService#getDocuments()
405          */
406         @Override
407         public Documents getDocuments() {
408                 final Documents documents = new Documents();
409                 for (final String documentID : documentMapping.keySet()) {
410                         final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
411                         if (configuredDoc == null)
412                                 continue;
413
414                         final Document glomDocument = configuredDoc.getDocument();
415                         if (glomDocument == null) {
416                                 final String errorMessage = "getDocuments(): getDocument() failed.";
417                                 Log.fatal(errorMessage);
418                                 // TODO: throw new Exception(errorMessage);
419                                 continue;
420                         }
421
422                         final String localeID = StringUtils.defaultString(configuredDoc.getDefaultLocaleID());
423                         documents.addDocument(documentID, glomDocument.get_database_title(localeID), localeID);
424                 }
425                 return documents;
426         }
427
428         /*
429          * (non-Javadoc)
430          * 
431          * @see org.glom.web.client.OnlineGlomService#isAuthenticated(java.lang.String)
432          */
433         @Override
434         public boolean isAuthenticated(final String documentID) {
435                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
436                 if (configuredDoc == null)
437                         return false;
438
439                 return configuredDoc.isAuthenticated();
440         }
441
442         /*
443          * (non-Javadoc)
444          * 
445          * @see org.glom.web.client.OnlineGlomService#checkAuthentication(java.lang.String, java.lang.String,
446          * java.lang.String)
447          */
448         @Override
449         public boolean checkAuthentication(final String documentID, final String username, final String password) {
450                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
451                 if (configuredDoc == null) {
452                         Log.error(documentID, "The document could not be found for this ID: " + documentID);
453                         return false;
454                 }
455
456                 try {
457                         return configuredDoc.setUsernameAndPassword(username, password);
458                 } catch (final SQLException e) {
459                         Log.error(documentID, "Unknown SQL Error checking the database authentication.", e);
460                         return false;
461                 }
462         }
463
464         /*
465          * (non-Javadoc)
466          * 
467          * @see org.glom.web.client.OnlineGlomService#getReportsList(java.lang.String, java.lang.String, java.lang.String)
468          */
469         @Override
470         public Reports getReportsList(final String documentID, final String tableName, final String localeID) {
471                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
472                 return configuredDoc.getReports(tableName, localeID);
473         }
474
475         /*
476          * (non-Javadoc)
477          * 
478          * @see org.glom.web.client.OnlineGlomService#getDetailsData(java.lang.String, java.lang.String, java.lang.String)
479          */
480         @Override
481         public DataItem[] getDetailsData(final String documentID, final String tableName,
482                         final TypedDataItem primaryKeyValue) {
483                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
484                 if (configuredDoc == null)
485                         return new DataItem[0];
486
487                 // FIXME check for authentication
488
489                 return configuredDoc.getDetailsData(tableName, primaryKeyValue);
490         }
491
492         /*
493          * (non-Javadoc)
494          * 
495          * @see org.glom.web.client.OnlineGlomService#getDetailsLayoutAndData(java.lang.String, java.lang.String,
496          * java.lang.String)
497          */
498         @Override
499         public DetailsLayoutAndData getDetailsLayoutAndData(final String documentID, final String tableName,
500                         final TypedDataItem primaryKeyValue, final String localeID) {
501                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
502                 if (configuredDoc == null)
503                         return null;
504
505                 // FIXME check for authentication
506
507                 final DetailsLayoutAndData initalDetailsView = new DetailsLayoutAndData();
508                 initalDetailsView
509                                 .setLayout(configuredDoc.getDetailsLayoutGroup(tableName, StringUtils.defaultString(localeID)));
510                 initalDetailsView.setData(configuredDoc.getDetailsData(tableName, primaryKeyValue));
511
512                 return initalDetailsView;
513         }
514
515         /*
516          * (non-Javadoc)
517          * 
518          * @see org.glom.web.client.OnlineGlomService#getRelatedListData(java.lang.String, java.lang.String, int, int)
519          */
520         @Override
521         public ArrayList<DataItem[]> getRelatedListData(final String documentID, final String tableName,
522                         final String relationshipName, final TypedDataItem foreignKeyValue, final int start, final int length) {
523                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
524
525                 // FIXME check for authentication
526
527                 return configuredDoc.getRelatedListData(tableName, relationshipName, foreignKeyValue, start, length, false, 0,
528                                 false);
529         }
530
531         /*
532          * (non-Javadoc)
533          * 
534          * @see org.glom.web.client.OnlineGlomService#getSortedRelatedListData(java.lang.String, java.lang.String, int, int,
535          * int, boolean)
536          */
537         @Override
538         public ArrayList<DataItem[]> getSortedRelatedListData(final String documentID, final String tableName,
539                         final String relationshipName, final TypedDataItem foreignKeyValue, final int start, final int length,
540                         final int sortColumnIndex, final boolean ascending) {
541                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
542                 if (configuredDoc == null)
543                         return new ArrayList<DataItem[]>();
544
545                 // FIXME check for authentication
546
547                 return configuredDoc.getRelatedListData(tableName, relationshipName, foreignKeyValue, start, length, true,
548                                 sortColumnIndex, ascending);
549         }
550
551         @Override
552         public int getRelatedListRowCount(final String documentID, final String tableName, final String relationshipName,
553                         final TypedDataItem foreignKeyValue) {
554                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
555                 if (configuredDoc == null)
556                         return 0;
557
558                 // FIXME check for authentication
559
560                 return configuredDoc.getRelatedListRowCount(tableName, relationshipName, foreignKeyValue);
561         }
562
563         /*
564          * (non-Javadoc)
565          * 
566          * @see org.glom.web.client.OnlineGlomService#getSuitableRecordToViewDetails(java.lang.String, java.lang.String,
567          * java.lang.String, java.lang.String)
568          */
569         @Override
570         public NavigationRecord getSuitableRecordToViewDetails(final String documentID, final String tableName,
571                         final String relationshipName, final TypedDataItem primaryKeyValue) {
572                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
573                 if (configuredDoc == null)
574                         return new NavigationRecord();
575
576                 // FIXME check for authentication
577
578                 return configuredDoc.getSuitableRecordToViewDetails(tableName, relationshipName, primaryKeyValue);
579         }
580
581 }