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