OnlineGlomServiceImpl: Add to overview comments.
[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.SQLException;
26 import java.util.ArrayList;
27 import java.util.Hashtable;
28 import java.util.Properties;
29
30 import javax.servlet.ServletException;
31
32 import org.glom.libglom.BakeryDocument.LoadFailureCodes;
33 import org.glom.libglom.Document;
34 import org.glom.libglom.Glom;
35 import org.glom.libglom.TranslatableItem;
36 import org.glom.web.client.OnlineGlomService;
37 import org.glom.web.shared.DataItem;
38 import org.glom.web.shared.DetailsLayoutAndData;
39 import org.glom.web.shared.DocumentInfo;
40 import org.glom.web.shared.Documents;
41 import org.glom.web.shared.NavigationRecord;
42 import org.glom.web.shared.TypedDataItem;
43 import org.glom.web.shared.layout.LayoutGroup;
44
45 import com.google.gwt.user.server.rpc.RemoteServiceServlet;
46 import com.mchange.v2.c3p0.DataSources;
47
48 /**
49  * This is the servlet class for setting up the server side of Online Glom.
50  * The client side can call the public methods in this class via OnlineGlom
51  * 
52  * For instance, it loads all the available documents and provide a list -
53  * see getDocuments().
54  * It then provides information from each document. For instance, see
55  * getListViewLayout().
56  * 
57  * TODO: Watch for changes to the .glom files, to reload new versions
58  * and to load newly-added files.
59  * TODO: Watch for changes to the properties (configuration)?
60  */
61 @SuppressWarnings("serial")
62 public class OnlineGlomServiceImpl extends RemoteServiceServlet implements OnlineGlomService {
63
64         private static final String GLOM_FILE_EXTENSION = ".glom";
65
66         // convenience class for dealing with the Online Glom configuration file
67         private class OnlineGlomProperties extends Properties {
68                 public String getKey(final String value) {
69                         for (final String key : stringPropertyNames()) {
70                                 if (getProperty(key).trim().equals(value))
71                                         return key;
72                         }
73                         return null;
74                 }
75         }
76
77         private final Hashtable<String, ConfiguredDocument> documentMapping = new Hashtable<String, ConfiguredDocument>();
78         private Exception configurtionException = null;
79
80         /*
81          * This is called when the servlet is started or restarted.
82          * 
83          * (non-Javadoc)
84          * 
85          * @see javax.servlet.GenericServlet#init()
86          */
87         @Override
88         public void init() throws ServletException {
89
90                 // All of the initialisation code is surrounded by a try/catch block so that the servlet can be in an
91                 // initialised state and the error message can be retrieved by the client code.
92                 try {
93                         // Find the configuration file. See this thread for background info:
94                         // http://stackoverflow.com/questions/2161054/where-to-place-properties-files-in-a-jsp-servlet-web-application
95                         // FIXME move onlineglom.properties to the WEB-INF folder (option number 2 from the stackoverflow question)
96                         final OnlineGlomProperties config = new OnlineGlomProperties();
97                         final InputStream is = Thread.currentThread().getContextClassLoader()
98                                         .getResourceAsStream("onlineglom.properties");
99                         if (is == null) {
100                                 final String errorMessage = "onlineglom.properties not found.";
101                                 Log.fatal(errorMessage);
102                                 throw new Exception(errorMessage);
103                         }
104                         config.load(is); // can throw an IOException
105
106                         // check if we can read the configured glom file directory
107                         final String documentDirName = config.getProperty("glom.document.directory");
108                         final File documentDir = new File(documentDirName);
109                         if (!documentDir.isDirectory()) {
110                                 final String errorMessage = documentDirName + " is not a directory.";
111                                 Log.fatal(errorMessage);
112                                 throw new Exception(errorMessage);
113                         }
114                         if (!documentDir.canRead()) {
115                                 final String errorMessage = "Can't read the files in directory " + documentDirName + " .";
116                                 Log.fatal(errorMessage);
117                                 throw new Exception(errorMessage);
118                         }
119
120                         // get and check the glom files in the specified directory
121                         final File[] glomFiles = documentDir.listFiles(new FilenameFilter() {
122                                 @Override
123                                 public boolean accept(final File dir, final String name) {
124                                         return name.endsWith(GLOM_FILE_EXTENSION);
125                                 }
126                         });
127
128                         // don't continue if there aren't any Glom files to configure
129                         if (glomFiles.length <= 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                                 Log.error(errorMessage);
142                                 throw new Exception(errorMessage);
143                         }
144
145                         // Check for a specified default locale,
146                         // for table titles, field titles, etc:
147                         final String localeID = config.getProperty("glom.document.locale");
148
149                         if (localeID != null && !localeID.isEmpty()) {
150                                 TranslatableItem.set_current_locale(localeID.trim());
151                         }
152
153                         // This initialisation method can throw an UnsatisfiedLinkError if the java-libglom native library isn't
154                         // available. But since we're checking for the error condition above, the UnsatisfiedLinkError will never be
155                         // thrown.
156                         Glom.libglom_init();
157
158                         // Allow a fake connection, so sqlbuilder_get_full_query() can work:
159                         Glom.set_fake_connection();
160
161                         for (final File glomFile : glomFiles) {
162                                 final Document document = new Document();
163                                 document.set_file_uri("file://" + glomFile.getAbsolutePath());
164                                 final int error = 0;
165                                 final boolean retval = document.load(error);
166                                 if (retval == false) {
167                                         String message;
168                                         if (LoadFailureCodes.LOAD_FAILURE_CODE_NOT_FOUND == LoadFailureCodes.swigToEnum(error)) {
169                                                 message = "Could not find file: " + glomFile.getAbsolutePath();
170                                         } else {
171                                                 message = "An unknown error occurred when trying to load file: " + glomFile.getAbsolutePath();
172                                         }
173                                         Log.error(message);
174                                         // continue with for loop because there may be other documents in the directory
175                                         continue;
176                                 }
177
178                                 final ConfiguredDocument configuredDocument = new ConfiguredDocument(document); // can throw a
179                                 // PropertyVetoException
180
181                                 // check if a username and password have been set and work for the current document
182                                 final String filename = glomFile.getName();
183                                 final String key = config.getKey(filename);
184                                 if (key != null) {
185                                         final String[] keyArray = key.split("\\.");
186                                         if (keyArray.length == 3 && "filename".equals(keyArray[2])) {
187                                                 // username/password could be set, let's check to see if it works
188                                                 final String usernameKey = key.replaceAll(keyArray[2], "username");
189                                                 final String passwordKey = key.replaceAll(keyArray[2], "password");
190                                                 configuredDocument.setUsernameAndPassword(config.getProperty(usernameKey).trim(),
191                                                                 config.getProperty(passwordKey)); // can throw an SQLException
192                                         }
193                                 }
194
195                                 // check the if the global username and password have been set and work with this document
196                                 if (!configuredDocument.isAuthenticated()) {
197                                         configuredDocument.setUsernameAndPassword(config.getProperty("glom.document.username").trim(),
198                                                         config.getProperty("glom.document.password")); // can throw an SQLException
199                                 }
200
201                                 // The key for the hash table is the file name without the .glom extension and with spaces ( ) replaced
202                                 // with pluses (+). The space/plus replacement makes the key more friendly for URLs.
203                                 final String documentID = filename.substring(0, glomFile.getName().length() - GLOM_FILE_EXTENSION.length())
204                                                 .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                         configurtionException = 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                         try {
260                                 DataSources.destroy(configuredDoc.getCpds());
261                         } catch (final SQLException e) {
262                                 Log.error(documenTitle, "Error cleaning up the ComboPooledDataSource.", e);
263                         }
264                 }
265         }
266
267         /*
268          * (non-Javadoc)
269          * 
270          * @see org.glom.web.client.OnlineGlomService#getConfigurationErrorMessage()
271          */
272         @Override
273         public String getConfigurationErrorMessage() {
274                 if (configurtionException == null)
275                         return "No configuration errors to report.";
276                 else if (configurtionException.getMessage() == null)
277                         return configurtionException.toString();
278                 else
279                         return configurtionException.getMessage();
280         }
281
282         /*
283          * (non-Javadoc)
284          * 
285          * @see org.glom.web.client.OnlineGlomService#getDocumentInfo(java.lang.String)
286          */
287         @Override
288         public DocumentInfo getDocumentInfo(final String documentID) {
289
290                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
291
292                 // FIXME check for authentication
293
294                 return configuredDoc.getDocumentInfo();
295
296         }
297
298         /*
299          * (non-Javadoc)
300          * 
301          * @see org.glom.web.client.OnlineGlomService#getListViewLayout(java.lang.String, java.lang.String)
302          */
303         @Override
304         public LayoutGroup getListViewLayout(final String documentID, final String tableName) {
305                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
306
307                 // FIXME check for authentication
308
309                 return configuredDoc.getListViewLayoutGroup(tableName);
310         }
311
312         /*
313          * (non-Javadoc)
314          * 
315          * @see org.glom.web.client.OnlineGlomService#getListViewData(java.lang.String, java.lang.String, int, int)
316          */
317         @Override
318         public ArrayList<DataItem[]> getListViewData(final String documentID, final String tableName,
319                         final String quickFind, final int start, final int length) {
320                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
321                 if (!configuredDoc.isAuthenticated()) {
322                         return new ArrayList<DataItem[]>();
323                 }
324                 return configuredDoc.getListViewData(tableName, quickFind, start, length, false, 0, false);
325         }
326
327         /*
328          * (non-Javadoc)
329          * 
330          * @see org.glom.web.client.OnlineGlomService#getSortedListViewData(java.lang.String, java.lang.String, int, int,
331          * int, boolean)
332          */
333         @Override
334         public ArrayList<DataItem[]> getSortedListViewData(final String documentID, final String tableName,
335                         final String quickFind,
336                         final int start, final int length,
337                         final int sortColumnIndex, final boolean isAscending) {
338                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
339                 if (!configuredDoc.isAuthenticated()) {
340                         return new ArrayList<DataItem[]>();
341                 }
342                 return configuredDoc.getListViewData(tableName, quickFind, start, length, true, sortColumnIndex, isAscending);
343         }
344
345         /*
346          * (non-Javadoc)
347          * 
348          * @see org.glom.web.client.OnlineGlomService#getDocuments()
349          */
350         @Override
351         public Documents getDocuments() {
352                 final Documents documents = new Documents();
353                 for (final String documentID : documentMapping.keySet()) {
354                         final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
355                         documents.addDocument(documentID, configuredDoc.getDocument().get_database_title());
356                 }
357                 return documents;
358         }
359
360         /*
361          * (non-Javadoc)
362          * 
363          * @see org.glom.web.client.OnlineGlomService#isAuthenticated(java.lang.String)
364          */
365         @Override
366         public boolean isAuthenticated(final String documentID) {
367                 return documentMapping.get(documentID).isAuthenticated();
368         }
369
370         /*
371          * (non-Javadoc)
372          * 
373          * @see org.glom.web.client.OnlineGlomService#checkAuthentication(java.lang.String, java.lang.String,
374          * java.lang.String)
375          */
376         @Override
377         public boolean checkAuthentication(final String documentID, final String username, final String password) {
378                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
379                 try {
380                         return configuredDoc.setUsernameAndPassword(username, password);
381                 } catch (final SQLException e) {
382                         Log.error(documentID, "Unknown SQL Error checking the database authentication.", e);
383                         return false;
384                 }
385         }
386
387         /*
388          * (non-Javadoc)
389          * 
390          * @see org.glom.web.client.OnlineGlomService#getDetailsData(java.lang.String, java.lang.String, java.lang.String)
391          */
392         @Override
393         public DataItem[] getDetailsData(final String documentID, final String tableName, final TypedDataItem primaryKeyValue) {
394                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
395
396                 // FIXME check for authentication
397
398                 return configuredDoc.getDetailsData(tableName, primaryKeyValue);
399         }
400
401         /*
402          * (non-Javadoc)
403          * 
404          * @see org.glom.web.client.OnlineGlomService#getDetailsLayoutAndData(java.lang.String, java.lang.String,
405          * java.lang.String)
406          */
407         @Override
408         public DetailsLayoutAndData getDetailsLayoutAndData(final String documentID, final String tableName,
409                         final TypedDataItem primaryKeyValue) {
410                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
411                 if (configuredDoc == null)
412                         return null;
413
414                 // FIXME check for authentication
415
416                 final DetailsLayoutAndData initalDetailsView = new DetailsLayoutAndData();
417                 initalDetailsView.setLayout(configuredDoc.getDetailsLayoutGroup(tableName));
418                 initalDetailsView.setData(configuredDoc.getDetailsData(tableName, primaryKeyValue));
419
420                 return initalDetailsView;
421         }
422
423         /*
424          * (non-Javadoc)
425          * 
426          * @see org.glom.web.client.OnlineGlomService#getRelatedListData(java.lang.String, java.lang.String, int, int)
427          */
428         @Override
429         public ArrayList<DataItem[]> getRelatedListData(final String documentID, final String tableName, final String relationshipName,
430                         final TypedDataItem foreignKeyValue, final int start, final int length) {
431                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
432
433                 // FIXME check for authentication
434
435                 return configuredDoc.getRelatedListData(tableName, relationshipName, foreignKeyValue, start, length, false, 0,
436                                 false);
437         }
438
439         /*
440          * (non-Javadoc)
441          * 
442          * @see org.glom.web.client.OnlineGlomService#getSortedRelatedListData(java.lang.String, java.lang.String, int, int,
443          * int, boolean)
444          */
445         @Override
446         public ArrayList<DataItem[]> getSortedRelatedListData(final String documentID, final String tableName, final String relationshipName,
447                         final TypedDataItem foreignKeyValue, final int start, final int length, final int sortColumnIndex, final boolean ascending) {
448                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
449
450                 // FIXME check for authentication
451
452                 return configuredDoc.getRelatedListData(tableName, relationshipName, foreignKeyValue, start, length, true,
453                                 sortColumnIndex, ascending);
454         }
455
456         @Override
457         public int getRelatedListRowCount(final String documentID, final String tableName, final String relationshipName,
458                         final TypedDataItem foreignKeyValue) {
459                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
460
461                 // FIXME check for authentication
462
463                 return configuredDoc.getRelatedListRowCount(tableName, relationshipName, foreignKeyValue);
464         }
465
466         /*
467          * (non-Javadoc)
468          * 
469          * @see org.glom.web.client.OnlineGlomService#getSuitableRecordToViewDetails(java.lang.String, java.lang.String,
470          * java.lang.String, java.lang.String)
471          */
472         @Override
473         public NavigationRecord getSuitableRecordToViewDetails(final String documentID, final String tableName,
474                         final String relationshipName, final TypedDataItem primaryKeyValue) {
475                 final ConfiguredDocument configuredDoc = documentMapping.get(documentID);
476
477                 // FIXME check for authentication
478
479                 return configuredDoc.getSuitableRecordToViewDetails(tableName, relationshipName, primaryKeyValue);
480         }
481
482 }