Clover icon

magnolia-module-groovy 2.4.7

  1. Project Clover database Thu Dec 1 2016 10:48:40 CET
  2. Package info.magnolia.module.groovy.support.classes

File MgnlGroovyClassLoader.java

 

Coverage histogram

../../../../../../img/srcFileCovDistChart9.png
6% of files have more coverage

Code metrics

34
84
11
3
301
183
31
0.37
7.64
3.67
2.82

Classes

Class Line # Actions
MgnlGroovyClassLoader 78 37 0% 16 8
0.859649186%
MgnlGroovyClassLoader.AddDefaultImportOperation 182 7 0% 1 0
1.0100%
MgnlGroovyClassLoader.PackageAndClassNameConsistencyOperation 208 40 0% 14 4
0.937593.8%
 

Contributing tests

This file is covered by 34 tests. .

Source view

1    /**
2    * This file Copyright (c) 2010-2016 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 static info.magnolia.module.groovy.GroovyModule.SCRIPTS_REPOSITORY_NAME;
37   
38    import info.magnolia.context.Context;
39    import info.magnolia.context.SystemContext;
40    import info.magnolia.jcr.RuntimeRepositoryException;
41    import info.magnolia.module.groovy.support.HierarchyManagerProvider;
42    import info.magnolia.objectfactory.Components;
43   
44    import java.io.IOException;
45    import java.net.URL;
46    import java.net.URLConnection;
47    import java.security.CodeSource;
48    import java.util.List;
49   
50    import javax.inject.Provider;
51    import javax.jcr.RepositoryException;
52    import javax.jcr.Session;
53   
54    import org.apache.commons.lang.StringUtils;
55    import org.codehaus.groovy.ast.ASTNode;
56    import org.codehaus.groovy.ast.ClassNode;
57    import org.codehaus.groovy.ast.builder.AstBuilder;
58    import org.codehaus.groovy.control.CompilationFailedException;
59    import org.codehaus.groovy.control.CompilationUnit;
60    import org.codehaus.groovy.control.CompilationUnit.SourceUnitOperation;
61    import org.codehaus.groovy.control.CompilePhase;
62    import org.codehaus.groovy.control.CompilerConfiguration;
63    import org.codehaus.groovy.control.Phases;
64    import org.codehaus.groovy.control.SourceUnit;
65    import org.codehaus.groovy.control.io.URLReaderSource;
66    import org.slf4j.Logger;
67    import org.slf4j.LoggerFactory;
68   
69    import groovy.lang.GroovyClassLoader;
70   
71    /**
72    * Magnolia class loader which extends {@link GroovyClassLoader} and internally uses {@link MgnlGroovyResourceLoader}.
73    *
74    * @see MgnlGroovyClassLoader#isSourceNewer(URL, Class)
75    * @see MgnlGroovyClassLoader.AddDefaultImportOperation
76    * @see MgnlGroovyClassLoader.PackageAndClassNameConsistencyOperation
77    */
 
78    public final class MgnlGroovyClassLoader extends GroovyClassLoader {
79    private static final Logger log = LoggerFactory.getLogger(MgnlGroovyClassLoader.class);
80   
81    private final Provider<Context> provider;
82   
83    private boolean compileTimeChecks;
84    private String path;
85   
 
86  31 toggle public MgnlGroovyClassLoader() {
87  31 this.provider = new Provider<Context>() {
 
88  162 toggle @Override
89    public Context get() {
90  162 return Components.getComponent(SystemContext.class);
91    }
92    };
93  31 this.setShouldRecompile(true);
94  31 this.setResourceLoader(new MgnlGroovyResourceLoader(super.getResourceLoader(), provider, SCRIPTS_REPOSITORY_NAME));
95    }
96   
97    /**
98    * @deprecated since 2.4.4 please use {@link #MgnlGroovyClassLoader()} instead.
99    */
 
100  0 toggle @Deprecated
101    public MgnlGroovyClassLoader(Context context) {
102  0 this();
103    }
104   
105    /**
106    * @deprecated since 2.4.3 please use {@link #MgnlGroovyClassLoader()} instead.
107    */
 
108  0 toggle @Deprecated
109    public MgnlGroovyClassLoader(HierarchyManagerProvider hmp) {
110  0 this();
111    }
112   
 
113  42 toggle @Override
114    protected CompilationUnit createCompilationUnit(CompilerConfiguration config, CodeSource codeSource) {
115  42 CompilationUnit cu = super.createCompilationUnit(config, codeSource);
116  42 if (compileTimeChecks) {
117  14 log.debug("enforcing compile-time checks...");
118    /*
119    * "Generally speaking, there is more type information available later in the phases. If your transformation is concerned with
120    * reading the AST, then a later phase where information is more plentiful might be a good choice. If your transformation is
121    * concerned with writing AST, then an earlier phase where the tree is more sparse might be more convenient."
122    * see http://groovy.codehaus.org/Compiler+Phase+Guide
123    */
124  14 try {
125  14 cu.addPhaseOperation(new PackageAndClassNameConsistencyOperation(provider.get().getJCRSession(SCRIPTS_REPOSITORY_NAME), path), Phases.CONVERSION);
126    } catch (RepositoryException e) {
127  0 throw new RuntimeRepositoryException(e);
128    }
129    }
130   
131  42 cu.addPhaseOperation(new AddDefaultImportOperation(), Phases.CONVERSION);
132  42 return cu;
133    }
134   
135    /**
136    * <em>Source newer</em> in our case means that the groovy source
137    * representing a certain class in the <em>scripts</em> repository is more recent than that of
138    * the corresponding class currently loaded in this classloader (i.e. it has been saved more recently).
139    * Please note that if source protocol is <em>file</em>, then <strong>recompilation is forced</strong>
140    * otherwise the caller gets the old class in the current classloader which might be the compiled script
141    * from the scripts repository if the latter (the script, that is) was just disabled.
142    */
 
143  14 toggle @Override
144    protected boolean isSourceNewer(URL source, Class cls) throws IOException {
145  14 long lastMod;
146  14 if ("file".equals(source.getProtocol())) {
147  4 return true;
148    } else {
149  10 URLConnection conn = source.openConnection();
150  10 lastMod = conn.getLastModified();
151  10 conn.getInputStream().close();
152    }
153  10 boolean isNewer = lastMod > getTimeStamp(cls);
154  10 if (isNewer) {
155  4 log.debug("{} source has changed", cls.getName());
156    }
157  10 return isNewer;
158    }
159   
160    /**
161    * Checks that the given source compiles correctly and, in case of a script which has to act as a
162    * class, that some consistency constraints imposed by our classloading mechanism are enforced. Throws a
163    * {@link CompilationFailedException} in case of compilation failure.
164    * For the applied constraints, see {@link MgnlGroovyClassLoader.PackageAndClassNameConsistencyOperation}.
165    *
166    * @param source String the Groovy source
167    * @param enforceCompileChecks boolean if <code>true</code> enforce additional compile time checks
168    * @param path String the path to the script in the repository. The substring after the last '/' is assumed to be
169    * the script name itself. Will be ignored if <em>enforceCompileChecks</em> is <code>false</code>. Cannot be
170    * <code>null</code> if <em>enforceCompileChecks</em> is <code>true</code>.
171    */
 
172  17 toggle public final void verify(final String source, final boolean enforceCompileChecks, final String path) throws CompilationFailedException {
173  17 this.compileTimeChecks = enforceCompileChecks;
174  17 this.path = path;
175  17 if (compileTimeChecks && path == null) {
176  0 throw new IllegalArgumentException("When compilation checks are enforced, mgnlPath cannot be null");
177    }
178  17 parseClass(source);
179   
180    }
181   
 
182    private static final class AddDefaultImportOperation extends SourceUnitOperation {
183   
 
184  36 toggle @Override
185    public void call(SourceUnit source) throws CompilationFailedException {
186    // yes, to import all the classes of a certain package you have to omit the * at the end
187    // see http://jira.codehaus.org/browse/GROOVY-3041
188  36 log.debug("adding default imports...");
189  36 source.getAST().addStarImport("info.magnolia.context.");
190  36 source.getAST().addStarImport("info.magnolia.cms.core.");
191  36 source.getAST().addStarImport("info.magnolia.cms.util.");
192  36 source.getAST().addStarImport("info.magnolia.jcr.util.");
193  36 source.getAST().addStarImport("info.magnolia.module.groovy.support.");
194  36 source.getAST().addStarImport("info.magnolia.module.groovy.xml.");
195    }
196    }
197   
198    /**
199    * Checks package and class name consistency on source code during Groovy's
200    * compilation phase. The checks performed are
201    * <ol>
202    * <li>Ensure that the class declares a package, unless it is being saved at root level of the <code>scripts</code>
203    * repository</li>
204    * <li>Ensure that the package matches the path to the source in Magnolia's scripts repository</li>
205    * <li>Ensure that the source contains at least one class named as the script itself.</li>
206    * </ol>
207    */
 
208    private static final class PackageAndClassNameConsistencyOperation extends SourceUnitOperation {
209    private static final String COMPILATION_ERROR_PARENT_NODE_UNMATCHED = "%s compilation failed: class package '%s' does not match parent node '%s' path in the %s repository";
210    private static final String COMPILATION_ERROR_EXISTING_PATH_UNMATCHED = "%s compilation failed: class package '%s' does not match an existing '%s' path in the %s repository";
211    private static final String COMPILATION_ERROR_CLASSNAME_AND_PACKAGE = "%s compilation failed: %s should declare at least one class named %s %s";
212    private static final String COMPILATION_ERROR_CLASSNAME_UNDER_ROOT_WITH_PACKAGE = "%s compilation failed: your class is under root but declares a package %s";
213    private static final String COMPILATION_ERROR_NO_PACKAGE = "%s compilation failed: you must specify a package for your class.";
214   
215    private final Session session;
216    private final String mgnlPath;
217   
 
218  14 toggle public PackageAndClassNameConsistencyOperation(Session session, String mgnlPath) {
219  14 this.session = session;
220  14 this.mgnlPath = mgnlPath;
221    }
222   
 
223  14 toggle @Override
224    public void call(SourceUnit source) throws CompilationFailedException {
225  14 final String parentPath = StringUtils.defaultIfEmpty(StringUtils.substringBeforeLast(mgnlPath, "/"), "/");
226  14 final String scriptName = StringUtils.substringAfterLast(mgnlPath, "/");
227  14 final boolean isParentRoot = "/".equals(parentPath);
228  14 final String packageName = source.getAST().hasPackageName() ? source.getAST().getPackageName() : "";
229  14 final String packageNameAsPath = "/" + packageName.replace('.', '/');
230   
231  14 log.debug("parent path is {}, script name is {}, isParentRoot? {}", new Object[]{parentPath, scriptName, isParentRoot});
232   
233  14 if (!isParentRoot) {
234  11 if (StringUtils.isBlank(packageName)) {
235  1 String msg = String.format(COMPILATION_ERROR_NO_PACKAGE, scriptName);
236  1 log.warn(msg);
237  1 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
238    }
239   
240  10 if (!packageNameAsPath.equals(parentPath + "/")) {
241  1 final String msg = String.format(COMPILATION_ERROR_PARENT_NODE_UNMATCHED, scriptName, packageName, parentPath, SCRIPTS_REPOSITORY_NAME);
242  1 log.warn(msg);
243  1 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
244    }
245   
246  9 try {
247  9 if (!session.nodeExists(packageNameAsPath)) {
248  2 String msg = String.format(COMPILATION_ERROR_EXISTING_PATH_UNMATCHED, scriptName, packageName, packageNameAsPath, SCRIPTS_REPOSITORY_NAME);
249  2 log.warn(msg);
250  2 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
251    }
252    } catch (RepositoryException e) {
253  0 throw new RuntimeRepositoryException(e);
254    }
255    } else {
256  3 if (StringUtils.isNotBlank(packageName)) {
257  1 String msg = String.format(COMPILATION_ERROR_CLASSNAME_UNDER_ROOT_WITH_PACKAGE, scriptName, packageName);
258  1 log.warn(msg);
259  1 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
260    }
261    }
262    // we don't need to check that class A used in class B (included via URLReaderSource) declare class A in current script
263  9 if (source.getSource() instanceof URLReaderSource) {
264  0 return;
265    }
266   
267    // now checking that at least one class declared in the source matches the source file name
268  9 boolean match = false;
269  9 final String fullyQualifiedClassName = StringUtils.isNotBlank(packageName) ? packageName + scriptName : scriptName;
270  9 for (ClassNode cn : source.getAST().getClasses()) {
271  10 if (fullyQualifiedClassName.equals(cn.getName())) {
272  8 log.debug("found a matching class name {}, we can proceed", fullyQualifiedClassName);
273  8 match = true;
274  8 break;
275    }
276    }
277  9 if (!match) {
278  1 String msg = String.format(COMPILATION_ERROR_CLASSNAME_AND_PACKAGE, scriptName, fullyQualifiedClassName, scriptName, (StringUtils.isNotBlank(packageName) ? "and a package named " + packageName : ""));
279  1 log.warn(msg);
280  1 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
281    }
282    }
283    }
284   
285    /**
286    * Utility method to resolve the script or class name from source.
287    */
 
288  8 toggle public static final String resolveScriptNameFromSource(final String source) throws RepositoryException {
289  8 final List<ASTNode> nodes = new AstBuilder().buildFromString(CompilePhase.CONVERSION, source);
290  8 for (ASTNode astNode : nodes) {
291  15 if (astNode instanceof ClassNode) {
292  7 ClassNode cn = (ClassNode) astNode;
293  7 String name = cn.getNameWithoutPackage();
294  7 if (!name.startsWith("script")) {
295  7 return name;
296    }
297    }
298    }
299  1 return "untitledGroovyScript";
300    }
301    }