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