View Javadoc

1   /**
2    * This file Copyright (c) 2010-2011 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.module.groovy.support.classes;
35  
36  import groovy.lang.GroovyClassLoader;
37  import info.magnolia.cms.core.HierarchyManager;
38  import info.magnolia.module.groovy.support.HierarchyManagerProvider;
39  
40  import java.io.IOException;
41  import java.net.URL;
42  import java.net.URLConnection;
43  import java.security.CodeSource;
44  
45  import org.apache.commons.lang.StringUtils;
46  import org.codehaus.groovy.ast.ClassNode;
47  import org.codehaus.groovy.control.CompilationFailedException;
48  import org.codehaus.groovy.control.CompilationUnit;
49  import org.codehaus.groovy.control.CompilerConfiguration;
50  import org.codehaus.groovy.control.Phases;
51  import org.codehaus.groovy.control.SourceUnit;
52  import org.codehaus.groovy.control.CompilationUnit.SourceUnitOperation;
53  import org.slf4j.Logger;
54  import org.slf4j.LoggerFactory;
55  
56  
57  /**
58   * Magnolia class loader which extends {@link GroovyClassLoader} and internally uses
59   * {@link MgnlGroovyResourceLoader}.
60   * @see MgnlGroovyClassLoader#isSourceNewer(URL, Class)
61   * @see MgnlGroovyClassLoader.AddDefaultImportOperation
62   * @see MgnlGroovyClassLoader.PackageAndClassNameConsistencyOperation
63   * @author fgrilli
64   * @version $Id$
65   */
66  public final class MgnlGroovyClassLoader extends GroovyClassLoader {
67      private static final Logger log = LoggerFactory.getLogger(MgnlGroovyClassLoader.class);
68  
69      private static final String SCRIPTS = "scripts";
70  
71      private final HierarchyManagerProvider hmp;
72  
73      private boolean compileTimeChecks;
74  
75      private String mgnlPath;
76  
77      /**
78       * @param hmp {@link HierarchyManagerProvider} - it must not be null
79       */
80      public MgnlGroovyClassLoader(HierarchyManagerProvider hmp) {
81          if (hmp == null) {
82              throw new IllegalArgumentException("HierarchyManagerProvider can not be null");
83          }
84          this.hmp = hmp;
85          this.setShouldRecompile(true);
86          this.setResourceLoader(new MgnlGroovyResourceLoader(super.getResourceLoader(), hmp, SCRIPTS));
87      }
88  
89      @Override
90      protected CompilationUnit createCompilationUnit(CompilerConfiguration config, CodeSource codeSource) {
91          CompilationUnit cu = super.createCompilationUnit(config, codeSource);
92          if(compileTimeChecks){
93              log.debug("enforcing compile-time checks...");
94              /*
95               * "Generally speaking, there is more type information available later in the phases. If your transformation is concerned with
96               * reading the AST, then a later phase where information is more plentiful might be a good choice. If your transformation is
97               * concerned with writing AST, then an earlier phase where the tree is more sparse might be more convenient."
98               * see http://groovy.codehaus.org/Compiler+Phase+Guide
99               */
100              cu.addPhaseOperation(new PackageAndClassNameConsistencyOperation(hmp, mgnlPath), Phases.CONVERSION);
101         }
102 
103         cu.addPhaseOperation(new AddDefaultImportOperation(), Phases.CONVERSION);
104         return cu;
105     }
106 
107     /**
108      * <em>Source newer</em> in our case means that the groovy source
109      * representing a certain class in the <em>scripts</em> repository is more recent than that of
110      * the corresponding class currently loaded in this classloader (i.e. it has been changed).
111      * Please note that if source protocol is <em>file</em>, then <strong>recompilation is forced</strong>
112      * otherwise the caller gets the old class in the current classloader which might be the compiled script
113      * from the scripts repository if the latter (the script, that is) was just disabled.
114      */
115     @Override
116     protected boolean isSourceNewer(URL source, Class cls) throws IOException {
117         long lastMod = -1;
118         if ("file".equals(source.getProtocol())) {
119            return true;
120         } else {
121             URLConnection conn = source.openConnection();
122             lastMod = conn.getLastModified();
123             conn.getInputStream().close();
124         }
125         boolean isNewer = lastMod > getTimeStamp(cls);
126         if(isNewer){
127             log.info("{} source has changed", cls.getName());
128         }
129         return isNewer;
130     }
131 
132     /**
133      * Checks that the given source compiles correctly and, in case of a script which has to act as a
134      * class, that some consistency constraints imposed by our classloading mechanism are enforced. Throws a {@link CompilationFailedException}
135      * in case of compilation failure.
136      * For the applied constraints, see {@link PackageAndClassNameConsistencyOperation}
137      * @param source - String the Groovy source
138      * @param enforceCompileChecks - boolean if <code>true</code> enforce additional compile time checks
139      * @param mgnlPath - String the path to the script in the repository. The substring after the last '/' is assumed to be the script name itself. Will be ignored if
140      * <em>enforceCompileChecks</em> is <code>false</code>. Cannot be <code>null</code> if <em>enforceCompileChecks</em> is <code>true</code>.
141      *
142      */
143     public final void verify(final String source, final boolean enforceCompileChecks, final String mgnlPath) throws CompilationFailedException{
144         this.compileTimeChecks = enforceCompileChecks;
145         this.mgnlPath = mgnlPath;
146         if(compileTimeChecks && mgnlPath == null) {
147             throw new IllegalArgumentException("When compilation checks are enforced, mgnlPath cannot be null");
148         }
149         parseClass(source);
150    }
151 
152     private static final class AddDefaultImportOperation extends SourceUnitOperation {
153 
154         @Override
155         public void call(SourceUnit source) throws CompilationFailedException {
156             //yes, to import all the classes of a certain package you have to omit the * at the end
157             //see http://jira.codehaus.org/browse/GROOVY-3041
158             log.debug("adding default imports...");
159             source.getAST().addStarImport("info.magnolia.context.");
160             source.getAST().addStarImport("info.magnolia.cms.core.");
161             source.getAST().addStarImport("info.magnolia.cms.util.");
162             source.getAST().addStarImport("info.magnolia.jcr.util.");
163             source.getAST().addStarImport("info.magnolia.module.groovy.support.");
164         }
165     }
166 
167     /**
168      * Checks  package and class name consistency on source code during Groovy's
169      * compilation phase. The checks performed are
170      * <ol>
171      * <li>Ensure that the class declares a package, unless it is being saved at root level of the <code>scripts</code> repository
172      * <li>Ensure that the package matches the path to the source in Magnolia's
173      * scripts repository
174      * <li>Ensure that the source contains at least one class named as the script itself.
175      * </ol>
176      */
177     private static final class PackageAndClassNameConsistencyOperation extends SourceUnitOperation {
178         private HierarchyManagerProvider hmp;
179         private String mgnlPath;
180         public PackageAndClassNameConsistencyOperation(HierarchyManagerProvider hmp, String mgnlPath) {
181             this.hmp = hmp;
182             this.mgnlPath = mgnlPath;
183         }
184         @Override
185         public void call(SourceUnit source) throws CompilationFailedException {
186             final String parentPath = StringUtils.defaultIfEmpty(StringUtils.substringBeforeLast(mgnlPath, "/"), "/");
187             final String scriptName = StringUtils.substringAfterLast(mgnlPath, "/");
188             final boolean isParentRoot = "/".equals(parentPath);
189             String packageName = null;
190 
191             log.debug("parent path is {}, script name is {}, isParentRoot? {}", new Object[]{parentPath, scriptName, isParentRoot});
192 
193             //do the following checks only if the the parent node is other than root
194             if (!isParentRoot) {
195                 if(!source.getAST().hasPackageName()){
196                     String msg =  scriptName + " compilation failed: you must specify a package for your class.";
197                     log.warn(msg);
198                     throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
199                 }
200                 packageName = source.getAST().getPackageName();
201                 final String path = "/" + packageName.replace('.', '/');
202                 if (!path.equals(parentPath + "/")) {
203                     String msg = scriptName
204                         + " compilation failed: class package '"
205                         + packageName
206                         + "' does not match parent node  '"
207                         + parentPath
208                         + "' path in the scripts repository";
209                     log.warn(msg);
210                     throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
211                 }
212 
213                 final HierarchyManager hm = hmp.getHierarchyManager(SCRIPTS);
214                 if (!hm.isExist(path)) {
215                     String msg = scriptName
216                         + " compilation failed: class package '"
217                         + packageName
218                         + "' does not match an existing '"
219                         + path
220                         + "' path in the scripts repository";
221                     log.warn(msg);
222                     throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
223                 }
224             }
225             //now checking that at least one class declared in the source matches the source file name
226             boolean match = false;
227             final String fullyQualifiedClassName = packageName != null ? packageName + scriptName: scriptName;
228             for(ClassNode cn : source.getAST().getClasses()){
229                 if(fullyQualifiedClassName.equals(cn.getName())){
230                     log.debug("found a matching class name {}, we can proceed", fullyQualifiedClassName);
231                     match = true;
232                     break;
233                 }
234             }
235             if(!match){
236                 String msg =  fullyQualifiedClassName + " should declare at least one class named " + scriptName;
237                 log.warn(msg);
238                 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
239             }
240         }
241     }
242 }