1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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.lang3.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
73
74
75
76
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 public MgnlGroovyClassLoader() {
87 this.provider = new Provider<Context>() {
88 @Override
89 public Context get() {
90 return Components.getComponent(SystemContext.class);
91 }
92 };
93 this.setShouldRecompile(true);
94 this.setResourceLoader(new MgnlGroovyResourceLoader(super.getResourceLoader(), provider, SCRIPTS_REPOSITORY_NAME));
95 }
96
97
98
99
100 @Deprecated
101 public MgnlGroovyClassLoader(Context context) {
102 this();
103 }
104
105
106
107
108 @Deprecated
109 public MgnlGroovyClassLoader(HierarchyManagerProvider hmp) {
110 this();
111 }
112
113 @Override
114 protected CompilationUnit createCompilationUnit(CompilerConfiguration config, CodeSource codeSource) {
115 CompilationUnit cu = super.createCompilationUnit(config, codeSource);
116 if (compileTimeChecks) {
117 log.debug("enforcing compile-time checks...");
118
119
120
121
122
123
124 try {
125 cu.addPhaseOperation(new PackageAndClassNameConsistencyOperation(provider.get().getJCRSession(SCRIPTS_REPOSITORY_NAME), path), Phases.CONVERSION);
126 } catch (RepositoryException e) {
127 throw new RuntimeRepositoryException(e);
128 }
129 }
130
131 cu.addPhaseOperation(new AddDefaultImportOperation(), Phases.CONVERSION);
132 return cu;
133 }
134
135
136
137
138
139
140
141
142
143 @Override
144 protected boolean isSourceNewer(URL source, Class cls) throws IOException {
145 long lastMod;
146 if ("file".equals(source.getProtocol())) {
147 return true;
148 } else {
149 URLConnection conn = source.openConnection();
150 lastMod = conn.getLastModified();
151 conn.getInputStream().close();
152 }
153 boolean isNewer = lastMod > getTimeStamp(cls);
154 if (isNewer) {
155 log.debug("{} source has changed", cls.getName());
156 }
157 return isNewer;
158 }
159
160
161
162
163
164
165
166
167
168
169
170
171
172 public final void verify(final String source, final boolean enforceCompileChecks, final String path) throws CompilationFailedException {
173 this.compileTimeChecks = enforceCompileChecks;
174 this.path = path;
175 if (compileTimeChecks && path == null) {
176 throw new IllegalArgumentException("When compilation checks are enforced, mgnlPath cannot be null");
177 }
178 parseClass(source);
179
180 }
181
182 private static final class AddDefaultImportOperation extends SourceUnitOperation {
183
184 @Override
185 public void call(SourceUnit source) throws CompilationFailedException {
186
187
188 log.debug("adding default imports...");
189 source.getAST().addStarImport("info.magnolia.context.");
190 source.getAST().addStarImport("info.magnolia.cms.core.");
191 source.getAST().addStarImport("info.magnolia.cms.util.");
192 source.getAST().addStarImport("info.magnolia.jcr.util.");
193 source.getAST().addStarImport("info.magnolia.module.groovy.support.");
194 source.getAST().addStarImport("info.magnolia.module.groovy.xml.");
195 }
196 }
197
198
199
200
201
202
203
204
205
206
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 public PackageAndClassNameConsistencyOperation(Session session, String mgnlPath) {
219 this.session = session;
220 this.mgnlPath = mgnlPath;
221 }
222
223 @Override
224 public void call(SourceUnit source) throws CompilationFailedException {
225 final String parentPath = StringUtils.defaultIfEmpty(StringUtils.substringBeforeLast(mgnlPath, "/"), "/");
226 final String scriptName = StringUtils.substringAfterLast(mgnlPath, "/");
227 final boolean isParentRoot = "/".equals(parentPath);
228 final String packageName = source.getAST().hasPackageName() ? source.getAST().getPackageName() : "";
229 final String packageNameAsPath = "/" + packageName.replace('.', '/');
230
231 log.debug("parent path is {}, script name is {}, isParentRoot? {}", new Object[]{parentPath, scriptName, isParentRoot});
232
233 if (!isParentRoot) {
234 if (StringUtils.isBlank(packageName)) {
235 String msg = String.format(COMPILATION_ERROR_NO_PACKAGE, scriptName);
236 log.warn(msg);
237 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
238 }
239
240 if (!packageNameAsPath.equals(parentPath + "/")) {
241 final String msg = String.format(COMPILATION_ERROR_PARENT_NODE_UNMATCHED, scriptName, packageName, parentPath, SCRIPTS_REPOSITORY_NAME);
242 log.warn(msg);
243 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
244 }
245
246 try {
247 if (!session.nodeExists(packageNameAsPath)) {
248 String msg = String.format(COMPILATION_ERROR_EXISTING_PATH_UNMATCHED, scriptName, packageName, packageNameAsPath, SCRIPTS_REPOSITORY_NAME);
249 log.warn(msg);
250 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
251 }
252 } catch (RepositoryException e) {
253 throw new RuntimeRepositoryException(e);
254 }
255 } else {
256 if (StringUtils.isNotBlank(packageName)) {
257 String msg = String.format(COMPILATION_ERROR_CLASSNAME_UNDER_ROOT_WITH_PACKAGE, scriptName, packageName);
258 log.warn(msg);
259 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
260 }
261 }
262
263 if (source.getSource() instanceof URLReaderSource) {
264 return;
265 }
266
267
268 boolean match = false;
269 final String fullyQualifiedClassName = StringUtils.isNotBlank(packageName) ? packageName + scriptName : scriptName;
270 for (ClassNode cn : source.getAST().getClasses()) {
271 if (fullyQualifiedClassName.equals(cn.getName())) {
272 log.debug("found a matching class name {}, we can proceed", fullyQualifiedClassName);
273 match = true;
274 break;
275 }
276 }
277 if (!match) {
278 String msg = String.format(COMPILATION_ERROR_CLASSNAME_AND_PACKAGE, scriptName, fullyQualifiedClassName, scriptName, (StringUtils.isNotBlank(packageName) ? "and a package named " + packageName : ""));
279 log.warn(msg);
280 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
281 }
282 }
283 }
284
285
286
287
288 public static final String resolveScriptNameFromSource(final String source) throws RepositoryException {
289 final List<ASTNode> nodes = new AstBuilder().buildFromString(CompilePhase.CONVERSION, source);
290 for (ASTNode astNode : nodes) {
291 if (astNode instanceof ClassNode) {
292 ClassNode cn = (ClassNode) astNode;
293 String name = cn.getNameWithoutPackage();
294 if (!name.startsWith("script")) {
295 return name;
296 }
297 }
298 }
299 return "untitledGroovyScript";
300 }
301 }