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.objectfactory.Components;
42
43 import java.io.IOException;
44 import java.net.URL;
45 import java.net.URLConnection;
46 import java.security.CodeSource;
47 import java.util.List;
48
49 import javax.inject.Provider;
50 import javax.jcr.RepositoryException;
51 import javax.jcr.Session;
52
53 import org.apache.commons.lang3.StringUtils;
54 import org.codehaus.groovy.ast.ASTNode;
55 import org.codehaus.groovy.ast.ClassNode;
56 import org.codehaus.groovy.ast.builder.AstBuilder;
57 import org.codehaus.groovy.control.CompilationFailedException;
58 import org.codehaus.groovy.control.CompilationUnit;
59 import org.codehaus.groovy.control.CompilationUnit.SourceUnitOperation;
60 import org.codehaus.groovy.control.CompilePhase;
61 import org.codehaus.groovy.control.CompilerConfiguration;
62 import org.codehaus.groovy.control.Phases;
63 import org.codehaus.groovy.control.SourceUnit;
64 import org.codehaus.groovy.control.io.URLReaderSource;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
67
68 import groovy.lang.GroovyClassLoader;
69
70
71
72
73
74
75
76
77 public final class MgnlGroovyClassLoader extends GroovyClassLoader {
78 private static final Logger log = LoggerFactory.getLogger(MgnlGroovyClassLoader.class);
79
80 private final Provider<Context> provider;
81
82 private boolean compileTimeChecks;
83 private String path;
84
85 public MgnlGroovyClassLoader() {
86 this.provider = new Provider<Context>() {
87 @Override
88 public Context get() {
89 return Components.getComponent(SystemContext.class);
90 }
91 };
92 this.setShouldRecompile(true);
93 this.setResourceLoader(new MgnlGroovyResourceLoader(super.getResourceLoader(), provider, SCRIPTS_REPOSITORY_NAME));
94 }
95
96
97
98
99 @Deprecated
100 public MgnlGroovyClassLoader(Context context) {
101 this();
102 }
103
104 @Override
105 protected CompilationUnit createCompilationUnit(CompilerConfiguration config, CodeSource codeSource) {
106 CompilationUnit cu = super.createCompilationUnit(config, codeSource);
107 if (compileTimeChecks) {
108 log.debug("enforcing compile-time checks...");
109
110
111
112
113
114
115 try {
116 cu.addPhaseOperation(new PackageAndClassNameConsistencyOperation(provider.get().getJCRSession(SCRIPTS_REPOSITORY_NAME), path), Phases.CONVERSION);
117 } catch (RepositoryException e) {
118 throw new RuntimeRepositoryException(e);
119 }
120 }
121
122 cu.addPhaseOperation(new AddDefaultImportOperation(), Phases.CONVERSION);
123 return cu;
124 }
125
126
127
128
129
130
131
132
133
134 @Override
135 protected boolean isSourceNewer(URL source, Class cls) throws IOException {
136 long lastMod;
137 if ("file".equals(source.getProtocol())) {
138 return true;
139 } else {
140 URLConnection conn = source.openConnection();
141 lastMod = conn.getLastModified();
142 conn.getInputStream().close();
143 }
144 boolean isNewer = lastMod > getTimeStamp(cls);
145 if (isNewer) {
146 log.debug("{} source has changed", cls.getName());
147 }
148 return isNewer;
149 }
150
151
152
153
154
155
156
157
158
159
160
161
162
163 public final void verify(final String source, final boolean enforceCompileChecks, final String path) throws CompilationFailedException {
164 this.compileTimeChecks = enforceCompileChecks;
165 this.path = path;
166 if (compileTimeChecks && path == null) {
167 throw new IllegalArgumentException("When compilation checks are enforced, mgnlPath cannot be null");
168 }
169 parseClass(source);
170
171 }
172
173 private static final class AddDefaultImportOperation extends SourceUnitOperation {
174
175 @Override
176 public void call(SourceUnit source) throws CompilationFailedException {
177
178
179 log.debug("adding default imports...");
180 source.getAST().addStarImport("info.magnolia.context.");
181 source.getAST().addStarImport("info.magnolia.cms.core.");
182 source.getAST().addStarImport("info.magnolia.cms.util.");
183 source.getAST().addStarImport("info.magnolia.jcr.util.");
184 source.getAST().addStarImport("info.magnolia.module.groovy.support.");
185 source.getAST().addStarImport("info.magnolia.module.groovy.xml.");
186 }
187 }
188
189
190
191
192
193
194
195
196
197
198
199
200 private static final class PackageAndClassNameConsistencyOperation extends SourceUnitOperation {
201 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";
202 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";
203 private static final String COMPILATION_ERROR_CLASSNAME_AND_PACKAGE = "%s compilation failed: %s should declare at least one class named %s %s";
204 private static final String COMPILATION_ERROR_CLASSNAME_UNDER_ROOT_WITH_PACKAGE = "%s compilation failed: your class is under root but declares a package %s";
205 private static final String COMPILATION_ERROR_NO_PACKAGE = "%s compilation failed: you must specify a package for your class.";
206 private static final String COMPILATION_ERROR_CLASS_AND_SCRIPT_MIXED = "%s compilation failed: you cannot mix classes declarations and scripts here. " +
207 "Please, either remove the script part or set this source as a script in the UI";
208
209 private final Session session;
210 private final String mgnlPath;
211
212 public PackageAndClassNameConsistencyOperation(Session session, String mgnlPath) {
213 this.session = session;
214 this.mgnlPath = mgnlPath;
215 }
216
217 @Override
218 public void call(SourceUnit source) throws CompilationFailedException {
219 final String parentPath = StringUtils.defaultIfEmpty(StringUtils.substringBeforeLast(mgnlPath, "/"), "/");
220 final String scriptName = StringUtils.substringAfterLast(mgnlPath, "/");
221 final boolean isParentRoot = "/".equals(parentPath);
222 final String packageName = source.getAST().hasPackageName() ? source.getAST().getPackageName() : "";
223 final String packageNameAsPath = "/" + packageName.replace('.', '/');
224
225 log.debug("parent path is {}, script name is {}, isParentRoot? {}", new Object[]{parentPath, scriptName, isParentRoot});
226
227 if (!source.getAST().getStatementBlock().isEmpty()) {
228 final String msg = String.format(COMPILATION_ERROR_CLASS_AND_SCRIPT_MIXED, scriptName);
229 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
230 }
231
232 if (isParentRoot) {
233 if (StringUtils.isNotBlank(packageName)) {
234 final String msg = String.format(COMPILATION_ERROR_CLASSNAME_UNDER_ROOT_WITH_PACKAGE, scriptName, packageName);
235 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
236 }
237 } else {
238 if (StringUtils.isBlank(packageName)) {
239 String msg = String.format(COMPILATION_ERROR_NO_PACKAGE, scriptName);
240 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
241 }
242
243 if (!packageNameAsPath.equals(parentPath + "/")) {
244 final String msg = String.format(COMPILATION_ERROR_PARENT_NODE_UNMATCHED, scriptName, packageName, parentPath, SCRIPTS_REPOSITORY_NAME);
245 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
246 }
247
248 try {
249 if (!session.nodeExists(packageNameAsPath)) {
250 final String msg = String.format(COMPILATION_ERROR_EXISTING_PATH_UNMATCHED, scriptName, packageName, packageNameAsPath, SCRIPTS_REPOSITORY_NAME);
251 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
252 }
253 } catch (RepositoryException e) {
254 throw new RuntimeRepositoryException(e);
255 }
256 }
257
258 if (source.getSource() instanceof URLReaderSource) {
259 return;
260 }
261
262
263 boolean match = false;
264 final String fullyQualifiedClassName = StringUtils.isNotBlank(packageName) ? packageName + scriptName : scriptName;
265 for (ClassNode cn : source.getAST().getClasses()) {
266 if (fullyQualifiedClassName.equals(cn.getName())) {
267 log.debug("found a matching class name {}, we can proceed", fullyQualifiedClassName);
268 match = true;
269 break;
270 }
271 }
272 if (!match) {
273 final String msg = String.format(COMPILATION_ERROR_CLASSNAME_AND_PACKAGE, scriptName, fullyQualifiedClassName, scriptName, (StringUtils.isNotBlank(packageName) ? "and a package named " + packageName : ""));
274 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
275 }
276 }
277 }
278
279
280
281
282 public static final String resolveScriptNameFromSource(final String source) throws RepositoryException {
283 final List<ASTNode> nodes = new AstBuilder().buildFromString(CompilePhase.CONVERSION, source);
284 for (ASTNode astNode : nodes) {
285 if (astNode instanceof ClassNode) {
286 ClassNode cn = (ClassNode) astNode;
287 String name = cn.getNameWithoutPackage();
288 if (!name.startsWith("script")) {
289 return name;
290 }
291 }
292 }
293 return "untitledGroovyScript";
294 }
295 }