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