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