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