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