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