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;
22  
23  import java.io.IOException;
24  import java.lang.reflect.InvocationTargetException;
25  import java.lang.reflect.Method;
26  import java.net.URL;
27  import java.sql.SQLException;
28  import java.util.ArrayList;
29  import java.util.Collections;
30  import java.util.Comparator;
31  import java.util.HashMap;
32  import java.util.HashSet;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Properties;
36  import java.util.Set;
37  
38  import org.apache.commons.jexl2.JexlContext;
39  import org.apache.commons.jexl2.MapContext;
40  import org.apache.commons.lang3.builder.ToStringBuilder;
41  import org.efaps.admin.EFapsSystemConfiguration;
42  import org.efaps.admin.KernelSettings;
43  import org.efaps.ci.CIAdminCommon;
44  import org.efaps.db.Context;
45  import org.efaps.db.MultiPrintQuery;
46  import org.efaps.db.QueryBuilder;
47  import org.efaps.update.util.InstallationException;
48  import org.efaps.util.EFapsException;
49  import org.slf4j.Logger;
50  import org.slf4j.LoggerFactory;
51  import org.xml.sax.SAXException;
52  
53  /**
54   * TODO description.
55   *
56   * @author The eFaps Team
57   * @version $Id$
58   */
59  public class Install
60  {
61      /**
62       * Logging instance used to give logging information of this class.
63       */
64      private static final Logger LOG = LoggerFactory.getLogger(Install.class);
65  
66      /**
67       * All defined file urls which are updated.
68       *
69       * @see #addFile(URL, String)
70       */
71      private final List<InstallFile> files = new ArrayList<InstallFile>();
72  
73      /**
74       * Flag to store that the cache is initialised.
75       *
76       * @see #initialise
77       * @see #addURL
78       */
79      private boolean initialised = false;
80  
81      /**
82       * Cache with all update instances (loaded from the list of {@link #urls}).
83       *
84       * @see #initialise
85       * @see #install
86       */
87      private final Map<Class<? extends IUpdate>, List<IUpdate>> cache
88          = new HashMap<Class<? extends IUpdate>, List<IUpdate>>();
89  
90      /**
91       * Evaluate the profiles from SystemConfiguration.
92       */
93      private final boolean evaluateProfiles;
94  
95      /**
96       * Standard Constructor.
97       */
98      public Install()
99      {
100         this(false);
101     }
102 
103     /**
104      * @param _evaluateProfiles evaluate profiles from systemconfiguration
105      */
106     public Install(final boolean _evaluateProfiles)
107     {
108         this.evaluateProfiles = _evaluateProfiles;
109     }
110 
111     /**
112      * Installs the XML update scripts of the schema definitions for this
113      * version defined in {@link #number}. The install itself is done for given
114      * version normally in one big transaction. If the database does not support
115      * to big transactions (method
116      * {@link org.efaps.db.databases.AbstractDatabase#supportsBigTransactions()},
117      * each modification of one update is committed within small single
118      * transactions.
119      *
120      * @param _number           number to install
121      * @param _latestNumber     latest version number to install (e..g. defined
122      *                          in the version.xml file)
123      * @param _profiles         profiles to be applied
124      * @param _ignoredSteps     set of ignored life cycle steps which are not
125      *                          executed
126      * @throws InstallationException on error
127      * @see org.efaps.db.databases.AbstractDatabase#supportsBigTransactions()
128      */
129     public void install(final Long _number,
130                         final Long _latestNumber,
131                         final Set<Profile> _profiles,
132                         final Set<UpdateLifecycle> _ignoredSteps)
133         throws InstallationException
134     {
135         final boolean bigTrans = Context.getDbType().supportsBigTransactions();
136         final String user;
137         try  {
138             user = Context.getThreadContext().getPerson() != null
139                    ? Context.getThreadContext().getPerson().getName()
140                    : null;
141         } catch (final EFapsException e)  {
142             throw new InstallationException("No context in this thread defined!", e);
143         }
144 
145         // initialize cache
146         initialise();
147 
148         // initialize JexlContext (used to evaluate version)
149         final JexlContext jexlContext = new MapContext();
150         if (_number != null) {
151             jexlContext.set("version", _number);
152         }
153         if (_latestNumber != null) {
154             jexlContext.set("latest", _latestNumber);
155         }
156 
157         // loop through all life cycle steps
158         for (final UpdateLifecycle step : getUpdateLifecycles())  {
159             if (!_ignoredSteps.contains(step)) {
160                 if (Install.LOG.isInfoEnabled())  {
161                     Install.LOG.info("..Running Lifecycle step " + step);
162                 }
163                 for (final Map.Entry<Class<? extends IUpdate>, List<IUpdate>> entry : this.cache.entrySet()) {
164                     final List<IUpdate> updates = entry.getValue();
165                     Collections.sort(updates, new Comparator<IUpdate>()
166                     {
167                         @Override
168                         public int compare(final IUpdate _update0,
169                                            final IUpdate _update1)
170                         {
171                             return String.valueOf(_update0.getURL()).compareTo(String.valueOf(_update1.getURL()));
172                         }
173                     });
174                     for (final IUpdate update : updates) {
175                         try {
176                             update.updateInDB(jexlContext, step,
177                                             evaluateProfiles(update.getFileApplication(), _profiles));
178                             if (!bigTrans) {
179                                 Context.commit();
180                                 Context.begin(user);
181                             }
182                         } catch (final EFapsException e) {
183                             throw new InstallationException("Transaction start failed", e);
184                         }
185 
186                     }
187                 }
188             } else if (Install.LOG.isInfoEnabled())  {
189                 Install.LOG.info("..Skipped Lifecycle step " + step);
190             }
191         }
192     }
193 
194     /**
195      * Method to get all UpdateLifecycle in an ordered List.
196      * @return ordered List of all UpdateLifecycle
197      */
198     private List<UpdateLifecycle> getUpdateLifecycles()
199     {
200         final List<UpdateLifecycle> ret = new ArrayList<UpdateLifecycle>();
201         for (final UpdateLifecycle cycle : UpdateLifecycle.values()) {
202             ret.add(cycle);
203         }
204         Collections.sort(ret, new Comparator<UpdateLifecycle>() {
205 
206             @Override
207             public int compare(final UpdateLifecycle _cycle1,
208                                final UpdateLifecycle _cycle2)
209             {
210                 return _cycle1.getOrder().compareTo(_cycle2.getOrder());
211             }
212         });
213 
214         return ret;
215     }
216 
217     /**
218      * All installation files are updated. For each file, the installation and
219      * latest version is evaluated depending from all installed version and the
220      * defined application in the XML update file. The installation version is
221      * the same as the latest version of the application.
222      * @param _profiles set of profiles to be used
223      * @throws InstallationException if update failed
224      */
225     public void updateLatest(final Set<Profile> _profiles)
226         throws InstallationException
227     {
228         final boolean bigTrans = Context.getDbType().supportsBigTransactions();
229         final String user;
230         try  {
231             user = Context.getThreadContext().getPerson() != null
232                    ? Context.getThreadContext().getPerson().getName()
233                    : null;
234         } catch (final EFapsException e)  {
235             throw new InstallationException("No context in this thread defined!", e);
236         }
237 
238         // initialize cache
239         initialise();
240 
241         // get for all applications the latest version
242         final Map<String, Integer> versions = getLatestVersions();
243 
244         // loop through all life cycle steps
245         for (final UpdateLifecycle step : getUpdateLifecycles()) {
246             if (Install.LOG.isInfoEnabled()) {
247                 Install.LOG.info("..Running Lifecycle step " + step);
248             }
249             for (final Map.Entry<Class<? extends IUpdate>, List<IUpdate>> entry : this.cache.entrySet()) {
250                 for (final IUpdate update : entry.getValue()) {
251                     final Integer latestVersion = versions.get(update.getFileApplication());
252                     // initialize JexlContext (used to evaluate version)
253                     final JexlContext jexlContext = new MapContext();
254                     if (latestVersion != null) {
255                         jexlContext.set("version", latestVersion);
256                         jexlContext.set("latest", latestVersion);
257                     }
258                     try {
259                         // and create
260                         update.updateInDB(jexlContext, step, evaluateProfiles(update.getFileApplication(), _profiles));
261                         if (!bigTrans) {
262                             Context.commit();
263                             Context.begin(user);
264                         }
265                     } catch (final EFapsException e) {
266                         throw new InstallationException("Transaction start failed", e);
267                     }
268 
269                 }
270             }
271         }
272     }
273 
274     /**
275      * Load the already installed versions for this application from eFaps. The
276      * method must be called within a Context begin and commit (it is not done
277      * itself in this method!
278      * @return Map containing the versions
279      * @throws InstallationException on error
280      */
281     public Map<String, Integer> getLatestVersions()
282         throws InstallationException
283     {
284         final Map<String, Integer> versions = new HashMap<String, Integer>();
285         try {
286             if (Context.getDbType().existsView(Context.getThreadContext().getConnection(), "V_ADMINTYPE")
287                             && CIAdminCommon.Version.getType() != null) {
288                 final QueryBuilder queryBldr = new QueryBuilder(CIAdminCommon.Version);
289                 final MultiPrintQuery multi = queryBldr.getPrint();
290                 multi.addAttribute(CIAdminCommon.Version.Name, CIAdminCommon.Version.Revision);
291                 multi.executeWithoutAccessCheck();
292                 while (multi.next()) {
293                     final String name = multi.<String>getAttribute(CIAdminCommon.Version.Name);
294                     final Integer revision = multi.<Integer>getAttribute(CIAdminCommon.Version.Revision);
295                     if (!versions.containsKey(name) || versions.get(name) < revision) {
296                         versions.put(name, revision);
297                     }
298                 }
299             }
300         } catch (final EFapsException e) {
301             throw new InstallationException("Latest version could not be found", e);
302         } catch (final SQLException e) {
303             throw new InstallationException("Latest version could not be found", e);
304         }
305         return versions;
306     }
307 
308     /**
309      * Reads all XML update files and parses them.
310      *
311      * @see #initialised
312      * @throws InstallationException on error
313      */
314     protected void initialise()
315         throws InstallationException
316     {
317 
318         if (!this.initialised) {
319             this.initialised = true;
320             this.cache.clear();
321             AppDependency.initialise();
322             for (final FileType fileType : FileType.values()) {
323 
324                 if (fileType == FileType.XML) {
325                     for (final InstallFile file : this.files) {
326                         if (file.getType() == fileType) {
327                             final SaxHandler handler = new SaxHandler();
328                             try {
329                                 final IUpdate elem = handler.parse(file.getUrl());
330                                 List<IUpdate> list = this.cache.get(elem.getClass());
331                                 if (list == null) {
332                                     list = new ArrayList<IUpdate>();
333                                     this.cache.put(elem.getClass(), list);
334                                 }
335                                 list.add(handler.getUpdate());
336                             } catch (final SAXException e) {
337                                 throw new InstallationException("initialise()", e);
338                             } catch (final IOException e) {
339                                 throw new InstallationException("initialise()", e);
340                             }
341                         }
342                     }
343                 } else {
344                     for (final Class<? extends AbstractUpdate> updateClass : fileType.getClazzes()) {
345 
346                         final List<IUpdate> list = new ArrayList<IUpdate>();
347                         this.cache.put(updateClass, list);
348 
349                         Method method = null;
350                         try {
351                             method = updateClass.getMethod("readFile", URL.class);
352                         } catch (final SecurityException e) {
353                             throw new InstallationException("initialise()", e);
354                         } catch (final NoSuchMethodException e) {
355                             throw new InstallationException("initialise()", e);
356                         }
357                         for (final InstallFile file : this.files) {
358                             if (file.getType() == fileType) {
359                                 Object obj = null;
360                                 try {
361                                     obj = method.invoke(null, file.getUrl());
362                                 } catch (final IllegalArgumentException e) {
363                                     throw new InstallationException("initialise()", e);
364                                 } catch (final IllegalAccessException e) {
365                                     throw new InstallationException("initialise()", e);
366                                 } catch (final InvocationTargetException e) {
367                                     throw new InstallationException("initialise()", e);
368                                 }
369                                 if (obj != null) {
370                                     list.add((AbstractUpdate) obj);
371                                 }
372                             }
373                         }
374                     }
375                 }
376             }
377         }
378     }
379 
380     /**
381      * Appends a new file defined through an URL and the string representation
382      * of the file type.
383      *
384      * @param _url URL of the file to append
385      * @param _type type of the file
386      * @see #files
387      * @see #initialised
388      * @see #addFile(URL, FileType) method called to add the URL after convert
389      *      the string representation of the type to a file type instance
390      */
391     public void addFile(final URL _url,
392                         final String _type)
393     {
394         addFile(_url, FileType.getFileTypeByType(_type));
395     }
396 
397     /**
398      * Appends a new file defined through an URL. The initialized flag
399      * {@link #initialized} is automatically reseted.
400      *
401      * @param _url URL of the file to add
402      * @param _fileType file type of the file to add
403      */
404     public void addFile(final URL _url,
405                         final FileType _fileType)
406     {
407         this.files.add(new InstallFile(_url, _fileType));
408         this.initialised = false;
409     }
410 
411     /**
412      * This is the getter method for the instance variable {@link #files}.
413      *
414      * @return value of instance variable {@link #files}
415      */
416     public List<InstallFile> getFiles()
417     {
418         return this.files;
419     }
420 
421     /**
422      * Getter method for the instance variable {@link #evaluateProfiles}.
423      *
424      * @return value of instance variable {@link #evaluateProfiles}
425      */
426     protected boolean isEvaluateProfiles()
427     {
428         return this.evaluateProfiles;
429     }
430 
431     /**
432      * @param _application  application used as key for SystemConfiguration
433      * @param _profile      profiles
434      * @return set of profiles
435      * @throws EFapsException on error
436      */
437     private Set<Profile> evaluateProfiles(final String _application,
438                                           final Set<Profile> _profile)
439         throws EFapsException
440     {
441         final Set<Profile> ret;
442         if (_profile == null && _application != null) {
443             ret = new HashSet<Profile>();
444             final Properties props = EFapsSystemConfiguration.get().getAttributeValueAsProperties(
445                             KernelSettings.PROFILES4UPDATE, true);
446             if (props.containsKey(_application)) {
447                 final String[] profileNames = props.getProperty(_application).split(";");
448                 for (final String name : profileNames) {
449                     ret.add(Profile.getProfile(name));
450                 }
451             }
452             if (ret.isEmpty()) {
453                 ret.add(Profile.getDefaultProfile());
454             }
455         } else {
456             ret = _profile;
457         }
458         Install.LOG.debug("Applying profiles: {}", ret);
459         return ret;
460     }
461 
462     /**
463      * Returns a string representation with values of all instance variables.
464      *
465      * @return string representation of this Application
466      */
467     @Override
468     public String toString()
469     {
470         return new ToStringBuilder(this)
471             .append("urls", this.files)
472             .toString();
473     }
474 
475     /**
476      * Class is used as a container for one file that must be installed.
477      */
478     public static class InstallFile
479     {
480         /**
481          * URL to the file.
482          */
483         private final URL url;
484 
485         /**
486          * Type of the file.
487          */
488         private final FileType type;
489 
490         /**
491          * @param _url      Url to the file
492          * @param _type     Type of the file.
493          */
494         public InstallFile(final URL _url,
495                            final FileType _type)
496         {
497             this.url = _url;
498             this.type = _type;
499         }
500 
501         /**
502          * This is the getter method for the instance variable {@link #url}.
503          *
504          * @return value of instance variable {@link #url}
505          */
506         public URL getUrl()
507         {
508             return this.url;
509         }
510 
511         /**
512          * This is the getter method for the instance variable {@link #type}.
513          *
514          * @return value of instance variable {@link #type}
515          */
516         public FileType getType()
517         {
518             return this.type;
519         }
520 
521         @Override
522         public String toString()
523         {
524             return ToStringBuilder.reflectionToString(this);
525         }
526     }
527 }