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.version;
22  
23  import groovy.lang.Binding;
24  import groovy.lang.GroovyShell;
25  import groovy.lang.Script;
26  
27  import java.io.IOException;
28  import java.io.InputStreamReader;
29  import java.io.Reader;
30  import java.io.StringReader;
31  import java.net.MalformedURLException;
32  import java.net.URL;
33  import java.util.ArrayList;
34  import java.util.HashSet;
35  import java.util.List;
36  import java.util.Set;
37  
38  import org.apache.commons.digester3.annotations.rules.CallMethod;
39  import org.apache.commons.digester3.annotations.rules.CallParam;
40  import org.apache.commons.digester3.annotations.rules.ObjectCreate;
41  import org.apache.commons.digester3.annotations.rules.SetProperty;
42  import org.apache.commons.lang3.builder.ToStringBuilder;
43  import org.efaps.admin.program.esjp.EFapsClassLoader;
44  import org.efaps.db.Context;
45  import org.efaps.update.Install;
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.mozilla.javascript.ImporterTopLevel;
51  import org.mozilla.javascript.Scriptable;
52  import org.mozilla.javascript.ScriptableObject;
53  import org.slf4j.Logger;
54  import org.slf4j.LoggerFactory;
55  
56  /**
57   * Defines one version of the application to install.
58   *
59   * @author The eFaps Team
60   * @version $Id$
61   * TODO: in case of a script: it must be possible to deactivate the context
62   */
63  @ObjectCreate(pattern = "install/version")
64  public class ApplicationVersion
65      implements Comparable<ApplicationVersion>
66  {
67      /**
68       * Logging instance used to give logging information of this class.
69       */
70      private static final Logger LOG = LoggerFactory.getLogger(ApplicationVersion.class);
71  
72      /**
73       * The number of the version is stored in this instance variable.
74       *
75       * @see #setNumber
76       * @see #getNumber
77       */
78      @SetProperty(pattern = "install/version", attributeName = "number")
79      private Long number = Long.valueOf(0);
80  
81      /**
82       * Store the information weather a compile must be done after installing
83       * this version.
84       *
85       * @see #setCompile(boolean)
86       */
87      @SetProperty(pattern = "install/version", attributeName = "compile")
88      private boolean compile = false;
89  
90      /**
91       * Is a login for this version needed? This means if a new transaction is
92       * started, a login with given user is made. The default value is
93       * <i>true</i>.
94       *
95       * @see #setLoginNeeded(boolean)
96       */
97      @SetProperty(pattern = "install/version", attributeName = "login")
98      private boolean loginNeeded = true;
99  
100     /**
101      * Is a reload cache for this version needed? This means before the
102      * installation of this version starts, a reload cache is done. The default
103      * value is <i>true</i>.
104      *
105      * @see #setReloadCacheNeeded
106      */
107     @SetProperty(pattern = "install/version", attributeName = "reloadCache")
108     private boolean reloadCacheNeeded = true;
109 
110     /**
111      * List of all scripts for this version.
112      *
113      * @see #addScript(String, String, String, String)
114      */
115     private final List<AbstractScript> scripts = new ArrayList<AbstractScript>();
116 
117     /**
118      * Description of this version.
119      *
120      * @see #appendDescription(String)
121      * @see #getDescription()
122      */
123     private final StringBuilder description = new StringBuilder();
124 
125     /**
126      * Set of ignored life cycle steps.
127      *
128      * @see #addIgnoredStep(String)
129      */
130     private final Set<UpdateLifecycle> ignoredSteps = new HashSet<UpdateLifecycle>();
131 
132     /**
133      * Application this version belongs to.
134      */
135     private Application application;
136 
137     /**
138      * Installs the XML update scripts of the schema definitions for this
139      * version defined in {@link #number}.
140      *
141      * @param _install install instance with all cached XML definitions
142      * @param _latestVersionNumber latest version number (defined in the
143      *            version.xml file)
144      * @param _profiles profiles to be applied
145      * @param _userName name of logged in user
146      * @param _password password of logged in user
147      * @throws InstallationException on error
148      */
149     public void install(final Install _install,
150                         final long _latestVersionNumber,
151                         final Set<Profile> _profiles,
152                         final String _userName,
153                         final String _password)
154         throws InstallationException
155     {
156         // reload cache if needed
157         if (this.reloadCacheNeeded) {
158             this.application.reloadCache();
159         }
160         try {
161             // start transaction (with user name if needed)
162             if (this.loginNeeded) {
163                 Context.begin(_userName);
164             } else {
165                 Context.begin();
166             }
167 
168             _install.install(this.number, _latestVersionNumber, _profiles, this.ignoredSteps);
169 
170             // commit transaction
171             Context.commit();
172 
173             // execute all scripts
174             for (final AbstractScript script : this.scripts) {
175                 script.execute(_userName, _password);
176             }
177 
178             // Compile esjp's in the database (if the compile flag is set).
179             if (this.compile) {
180                 this.application.compileAll(_userName, true);
181             }
182         } catch (final EFapsException e) {
183             throw new InstallationException("error in Context", e);
184         }
185     }
186 
187     /**
188      * Adds a new Script to this version.
189      *
190      * @param _code     code of script to execute
191      * @param _type     type of the code, groovy, rhino
192      * @param _name     file name of the script
193      * @param _function name of function which is called
194      */
195     @CallMethod(pattern = "install/version/script")
196     public void addScript(@CallParam(pattern = "install/version/script") final String _code,
197                           @CallParam(pattern = "install/version/script", attributeName = "type") final String _type,
198                           @CallParam(pattern = "install/version/script", attributeName = "name") final String _name,
199                           @CallParam(pattern = "install/version/script", attributeName = "function")
200                             final String _function)
201     {
202         AbstractScript script = null;
203         if ("rhino".equalsIgnoreCase(_type)) {
204             script = new RhinoScript(_code, _name, _function);
205         } else if ("groovy".equalsIgnoreCase(_type)) {
206             script = new GroovyScript(_code, _name, _function);
207         }
208         if (script != null) {
209             this.scripts.add(script);
210         }
211     }
212 
213     /**
214      * Append a description for this version.
215      *
216      * @param _desc text of description to append
217      * @see #description
218      */
219     @CallMethod(pattern = "install/version/description")
220     public void appendDescription(@CallParam(pattern = "install/version/description") final String _desc)
221     {
222         if (_desc != null) {
223             this.description.append(_desc.trim()).append("\n");
224         }
225     }
226 
227     /**
228      * @param _appl Application
229      */
230     public void setApplication(final Application _appl)
231     {
232         this.application = _appl;
233     }
234 
235     /**
236      * The description for this version is returned. If no description exists, a
237      * zero length description is returned.
238      *
239      * @return string value of instance variable {@link #description}
240      * @see #description
241      */
242     public String getDescription()
243     {
244         return this.description.toString().trim();
245     }
246 
247     /**
248      * Appends a step which is ignored within the installation of this version.
249      *
250      * @param _step ignored step
251      * @see #ignoredSteps
252      */
253     @CallMethod(pattern = "install/version/lifecyle/ignore")
254     public void addIgnoredStep(@CallParam(pattern = "install/version/lifecyle/ignore", attributeName = "step")
255                                 final String _step)
256     {
257         this.ignoredSteps.add(UpdateLifecycle.valueOf(_step.toUpperCase()));
258     }
259 
260     /**
261      * Compares this application version with the specified application version.<br/>
262      * The method compares the version number of the application version. To do
263      * this, the method {@link java.lang.Long#compareTo} is called.
264      *
265      * @param _compareTo application version instance to compare to
266      * @return a negative integer, zero, or a positive integer as this
267      *         application version is less than, equal to, or greater than the
268      *         specified application version
269      * @see java.lang.Long#compareTo
270      * @see java.lang.Comparable#compareTo
271      */
272     public int compareTo(final ApplicationVersion _compareTo)
273     {
274         return new Long(this.number).compareTo(_compareTo.number);
275     }
276 
277     /**
278      * This is the setter method for instance variable {@link #number}.
279      *
280      * @param _number new value for instance variable {@link #number}
281      * @see #number
282      * @see #getNumber
283      */
284     public void setNumber(final Long _number)
285     {
286         this.number = _number;
287     }
288 
289     /**
290      * This is the getter method for instance variable {@link #number}.
291      *
292      * @return value of instance variable {@link #number}
293      * @see #number
294      * @see #setNumber
295      */
296     public Long getNumber()
297     {
298         return this.number;
299     }
300 
301     /**
302      * This is the setter method for instance variable {@link #compile}.
303      *
304      * @param _compile new value for instance variable {@link #compile}
305      * @see #compile
306      */
307     public void setCompile(final boolean _compile)
308     {
309         this.compile = _compile;
310     }
311 
312     /**
313      * This is the setter method for instance variable {@link #loginNeeded}.
314      *
315      * @param _loginNeeded      <i>true</i> means that a login is needed for
316      *                          this version
317      * @see #loginNeeded
318      */
319     public void setLoginNeeded(final boolean _loginNeeded)
320     {
321         this.loginNeeded = _loginNeeded;
322     }
323 
324     /**
325      * This is the setter method for instance variable
326      * {@link #reloadCacheNeeded}.
327      *
328      * @param _reloadCacheNeeded    <i>true</i> means that the cache must be
329      *                              reloaded
330      * @see #reloadCacheNeeded
331      */
332     public void setReloadCacheNeeded(final boolean _reloadCacheNeeded)
333     {
334         this.reloadCacheNeeded = _reloadCacheNeeded;
335     }
336 
337     /**
338      * Returns the complete root URL so that resources in the installation
339      * package could be fetched. If an installation from the source directory
340      * is done, the {@link Application#getRootUrl() root URL} is directly
341      * returned, in the case that an installation is done from a JAR container
342      * the {@link Application#getRootUrl() root URL} is appended with the name
343      * of the {@link Application#getRootPackageName() root package name}.
344      *
345      * @return complete root URL
346      * @throws InstallationException if complete root URL could not be prepared
347      */
348     protected URL getCompleteRootUrl()
349         throws InstallationException
350     {
351         try {
352             final URL url;
353             if (this.application.getRootPackageName() == null) {
354                 url = this.application.getRootUrl();
355             } else {
356                 url = new URL(this.application.getRootUrl(), this.application.getRootPackageName());
357             }
358             return url;
359         } catch (final MalformedURLException e)  {
360             throw new InstallationException("Root url could not be prepared", e);
361         }
362     }
363 
364     /**
365      * Returns a string representation with values of all instance variables.
366      *
367      * @return string representation of this Application
368      */
369     @Override
370     public String toString()
371     {
372         return new ToStringBuilder(this)
373             .append("number", this.number)
374             .toString();
375     }
376 
377     /**
378      * Class used to store information of needed called scripts within an
379      * application version.
380      */
381     private abstract class AbstractScript
382     {
383 
384         /**
385          * Script code to execute.
386          */
387         private final String code;
388 
389         /**
390          * File name of the script (within the class path).
391          */
392         private final String fileName;
393 
394         /**
395          * Name of called function.
396          */
397         private final String function;
398 
399         /**
400          * Constructor to initialize a script.
401          *
402          * @param _code         script code
403          * @param _fileName     script file name
404          * @param _function     called function name
405          */
406         private AbstractScript(final String _code,
407                                final String _fileName,
408                                final String _function)
409         {
410             this.code = (_code == null) || ("".equals(_code.trim())) ? null : _code.trim();
411             this.fileName = _fileName;
412             this.function = _function;
413         }
414 
415         /**
416          * Executes this script.
417          *
418          * @param _userName name of logged in user
419          * @param _password password of logged in user
420          * @throws InstallationException on error
421          */
422         public abstract void execute(final String _userName,
423                                      final String _password)
424             throws InstallationException;
425 
426         /**
427          * Getter method for instance variable {@link #code}.
428          *
429          * @return value of instance variable {@link #code}
430          */
431         public String getCode()
432         {
433             return this.code;
434         }
435 
436         /**
437          * Getter method for instance variable {@link #fileName}.
438          *
439          * @return value of instance variable {@link #fileName}
440          */
441         public String getFileName()
442         {
443             return this.fileName;
444         }
445 
446         /**
447          * Getter method for instance variable {@link #function}.
448          *
449          * @return value of instance variable {@link #function}
450          */
451         public String getFunction()
452         {
453             return this.function;
454         }
455     }
456 
457     /**
458      *Script for groovy.
459      */
460     private final class GroovyScript
461         extends ApplicationVersion.AbstractScript
462     {
463         /**
464          * Constructor.
465          * @param _code         code
466          * @param _fileName     filename
467          * @param _function     function
468          */
469         private GroovyScript(final String _code,
470                              final String _fileName,
471                              final String _function)
472         {
473             super(_code, _fileName, _function);
474         }
475 
476         /**
477          * {@inheritDoc}
478          * @throws InstallationException if installation failed
479          * TODO: it must be able to deactivate the CONTEXT
480          */
481         @Override
482         public void execute(final String _userName,
483                             final String _password)
484             throws InstallationException
485         {
486             boolean commit = false;
487             try {
488                 try {
489                     Context.begin(_userName);
490                 } catch (final EFapsException e) {
491                     throw new InstallationException("Context could not be started", e);
492                 }
493                 final ClassLoader parent = getClass().getClassLoader();
494                 final EFapsClassLoader efapsClassLoader = EFapsClassLoader.getOfflineInstance(parent);
495 
496                 if (getCode() != null) {
497                     final Binding binding = new Binding();
498                     binding.setVariable("EFAPS_LOGGER", ApplicationVersion.LOG);
499                     binding.setVariable("EFAPS_USERNAME", _userName);
500                     binding.setVariable("EFAPS_PASSWORD", _userName);
501                     binding.setVariable("EFAPS_ROOTURL", getCompleteRootUrl());
502 
503                     final GroovyShell shell = new GroovyShell(efapsClassLoader, binding);
504                     final Script script = shell.parse(getCode());
505                     script.run();
506                 }
507                 try  {
508                     Context.commit();
509                 } catch (final EFapsException e) {
510                     throw new InstallationException("Transaction could not be commited", e);
511                 }
512                 commit = true;
513             } finally {
514                 if (!commit)  {
515                     try {
516                         Context.rollback();
517                     } catch (final EFapsException e) {
518                         throw new InstallationException("Tranaction could not be aborted", e);
519                     }
520                 }
521             }
522         }
523     }
524 
525     /**
526      * Script for mozilla rhino (Javascript).
527      */
528     private final class RhinoScript
529         extends ApplicationVersion.AbstractScript
530     {
531         /**
532          * Constructor.
533          *
534          * @param _code         code
535          * @param _fileName     filename
536          * @param _function     function
537          */
538         private RhinoScript(final String _code,
539                              final String _fileName,
540                              final String _function)
541         {
542             super(_code, _fileName, _function);
543         }
544 
545         /**
546          * {@inheritDoc}
547          */
548         @Override
549         public void execute(final String _userName,
550                             final String _password)
551             throws InstallationException
552         {
553             try {
554                 // create new javascript context
555                 final org.mozilla.javascript.Context javaScriptContext = org.mozilla.javascript.Context.enter();
556 
557                 final Scriptable scope = new ImporterTopLevel(javaScriptContext);
558 
559                 // define the context javascript property
560                 ScriptableObject.putProperty(scope, "javaScriptContext", javaScriptContext);
561 
562                 // define the scope javascript property
563                 ScriptableObject.putProperty(scope, "javaScriptScope", scope);
564 
565                 ScriptableObject.putProperty(scope, "EFAPS_LOGGER",
566                                 org.mozilla.javascript.Context.javaToJS(ApplicationVersion.LOG, scope));
567                 ScriptableObject.putProperty(scope, "EFAPS_USERNAME",
568                                 org.mozilla.javascript.Context.javaToJS(_userName, scope));
569                 ScriptableObject.putProperty(scope, "EFAPS_PASSWORD",
570                                 org.mozilla.javascript.Context.javaToJS(_userName, scope));
571                 ScriptableObject.putProperty(scope,
572                                              "EFAPS_ROOTURL",
573                                              org.mozilla.javascript.Context.javaToJS(getCompleteRootUrl(), scope));
574 
575                 // evaluate java script file (if defined)
576                 if (getFileName() != null) {
577                     if (ApplicationVersion.LOG.isInfoEnabled()) {
578                         ApplicationVersion.LOG.info("Execute script file '" + getFileName() + "'");
579                     }
580                     final Reader in = new InputStreamReader(
581                             new URL(getCompleteRootUrl(), getFileName()).openStream(), "UTF-8");
582                     javaScriptContext.evaluateReader(scope, in, getFileName(), 1, null);
583                     in.close();
584                 }
585 
586                 // evaluate script code (if defined)
587                 if (getCode() != null) {
588                     javaScriptContext.evaluateReader(scope, new StringReader(getCode()),
589                                     "Executing script code of version " + ApplicationVersion.this.number, 1, null);
590                 }
591 
592                 // evaluate script defined through the reader
593                 if (getFunction() != null) {
594                     if (ApplicationVersion.LOG.isInfoEnabled()) {
595                         ApplicationVersion.LOG.info("Execute script function '" + getFunction() + "'");
596                     }
597                     javaScriptContext.evaluateReader(scope, new StringReader(getFunction()), getFunction(), 1, null);
598                 }
599             } catch (final IOException e) {
600                 throw new InstallationException("IOException in RhinoScript", e);
601             }
602         }
603     }
604 }