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