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