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
209 private static final class PackageAndClassNameConsistencyOperation extends SourceUnitOperation {
210 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";
211 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";
212 private static final String COMPILATION_ERROR_CLASSNAME_AND_PACKAGE = "%s compilation failed: %s should declare at least one class named %s %s";
213 private static final String COMPILATION_ERROR_CLASSNAME_UNDER_ROOT_WITH_PACKAGE = "%s compilation failed: your class is under root but declares a package %s";
214 private static final String COMPILATION_ERROR_NO_PACKAGE = "%s compilation failed: you must specify a package for your class.";
215 private static final String COMPILATION_ERROR_CLASS_AND_SCRIPT_MIXED = "%s compilation failed: you cannot mix classes declarations and scripts here. " +
216 "Please, either remove the script part or set this source as a script in the UI";
217
218 private final Session session;
219 private final String mgnlPath;
220
221 public PackageAndClassNameConsistencyOperation(Session session, String mgnlPath) {
222 this.session = session;
223 this.mgnlPath = mgnlPath;
224 }
225
226 @Override
227 public void call(SourceUnit source) throws CompilationFailedException {
228 final String parentPath = StringUtils.defaultIfEmpty(StringUtils.substringBeforeLast(mgnlPath, "/"), "/");
229 final String scriptName = StringUtils.substringAfterLast(mgnlPath, "/");
230 final boolean isParentRoot = "/".equals(parentPath);
231 final String packageName = source.getAST().hasPackageName() ? source.getAST().getPackageName() : "";
232 final String packageNameAsPath = "/" + packageName.replace('.', '/');
233
234 log.debug("parent path is {}, script name is {}, isParentRoot? {}", new Object[]{parentPath, scriptName, isParentRoot});
235
236 if (!source.getAST().getStatementBlock().isEmpty()) {
237 final String msg = String.format(COMPILATION_ERROR_CLASS_AND_SCRIPT_MIXED, scriptName);
238 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
239 }
240
241 if (isParentRoot) {
242 if (StringUtils.isNotBlank(packageName)) {
243 final String msg = String.format(COMPILATION_ERROR_CLASSNAME_UNDER_ROOT_WITH_PACKAGE, scriptName, packageName);
244 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
245 }
246 } else {
247 if (StringUtils.isBlank(packageName)) {
248 String msg = String.format(COMPILATION_ERROR_NO_PACKAGE, scriptName);
249 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
250 }
251
252 if (!packageNameAsPath.equals(parentPath + "/")) {
253 final String msg = String.format(COMPILATION_ERROR_PARENT_NODE_UNMATCHED, scriptName, packageName, parentPath, SCRIPTS_REPOSITORY_NAME);
254 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
255 }
256
257 try {
258 if (!session.nodeExists(packageNameAsPath)) {
259 final String msg = String.format(COMPILATION_ERROR_EXISTING_PATH_UNMATCHED, scriptName, packageName, packageNameAsPath, SCRIPTS_REPOSITORY_NAME);
260 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
261 }
262 } catch (RepositoryException e) {
263 throw new RuntimeRepositoryException(e);
264 }
265 }
266
267 if (source.getSource() instanceof URLReaderSource) {
268 return;
269 }
270
271
272 boolean match = false;
273 final String fullyQualifiedClassName = StringUtils.isNotBlank(packageName) ? packageName + scriptName : scriptName;
274 for (ClassNode cn : source.getAST().getClasses()) {
275 if (fullyQualifiedClassName.equals(cn.getName())) {
276 log.debug("found a matching class name {}, we can proceed", fullyQualifiedClassName);
277 match = true;
278 break;
279 }
280 }
281 if (!match) {
282 final String msg = String.format(COMPILATION_ERROR_CLASSNAME_AND_PACKAGE, scriptName, fullyQualifiedClassName, scriptName, (StringUtils.isNotBlank(packageName) ? "and a package named " + packageName : ""));
283 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
284 }
285 }
286 }
287
288
289
290
291 public static final String resolveScriptNameFromSource(final String source) throws RepositoryException {
292 final List<ASTNode> nodes = new AstBuilder().buildFromString(CompilePhase.CONVERSION, source);
293 for (ASTNode astNode : nodes) {
294 if (astNode instanceof ClassNode) {
295 ClassNode cn = (ClassNode) astNode;
296 String name = cn.getNameWithoutPackage();
297 if (!name.startsWith("script")) {
298 return name;
299 }
300 }
301 }
302 return "untitledGroovyScript";
303 }
304 }