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.update.schema.dbproperty;
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.net.MalformedURLException;
27  import java.net.URL;
28  import java.util.ArrayList;
29  import java.util.Iterator;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Map.Entry;
33  import java.util.Properties;
34  import java.util.Set;
35  
36  import org.apache.commons.jexl2.JexlContext;
37  import org.apache.commons.lang3.BooleanUtils;
38  import org.efaps.admin.datamodel.Type;
39  import org.efaps.ci.CIAdmin;
40  import org.efaps.db.Insert;
41  import org.efaps.db.Instance;
42  import org.efaps.db.InstanceQuery;
43  import org.efaps.db.QueryBuilder;
44  import org.efaps.db.Update;
45  import org.efaps.update.IUpdate;
46  import org.efaps.update.Profile;
47  import org.efaps.update.UpdateLifecycle;
48  import org.efaps.update.util.InstallationException;
49  import org.efaps.util.EFapsException;
50  import org.slf4j.Logger;
51  import org.slf4j.LoggerFactory;
52  
53  /**
54   * Class for importing or updating of Properties from a properties-file into the
55   * Database for use as eFaps-Admin_Properties.<br>
56   * The import depends on the UUID of the Bundle. That means all Keys of the
57   * Properties must be unique within a Bundle. Therefore the import will update a
58   * key, if it is already existing inside this bundle. The Bundle will always be
59   * identified by the UUID and not by the name.
60   *
61   * @author The eFaps Team
62   * @version $Id$
63   */
64  public class DBPropertiesUpdate
65      implements IUpdate
66  {
67      /**
68       * name for the Type.
69       */
70      private static final String TYPE_PROPERTIES = "Admin_Common_DBProperties";
71  
72      /**
73       * name for the Type.
74       */
75      private static final String TYPE_PROPERTIES_BUNDLE = "Admin_Common_DBPropertiesBundle";
76  
77      /**
78       * name for the Type.
79       */
80      private static final String TYPE_PROPERTIES_LOCAL = "Admin_Common_DBPropertiesLocal";
81  
82      /**
83       * Logging instance used to give logging information of this class.
84       */
85      private static final Logger LOG = LoggerFactory.getLogger(DBPropertiesUpdate.class);
86  
87      /**
88       * the name of the Bundle.
89       */
90      private String bundlename;
91  
92      /**
93       * the UUID of the Bundle.
94       */
95      private String bundeluuid;
96  
97      /**
98       * the ID of the Bundle.
99       */
100     private long bundleid;
101 
102     /**
103      * Sequence of the Bundle.
104      */
105     private String bundlesequence;
106 
107     /**
108      * Set the load on start.
109      */
110     private boolean cacheOnStart;
111 
112     /**
113      * root of the XML-file to be imported.
114      */
115     private final String root;
116 
117     /**
118      * List of all Resources in this Properties.
119      */
120     private final List<Resource> resources = new ArrayList<Resource>();
121 
122     /**
123      * Name of the application.
124      */
125     private String fileApplication;
126 
127     /**
128      * Current read source.
129      * @see #readXML(List, Map, String)
130      */
131     private Resource curResource;
132 
133     /**
134      * The URL of the underlying file.
135      */
136     private final URL url;
137 
138     /**
139      * @param _url url for the file
140      */
141     public DBPropertiesUpdate(final URL _url)
142     {
143         this.url = _url;
144         final String urlStr = _url.toString();
145         this.root = urlStr.substring(0, urlStr.lastIndexOf(File.separator) + 1);
146     }
147 
148     /**
149      * find out the Id of the language used for this properties.
150      * @param _language Language
151      * @return ID of the Language
152      */
153     private Long getLanguageId(final String _language)
154     {
155         Long ret = null;
156         try {
157             final QueryBuilder queryBldr = new QueryBuilder(CIAdmin.Language);
158             queryBldr.addWhereAttrEqValue(CIAdmin.Language.Language, _language);
159             final InstanceQuery query = queryBldr.getQuery();
160             query.executeWithoutAccessCheck();
161             if (query.next()) {
162                 ret = query.getCurrentValue().getId();
163             } else {
164                 ret = insertNewLanguage(_language);
165             }
166         } catch (final EFapsException e) {
167             DBPropertiesUpdate.LOG.error("getLanguageId()", e);
168         }
169         return ret;
170     }
171 
172     /**
173      * inserts a new language into the Database.
174      *
175      * @param _language language to be inserted
176      * @return ID of the new language
177      */
178     private long insertNewLanguage(final String _language)
179     {
180         Long ret = null;
181         try {
182             final Insert insert = new Insert(CIAdmin.Language);
183             insert.add(CIAdmin.Language.Language, _language);
184             insert.executeWithoutAccessCheck();
185             ret = insert.getId();
186             insert.close();
187         } catch (final EFapsException e) {
188             DBPropertiesUpdate.LOG.error("insertNewLanguage()", e);
189         }
190         return ret;
191     }
192 
193     /**
194      * Insert a new Bundle into the Database.
195      *
196      * @return ID of the new Bundle
197      */
198     private long insertNewBundle()
199     {
200         Long ret = null;
201         try {
202             final Insert insert = new Insert(DBPropertiesUpdate.TYPE_PROPERTIES_BUNDLE);
203             insert.add("Name", this.bundlename);
204             insert.add("UUID", this.bundeluuid);
205             insert.add("Sequence", this.bundlesequence);
206             insert.add("CacheOnStart", this.cacheOnStart);
207             insert.executeWithoutAccessCheck();
208 
209             ret = insert.getId();
210             insert.close();
211 
212         } catch (final EFapsException e) {
213             DBPropertiesUpdate.LOG.error("insertNewBundle()", e);
214         }
215         return ret;
216     }
217 
218 
219     /**
220      * Import Properties from a Properties-File as default, if the key is
221      * already existing, the default will be replaced with the new default.
222      *
223      * @param _url Complete Path/Name of the property file to import
224      */
225     private void importFromProperties(final URL _url)
226     {
227         try {
228             final InputStream propInFile = _url.openStream();
229             final Properties props = new Properties();
230             props.load(propInFile);
231             final Iterator<Entry<Object, Object>> iter = props.entrySet().iterator();
232 
233             while (iter.hasNext()) {
234                 final Entry<Object, Object> element = iter.next();
235                 final Instance existing = getExistingKey(element.getKey().toString());
236                 if (existing == null) {
237                     insertNewProp(element.getKey().toString(), element.getValue().toString());
238                 } else {
239                     updateDefault(existing, element.getValue().toString());
240                 }
241             }
242 
243         } catch (final IOException e) {
244             DBPropertiesUpdate.LOG.error("ImportFromProperties() - I/O failed.", e);
245         }
246     }
247 
248     /**
249      * Import Properties from a Properties-File as language-specific value, if
250      * the key is not existing, a new default(=value) will also be created. If
251      * the language is not existing it will be created also.
252      *
253      * @param _url Complete Path/Name of the File to import
254      * @param _language Language to use for the Import
255      */
256     private void importFromProperties(final URL _url,
257                                       final String _language)
258     {
259         try {
260             final InputStream propInFile = _url.openStream();
261             final Properties props = new Properties();
262             props.load(propInFile);
263             final Iterator<Entry<Object, Object>> iter = props.entrySet().iterator();
264 
265             while (iter.hasNext()) {
266                 final Entry<Object, Object> element = iter.next();
267                 Instance propInstance = getExistingKey(element.getKey().toString());
268                 if (propInstance == null || !propInstance.isValid()) {
269                     propInstance = insertNewProp(element.getKey().toString(), element.getValue().toString());
270                 }
271 
272                 final Instance localInstance = getExistingLocale(propInstance.getId(), _language);
273                 if (localInstance == null || !localInstance.isValid()) {
274                     insertNewLocal(propInstance.getId(), element.getValue().toString(), _language);
275                 } else {
276                     updateLocale(localInstance, element.getValue().toString());
277                 }
278             }
279 
280         } catch (final IOException e) {
281             DBPropertiesUpdate.LOG.error("ImportFromProperties() - I/O failed.", e);
282         }
283     }
284 
285     /**
286      * Is a localized value already existing.
287      *
288      * @param _propertyid   ID of the Property, the localized value is related to
289      * @param _language     Language of the property
290      * @return OID of the value, otherwise null
291      */
292     private Instance getExistingLocale(final long _propertyid,
293                                        final String _language)
294     {
295         Instance ret = null;
296         try {
297             final QueryBuilder queryBldr = new QueryBuilder(Type.get(DBPropertiesUpdate.TYPE_PROPERTIES_LOCAL));
298             queryBldr.addWhereAttrEqValue("PropertyID", _propertyid);
299             queryBldr.addWhereAttrEqValue("LanguageID", getLanguageId(_language));
300             final InstanceQuery query = queryBldr.getQuery();
301             query.executeWithoutAccessCheck();
302             if (query.next()) {
303                 ret = query.getCurrentValue();
304             }
305         } catch (final EFapsException e) {
306             DBPropertiesUpdate.LOG.error("getExistingLocale(String)", e);
307         }
308         return ret;
309     }
310 
311     /**
312      * Insert a new localized Value.
313      *
314      * @param _propertyid   ID of the Property, the localized value is related to
315      * @param _value        Value of the Property
316      * @param _language     Language of the property
317      */
318     private void insertNewLocal(final long _propertyid,
319                                 final String _value,
320                                 final String _language)
321     {
322         try {
323             final Insert insert = new Insert(DBPropertiesUpdate.TYPE_PROPERTIES_LOCAL);
324             insert.add("Value", _value);
325             insert.add("PropertyID", _propertyid);
326             insert.add("LanguageID", getLanguageId(_language));
327             insert.executeWithoutAccessCheck();
328             insert.close();
329         } catch (final EFapsException e) {
330             DBPropertiesUpdate.LOG.error("insertNewLocal(String)", e);
331         }
332     }
333 
334     /**
335      * Update a localized Value.
336      *
337      * @param _localeInst OID, of the localized Value
338      * @param _value Value
339      */
340     private void updateLocale(final Instance _localeInst,
341                               final String _value)
342     {
343         try {
344             final Update update = new Update(_localeInst);
345             update.add("Value", _value);
346             update.execute();
347         } catch (final EFapsException e) {
348             DBPropertiesUpdate.LOG.error("updateLocale(String, String)", e);
349         }
350     }
351 
352     /**
353      * Is a key already existing.
354      *
355      * @param _key Key to search for
356      * @return OID of the key, otherwise null
357      */
358     private Instance getExistingKey(final String _key)
359     {
360         Instance ret = null;
361         try {
362             final QueryBuilder queryBldr = new QueryBuilder(Type.get(DBPropertiesUpdate.TYPE_PROPERTIES));
363             queryBldr.addWhereAttrEqValue("Key", _key);
364             queryBldr.addWhereAttrEqValue("BundleID", this.bundleid);
365             final InstanceQuery query = queryBldr.getQuery();
366             query.executeWithoutAccessCheck();
367             if (query.next()) {
368                 ret = query.getCurrentValue();
369             }
370         } catch (final EFapsException e) {
371             DBPropertiesUpdate.LOG.error("getExisting()", e);
372         }
373         return ret;
374     }
375 
376     /**
377      * Update a Default.
378      *
379      * @param _inst      OID of the value to update
380      * @param _value    value
381      */
382     private void updateDefault(final Instance _inst,
383                                final String _value)
384     {
385         try {
386             final Update update = new Update(_inst);
387             update.add("Default", _value);
388             update.execute();
389         } catch (final EFapsException e) {
390             DBPropertiesUpdate.LOG.error("updateDefault(String, String)", e);
391         }
392     }
393 
394     /**
395      * Insert a new Property.
396      *
397      * @param _key      Key to insert
398      * @param _value    value to insert
399      * @return ID of the new Property
400      */
401     private Instance insertNewProp(final String _key,
402                                  final String _value)
403     {
404         Instance ret = null;
405         try {
406             final Insert insert = new Insert(DBPropertiesUpdate.TYPE_PROPERTIES);
407             insert.add("BundleID", this.bundleid);
408             insert.add("Key", _key);
409             insert.add("Default", _value);
410             insert.executeWithoutAccessCheck();
411             ret = insert.getInstance();
412             insert.close();
413 
414         } catch (final EFapsException e) {
415             DBPropertiesUpdate.LOG.error("InsertNew(String, String)", e);
416         }
417         return ret;
418     }
419 
420     /**
421      * Is the Bundle allready existing.
422      *
423      * @param _uuid UUID of the Bundle
424      * @return ID of the Bundle if existing, else null
425      */
426     private Long getExistingBundle(final String _uuid)
427     {
428         Long ret = null;
429         try {
430             final QueryBuilder queryBldr = new QueryBuilder(Type.get(DBPropertiesUpdate.TYPE_PROPERTIES_BUNDLE));
431             queryBldr.addWhereAttrEqValue("UUID", _uuid);
432             final InstanceQuery query = queryBldr.getQuery();
433             query.executeWithoutAccessCheck();
434             if (query.next()) {
435                 ret = query.getCurrentValue().getId();
436             }
437         } catch (final EFapsException e) {
438             DBPropertiesUpdate.LOG.error("getExistingBundle(String)", e);
439         }
440         return ret;
441     }
442 
443     /**
444      * {@inheritDoc}
445      */
446     @Override
447     public String getFileApplication()
448     {
449         return this.fileApplication;
450     }
451 
452     /**
453      * {@inheritDoc}
454      */
455     @Override
456     public void updateInDB(final JexlContext _jexlContext,
457                            final UpdateLifecycle _step,
458                            final Set<Profile> _profile)
459         throws InstallationException
460     {
461         if (_step == UpdateLifecycle.DBPROPERTIES_UPDATE) {
462 
463             if (DBPropertiesUpdate.LOG.isInfoEnabled()) {
464                 DBPropertiesUpdate.LOG.info("Importing Properties '" + this.bundlename + "'");
465             }
466 
467             final Long bundleID = getExistingBundle(this.bundeluuid);
468 
469             if (bundleID == null) {
470                 this.bundleid = insertNewBundle();
471             } else {
472                 this.bundleid = bundleID;
473             }
474             try {
475                 for (final Resource resource : this.resources) {
476                     if ("Properties".equals(resource.type)) {
477                         if (resource.language == null || resource.language.length() < 1) {
478                             importFromProperties(new URL(this.root + resource.filename));
479                         } else {
480                             importFromProperties(new URL(this.root + resource.filename), resource.language);
481                         }
482                     }
483                 }
484             } catch (final MalformedURLException e) {
485                 DBPropertiesUpdate.LOG.error("The URL given for one File of the DBProperties is invalid", e);
486             }
487         }
488     }
489 
490     /**
491      * {@inheritDoc}
492      */
493     @Override
494     public void readXML(final List<String> _tags,
495                         final Map<String, String> _attributes,
496                         final String _text)
497     {
498         final String value = _tags.get(0);
499 
500         if ("uuid".equals(value)) {
501             this.bundeluuid = _text;
502         } else if ("file-application".equals(value)) {
503             this.fileApplication = _text;
504         } else if ("bundle".equals(value)) {
505             this.bundlename = _attributes.get("name");
506             this.bundlesequence = _attributes.get("sequence");
507             this.cacheOnStart = BooleanUtils.toBoolean(_attributes.get("cacheOnStart"));
508         } else if ("resource".equals(value)) {
509             if (_tags.size() == 1) {
510                 this.curResource = new Resource();
511                 this.resources.add(this.curResource);
512             } else {
513                 this.curResource.readXML(_tags.subList(1, _tags.size()), _attributes, _text);
514             }
515         }
516     }
517 
518     /**
519      * {@inheritDoc}
520      */
521     @Override
522     public URL getURL()
523     {
524         return this.url;
525     }
526 
527     /**
528      * Class to store the different Resources witch can come with one bundle.
529      */
530     public static class Resource
531     {
532 
533         /**
534          * type of the Properties.
535          */
536         private String type;
537 
538         /**
539          * language of the Properties.
540          */
541         private String language;
542 
543         /**
544          * Filename of the Properties.
545          */
546         private String filename;
547 
548         /**
549          * Read event for given tags path with attributes and text.
550          *
551          * @param _tags         tags path as list
552          * @param _attributes   map of attributes for current tag
553          * @param _text         content text of this tags path
554          */
555         public void readXML(final List<String> _tags,
556                             final Map<String, String> _attributes,
557                             final String _text)
558         {
559             final String value = _tags.get(0);
560             if ("type".equals(value)) {
561                 this.type = _text;
562             } else if ("language".equals(value)) {
563                 this.language = _text;
564             } else if ("file".equals(value)) {
565                 this.filename = _text;
566             }
567         }
568     }
569 }