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