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