1   /*
2    * Copyright 2003 - 2013 The eFaps Team
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *
16   * Revision:        $Rev$
17   * Last Changed:    $Date$
18   * Last Changed By: $Author$
19   */
20  
21  package org.efaps.init;
22  
23  import java.io.File;
24  import java.io.FileInputStream;
25  import java.io.FileNotFoundException;
26  import java.io.IOException;
27  import java.util.HashMap;
28  import java.util.Map;
29  import java.util.Map.Entry;
30  import java.util.Properties;
31  import java.util.regex.Matcher;
32  import java.util.regex.Pattern;
33  
34  import javax.naming.Context;
35  import javax.naming.InitialContext;
36  import javax.naming.NamingException;
37  import javax.naming.Reference;
38  import javax.naming.StringRefAddr;
39  import javax.naming.spi.ObjectFactory;
40  import javax.sql.DataSource;
41  import javax.transaction.TransactionManager;
42  import javax.transaction.TransactionSynchronizationRegistry;
43  
44  import org.efaps.db.databases.AbstractDatabase;
45  import org.efaps.db.transaction.DelegatingUserTransaction;
46  import org.slf4j.Logger;
47  import org.slf4j.LoggerFactory;
48  
49  /**
50   * Initialize the database connection for an eFaps instance.
51   *
52   * @author The eFaps Team
53   * @version $Id$
54   */
55  public final class StartupDatabaseConnection
56      implements INamingBinds
57  {
58  
59      /**
60       * Logging instance used to give logging information of this class.
61       */
62      private static final Logger LOG = LoggerFactory.getLogger(StartupDatabaseConnection.class);
63  
64      /**
65       * Name of the environment variable to define the bootstrap path.
66       */
67      private static final String ENV_PATH = "EFAPS_BOOTSTRAP_PATH";
68  
69      /**
70       * Name of the environment variable to define the name of the bootstrap file.
71       */
72      private static final String ENV_FILE = "EFAPS_BOOTSTRAP_FILE";
73  
74      /**
75       * Name of the property for the database type class.
76       */
77      private static final String PROP_DBTYPE_CLASS = "org.efaps.db.type";
78  
79      /**
80       * Name of the property for the database factory class.
81       */
82      private static final String PROP_DBFACTORY_CLASS = "org.efaps.db.factory";
83  
84      /**
85       * Name of the property for the database connection.
86       */
87      private static final String PROP_DBCONNECTION = "org.efaps.db.connection";
88  
89      /**
90       * Name of the property for the transaction manager class.
91       */
92      private static final String PROP_TM_CLASS = "org.efaps.transaction.manager";
93  
94      /**
95       * Name of the property for the timeout of the transaction manager.
96       */
97      private static final String PROP_CONFIGPROP = "org.efaps.configuration.properties";
98  
99      /**
100      * Name of the default bootstrap path in the user home directory.
101      */
102     private static final String DEFAULT_BOOTSTRAP_PATH = ".efaps/bootstrap";
103 
104     /**
105      * Name of the default bootstrap file.
106      */
107     private static final String DEFAULT_BOOTSTRAP_FILE = "default.efaps.xml";
108 
109     /**
110      * Constructor is hidden to prevent instantiation.
111      */
112     private StartupDatabaseConnection()
113     {
114     }
115 
116     /**
117      * Startups the eFaps kernel with the bootstrap configuration defined as shell variable. If the bootstrap
118      * configuration is not defined as shell variables, the default bootstrap definition is used.
119      *
120      * @throws StartupException if startup failed
121      * @see #startup(String, String)
122      */
123     public static void startup()
124         throws StartupException
125     {
126         StartupDatabaseConnection.startup(null, null);
127     }
128 
129     /**
130      * Startups the eFaps kernel with the bootstrap path defined as shell variable (or if not defined the default
131      * bootstrap path is used).
132      *
133      * @param _bootstrapFile name of the bootstrap file (without file extension); <code>null</code> means the the name
134      *            of the bootstrap is not predefined
135      * @throws StartupException if startup failed
136      * @see #startup(String, String)
137      */
138     public static void startup(final String _bootstrapFile)
139         throws StartupException
140     {
141         StartupDatabaseConnection.startup(null, _bootstrapFile);
142     }
143 
144     /**
145      * <p>
146      * Startups he kernel depending on a bootstrap definition.
147      * </p>
148      *
149      * <p>
150      * Following rules applies for the bootstrap path:
151      * <ul>
152      * <li>if <code>_bootstrapPath</code> is not <code>null</code>, <code>_bootstrapPath</code> is used as bootstrap
153      * path</li>
154      * <li>if in the system environment the shell variable {@link #ENV_PATH} is defined, this value of this shell
155      * variable is used</li>
156      * <li>in all other cases {@link #DEFAULT_BOOTSTRAP_PATH} is used</li>
157      * </ul>
158      * </p>
159      *
160      * <p>
161      * Following rules applies for the bootstrap name:
162      * <ul>
163      * <li>if <code>_bootstrapFile</code> is not <code>null</code>, <code>_bootstrapFile</code> is used as bootstrap
164      * name</li>
165      * <li>if in the system environment the shell variable {@link #ENV_FILE} is defined, this value of this shell
166      * variable is used</li>
167      * <li>in all other cases {@link #DEFAULT_BOOTSTRAP_FILE} is used</li>
168      * </ul>
169      * </p>
170      *
171      * <p>
172      * After the used bootstrap file is identified, this file is opened and the keys are read and evaluated.
173      * </p>
174      *
175      * @param _bootstrapPath path where the bootstrap files are located; <code>null</code> means that the path is not
176      *            predefined
177      * @param _bootstrapFile name of the bootstrap file (without file extension); <code>null</code> means the the name
178      *            of the bootstrap is not predefined
179      * @throws StartupException if startup failed
180      */
181     public static void startup(final String _bootstrapPath,
182                                final String _bootstrapFile)
183         throws StartupException
184     {
185         // evaluate bootstrap path
186         final File bsPath;
187         if (_bootstrapPath != null) {
188             bsPath = new File(_bootstrapPath);
189         } else {
190             final String envPath = System.getenv(StartupDatabaseConnection.ENV_PATH);
191             if (envPath != null) {
192                 bsPath = new File(envPath);
193             } else {
194                 bsPath = new File(System.getProperty("user.home"), StartupDatabaseConnection.DEFAULT_BOOTSTRAP_PATH);
195             }
196         }
197         // evaluate bootstrap file
198         final String bsFile;
199         File bootstrap = null;
200         if (_bootstrapFile != null) {
201             bsFile = _bootstrapFile;
202         } else {
203             final String envFile = System.getenv(StartupDatabaseConnection.ENV_FILE);
204             if (envFile != null) {
205                 bsFile = envFile;
206             } else {
207                 bsFile = StartupDatabaseConnection.DEFAULT_BOOTSTRAP_FILE;
208             }
209         }
210         bootstrap = new File(bsFile);
211         if (bootstrap == null || !bootstrap.exists()) {
212             bootstrap = new File(bsPath, bsFile);
213         }
214 
215         // read bootstrap file
216         final Properties props = new Properties();
217         try {
218             props.loadFromXML(new FileInputStream(bootstrap));
219         } catch (final FileNotFoundException e) {
220             throw new StartupException("bootstrap file " + bootstrap + " not found", e);
221         } catch (final IOException e) {
222             throw new StartupException("bootstrap file " + bootstrap + " could not be read", e);
223         }
224 
225         // and startup
226         final Map<String, String> eFapsProps;
227         if (props.containsKey(StartupDatabaseConnection.PROP_CONFIGPROP)) {
228             eFapsProps = StartupDatabaseConnection.convertToMap(props
229                             .getProperty(StartupDatabaseConnection.PROP_CONFIGPROP));
230         } else {
231             eFapsProps = new HashMap<String, String>();
232         }
233         StartupDatabaseConnection.startup(props.getProperty(StartupDatabaseConnection.PROP_DBTYPE_CLASS),
234                         props.getProperty(StartupDatabaseConnection.PROP_DBFACTORY_CLASS),
235                         props.getProperty(StartupDatabaseConnection.PROP_DBCONNECTION),
236                         props.getProperty(StartupDatabaseConnection.PROP_TM_CLASS),
237                         null,
238                         eFapsProps);
239     }
240 
241     /**
242      * Initialize the database information set for given parameter values.
243      *
244      * @param _classDBType class name of the database type
245      * @param _classDSFactory class name of the SQL data source factory
246      * @param _propConnection string with properties for the JDBC connection
247      * @param _classTM class name of the transaction manager
248      * @param _classTSR class name of TransactionSynchronizationRegistry
249      * @param _eFapsProps tring with properties
250      * @throws StartupException if the database connection or transaction manager could not be initialized
251      * @see StartupDatabaseConnection#startup(String, String, Map, String, Integer)
252      * @see StartupDatabaseConnection#convertToMap(String)
253      */
254     public static void startup(final String _classDBType,
255                                final String _classDSFactory,
256                                final String _propConnection,
257                                final String _classTM,
258                                final String _classTSR,
259                                final String _eFapsProps)
260         throws StartupException
261     {
262         StartupDatabaseConnection.startup(_classDBType,
263                         _classDSFactory,
264                         StartupDatabaseConnection.convertToMap(_propConnection),
265                         _classTM,
266                         _classTSR,
267                         StartupDatabaseConnection.convertToMap(_eFapsProps));
268     }
269 
270     /**
271      * Initialize the database information set for given parameter values.
272      *
273      * @param _classDBType class name of the database type
274      * @param _classDSFactory class name of the SQL data source factory
275      * @param _propConnection string with properties for the JDBC connection
276      * @param _classTM class name of the transaction manager
277      * @param _classTSR class name of TransactionSynchronizationRegistry
278      * @param _eFapsProps Map or properties
279      * @throws StartupException if the database connection or transaction manager could not be initialized
280      * @see StartupDatabaseConnection#startup(String, String, Map, String, Integer)
281      * @see StartupDatabaseConnection#convertToMap(String)
282      */
283     public static void startup(final String _classDBType,
284                                final String _classDSFactory,
285                                final String _propConnection,
286                                final String _classTM,
287                                final String _classTSR,
288                                final Map<String, String> _eFapsProps)
289         throws StartupException
290     {
291         StartupDatabaseConnection.startup(_classDBType,
292                         _classDSFactory,
293                         StartupDatabaseConnection.convertToMap(_propConnection),
294                         _classTM,
295                         _classTSR,
296                         _eFapsProps);
297     }
298 
299     /**
300      * Initialize the database information set for given parameter values.
301      * <ul>
302      * <li>configure the database type</li>
303      * <li>initialize the SQL data source (JDBC connection to the database)</li>
304      * <li>initialize transaction manager</li>
305      * </ul>
306      *
307      * @param _classDBType class name of the database type
308      * @param _classDSFactory class name of the SQL data source factory
309      * @param _propConnection map of properties for the JDBC connection
310      * @param _classTM class name of the transaction manager
311      * @param _classTSR class name of TransactionSynchronizationRegistry
312      * @param _eFapsProps Map or properties
313      * @throws StartupException if the database connection or transaction manager could not be initialized
314      * @see #configureDBType(Context, String)
315      * @see #configureDataSource(Context, String, Map)
316      * @see #configureTransactionManager(Context, String, Integer)
317      */
318     public static void startup(final String _classDBType,
319                                final String _classDSFactory,
320                                final Map<String, String> _propConnection,
321                                final String _classTM,
322                                final String _classTSR,
323                                final Map<String, String> _eFapsProps)
324         throws StartupException
325     {
326         if (StartupDatabaseConnection.LOG.isInfoEnabled()) {
327             StartupDatabaseConnection.LOG.info("Initialise Database Connection");
328         }
329 
330         final Context compCtx;
331         try {
332             final InitialContext context = new InitialContext();
333             compCtx = (javax.naming.Context) context.lookup("java:/comp");
334 
335             StartupDatabaseConnection.configureEFapsProperties(compCtx, _eFapsProps);
336             StartupDatabaseConnection.configureDBType(compCtx, _classDBType);
337             StartupDatabaseConnection.configureDataSource(compCtx, _classDSFactory, _propConnection);
338             StartupDatabaseConnection.configureTransactionManager(compCtx, _classTM);
339             StartupDatabaseConnection.configureTransactionSynchronizationRegistry(compCtx, _classTSR);
340         } catch (final NamingException e) {
341             throw new StartupException("Could not initialize JNDI", e);
342         }
343         // and reset eFaps context (to be sure..)
344         org.efaps.db.Context.reset();
345     }
346 
347     /**
348      * Add the eFaps Properties to the JNDI binding.
349      * @param _compCtx      Java root naming context
350      * @param _eFapsProps   Properties to bind
351      * @throws StartupException on error
352      */
353     protected static void configureEFapsProperties(final Context _compCtx,
354                                                    final Map<String, String> _eFapsProps)
355         throws StartupException
356     {
357         try {
358             Util.bind(_compCtx, "env/" + INamingBinds.RESOURCE_CONFIGPROPERTIES, _eFapsProps);
359         } catch (final NamingException e) {
360             throw new StartupException("could not bind eFaps Properties '" + _eFapsProps + "'", e);
361             // CHECKSTYLE:OFF
362         } catch (final Exception e) {
363             // CHECKSTYLE:ON
364             throw new StartupException("could not bind eFaps Properties '" + _eFapsProps + "'", e);
365         }
366     }
367 
368     /**
369      * The class defined with parameter <code>_classDSFactory</code> initialized and bind to
370      * {@link #RESOURCE_DATASOURCE}. The initialized class must implement interface {@link DataSource}. As JDBC
371      * connection properties the map <code>_propConneciton</code> is used.
372      *
373      * @param _compCtx Java root naming context
374      * @param _classDSFactory class name of the SQL data source factory
375      * @param _propConnection map of properties for the JDBC connection
376      * @throws StartupException on error
377      */
378     protected static void configureDataSource(final Context _compCtx,
379                                               final String _classDSFactory,
380                                               final Map<String, String> _propConnection)
381         throws StartupException
382     {
383         final Reference ref = new Reference(DataSource.class.getName(), _classDSFactory, null);
384         for (final Entry<String, String> entry : _propConnection.entrySet()) {
385             ref.add(new StringRefAddr(entry.getKey(), entry.getValue()));
386         }
387         try {
388             Util.bind(_compCtx, "env/" + INamingBinds.RESOURCE_DATASOURCE, ref);
389             Util.bind(_compCtx, "test", ref);
390         } catch (final NamingException e) {
391             throw new StartupException("could not bind JDBC pooling class '" + _classDSFactory + "'", e);
392             // CHECKSTYLE:OFF
393         } catch (final Exception e) {
394             // CHECKSTYLE:ON
395             throw new StartupException("coud not get object instance of factory '" + _classDSFactory + "'", e);
396         }
397     }
398 
399     /**
400      * The class defined with parameter _classDBType initialized and bind to {@link #RESOURCE_DBTYPE}. The initialized
401      * class must be extended from class {@link AbstractDatabase}.
402      *
403      * @param _compCtx Java root naming context
404      * @param _classDBType class name of the database type
405      * @throws StartupException if the database type class could not be found, initialized, accessed or bind to the
406      *             context
407      */
408     protected static void configureDBType(final Context _compCtx,
409                                           final String _classDBType)
410         throws StartupException
411     {
412         try {
413             final AbstractDatabase<?> dbType = (AbstractDatabase<?>) (Class.forName(_classDBType)).newInstance();
414             if (dbType == null) {
415                 throw new StartupException("could not initaliase database type '" + _classDBType + "'");
416             } else {
417                 Util.bind(_compCtx, "env/" + INamingBinds.RESOURCE_DBTYPE, dbType);
418             }
419         } catch (final ClassNotFoundException e) {
420             throw new StartupException("could not found database description class '" + _classDBType + "'", e);
421         } catch (final InstantiationException e) {
422             throw new StartupException("could not initialise database description class '" + _classDBType + "'", e);
423         } catch (final IllegalAccessException e) {
424             throw new StartupException("could not access database description class '" + _classDBType + "'", e);
425         } catch (final NamingException e) {
426             throw new StartupException("could not bind database description class '" + _classDBType + "'", e);
427         }
428     }
429 
430     /**
431      * The class defined with parameter _classTM initialized and bind to {@link #RESOURCE_TRANSMANAG}. The initialized
432      * class must implement interface {@link TransactionManager}.
433      *
434      * @param _compCtx Java root naming context
435      * @param _classTM class name of the transaction manager
436      * @throws StartupException if the transaction manager class could not be found, initialized, accessed or bind to
437      *             the context
438      */
439     protected static void configureTransactionManager(final Context _compCtx,
440                                                       final String _classTM)
441         throws StartupException
442     {
443         try {
444             final Object tm = (Class.forName(_classTM)).newInstance();
445             if (tm == null) {
446                 throw new StartupException("could not initialise TransactionManager ");
447             } else {
448                 if (tm instanceof TransactionManager) {
449                     Util.bind(_compCtx, "env/" + INamingBinds.RESOURCE_TRANSMANAG, tm);
450                     Util.bind(_compCtx, "env/" + INamingBinds.RESOURCE_USERTRANSACTION,
451                                     new DelegatingUserTransaction((TransactionManager) tm));
452                 } else {
453                     throw new StartupException("could not initialise TransactionManager with object:" + tm);
454                 }
455             }
456         } catch (final ClassNotFoundException e) {
457             throw new StartupException("could not find transaction manager class '" + _classTM + "'", e);
458         } catch (final InstantiationException e) {
459             throw new StartupException("could not initialise transaction manager class '" + _classTM + "'", e);
460         } catch (final IllegalAccessException e) {
461             throw new StartupException("could not access transaction manager class '" + _classTM + "'", e);
462         } catch (final NamingException e) {
463             throw new StartupException("could not bind transaction manager class '" + _classTM + "'", e);
464         }
465     }
466 
467 
468     /**
469      * The class defined with parameter _classTM initialized and bind to {@link #RESOURCE_TRANSSYNREG}. The initialized
470      * class must implement interface {@link TransactionSynchronizationRegistry}.
471      *
472      * @param _compCtx Java root naming context
473      * @param _classTSR class name of the transaction SynchronizationRegistry
474      * @throws StartupException if the transaction manager class could not be found, initialized, accessed or bind to
475      *             the context
476      */
477     protected static void configureTransactionSynchronizationRegistry(final Context _compCtx,
478                                                                       final String _classTSR)
479         throws StartupException
480     {
481         try {
482 
483             final Object clzz = (Class.forName(_classTSR)).newInstance();
484             if (clzz == null) {
485                 throw new StartupException("could not initaliase database type");
486             } else {
487                 if (clzz instanceof TransactionSynchronizationRegistry) {
488                     Util.bind(_compCtx, "env/" + INamingBinds.RESOURCE_TRANSSYNREG, clzz);
489                     Util.bind(_compCtx, "TransactionSynchronizationRegistry", clzz);
490                 } else if (clzz instanceof ObjectFactory) {
491                     final Reference ref = new Reference(TransactionSynchronizationRegistry.class.getName(), clzz
492                                     .getClass().getName(), null);
493                     Util.bind(_compCtx, "env/" + INamingBinds.RESOURCE_TRANSSYNREG, ref);
494                     Util.bind(_compCtx, "TransactionSynchronizationRegistry", ref);
495                 }
496             }
497 
498         } catch (final ClassNotFoundException e) {
499             throw new StartupException("could not found TransactionSynchronizationRegistry '" + _classTSR + "'", e);
500         } catch (final InstantiationException e) {
501             throw new StartupException("could not initialise TransactionSynchronizationRegistry class '" + _classTSR
502                             + "'", e);
503         } catch (final IllegalAccessException e) {
504             throw new StartupException("could not access TransactionSynchronizationRegistry class '" + _classTSR + "'",
505                             e);
506         } catch (final NamingException e) {
507             throw new StartupException("could not bind Transaction Synchronization Registry class '"
508                             + _classTSR + "'", e);
509         }
510     }
511 
512     /**
513      * <p>
514      * Separates all key / value pairs of given text string.
515      * </p>
516      * <p>
517      * <b>Evaluation algorithm:</b><br/>
518      * Separates the text by all found commas (only if in front of the comma is no back slash). This are the key / value
519      * pairs. A key / value pair is separated by the first equal ('=') sign.
520      * </p>
521      *
522      * @param _text text string to convert to a key / value map
523      * @return Map of strings with all found key / value pairs
524      */
525     public static Map<String, String> convertToMap(final String _text)
526     {
527         final Map<String, String> properties = new HashMap<String, String>();
528 
529         // separated all key / value pairs
530         final Pattern pattern = Pattern.compile("(([^\\\\,])|(\\\\,)|(\\\\))*");
531         final Matcher matcher = pattern.matcher(_text);
532 
533         while (matcher.find()) {
534             final String group = matcher.group().trim();
535             if (group.length() > 0) {
536                 // separated key from value
537                 final int index = group.indexOf('=');
538                 final String key = (index > 0)
539                                 ? group.substring(0, index).trim()
540                                 : group.trim();
541                 final String value = (index > 0)
542                                 ? group.substring(index + 1).trim()
543                                 : "";
544                 properties.put(key, value);
545             }
546         }
547         return properties;
548     }
549 
550     /**
551      * Shutdowns the connection to the database.
552      *
553      * @throws StartupException if shutdown failed
554      */
555     public static void shutdown()
556         throws StartupException
557     {
558         final Context compCtx;
559         try {
560             final InitialContext context = new InitialContext();
561             compCtx = (javax.naming.Context) context.lookup("java:comp");
562         } catch (final NamingException e) {
563             throw new StartupException("Could not initialize JNDI", e);
564         }
565         try {
566             Util.unbind(compCtx, "env/" + INamingBinds.RESOURCE_DATASOURCE);
567             Util.unbind(compCtx, "env/" + INamingBinds.RESOURCE_DBTYPE);
568             Util.unbind(compCtx, "env/" + INamingBinds.RESOURCE_CONFIGPROPERTIES);
569             Util.unbind(compCtx, "env/" + INamingBinds.RESOURCE_TRANSMANAG);
570         } catch (final NamingException e) {
571             throw new StartupException("unbind of the database connection failed", e);
572         }
573     }
574 }