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 import info.magnolia.cms.core.HierarchyManager;
38 import info.magnolia.module.groovy.support.HierarchyManagerProvider;
39
40 import java.io.IOException;
41 import java.net.URL;
42 import java.net.URLConnection;
43 import java.security.CodeSource;
44
45 import org.apache.commons.lang.StringUtils;
46 import org.codehaus.groovy.ast.ClassNode;
47 import org.codehaus.groovy.control.CompilationFailedException;
48 import org.codehaus.groovy.control.CompilationUnit;
49 import org.codehaus.groovy.control.CompilerConfiguration;
50 import org.codehaus.groovy.control.Phases;
51 import org.codehaus.groovy.control.SourceUnit;
52 import org.codehaus.groovy.control.CompilationUnit.SourceUnitOperation;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56
57
58
59
60
61
62
63
64
65
66 public final class MgnlGroovyClassLoader extends GroovyClassLoader {
67 private static final Logger log = LoggerFactory.getLogger(MgnlGroovyClassLoader.class);
68
69 private static final String SCRIPTS = "scripts";
70
71 private final HierarchyManagerProvider hmp;
72
73 private boolean compileTimeChecks;
74
75 private String mgnlPath;
76
77
78
79
80 public MgnlGroovyClassLoader(HierarchyManagerProvider hmp) {
81 if (hmp == null) {
82 throw new IllegalArgumentException("HierarchyManagerProvider can not be null");
83 }
84 this.hmp = hmp;
85 this.setShouldRecompile(true);
86 this.setResourceLoader(new MgnlGroovyResourceLoader(super.getResourceLoader(), hmp, SCRIPTS));
87 }
88
89 @Override
90 protected CompilationUnit createCompilationUnit(CompilerConfiguration config, CodeSource codeSource) {
91 CompilationUnit cu = super.createCompilationUnit(config, codeSource);
92 if(compileTimeChecks){
93 log.debug("enforcing compile-time checks...");
94
95
96
97
98
99
100 cu.addPhaseOperation(new PackageAndClassNameConsistencyOperation(hmp, mgnlPath), Phases.CONVERSION);
101 }
102
103 cu.addPhaseOperation(new AddDefaultImportOperation(), Phases.CONVERSION);
104 return cu;
105 }
106
107
108
109
110
111
112
113
114
115 @Override
116 protected boolean isSourceNewer(URL source, Class cls) throws IOException {
117 long lastMod = -1;
118 if ("file".equals(source.getProtocol())) {
119 return true;
120 } else {
121 URLConnection conn = source.openConnection();
122 lastMod = conn.getLastModified();
123 conn.getInputStream().close();
124 }
125 boolean isNewer = lastMod > getTimeStamp(cls);
126 if(isNewer){
127 log.info("{} source has changed", cls.getName());
128 }
129 return isNewer;
130 }
131
132
133
134
135
136
137
138
139
140
141
142
143 public final void verify(final String source, final boolean enforceCompileChecks, final String mgnlPath) throws CompilationFailedException{
144 this.compileTimeChecks = enforceCompileChecks;
145 this.mgnlPath = mgnlPath;
146 if(compileTimeChecks && mgnlPath == null) {
147 throw new IllegalArgumentException("When compilation checks are enforced, mgnlPath cannot be null");
148 }
149 parseClass(source);
150 }
151
152 private static final class AddDefaultImportOperation extends SourceUnitOperation {
153
154 @Override
155 public void call(SourceUnit source) throws CompilationFailedException {
156
157
158 log.debug("adding default imports...");
159 source.getAST().addStarImport("info.magnolia.context.");
160 source.getAST().addStarImport("info.magnolia.cms.core.");
161 source.getAST().addStarImport("info.magnolia.cms.util.");
162 source.getAST().addStarImport("info.magnolia.jcr.util.");
163 source.getAST().addStarImport("info.magnolia.module.groovy.support.");
164 }
165 }
166
167
168
169
170
171
172
173
174
175
176
177 private static final class PackageAndClassNameConsistencyOperation extends SourceUnitOperation {
178 private HierarchyManagerProvider hmp;
179 private String mgnlPath;
180 public PackageAndClassNameConsistencyOperation(HierarchyManagerProvider hmp, String mgnlPath) {
181 this.hmp = hmp;
182 this.mgnlPath = mgnlPath;
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 boolean match = false;
227 final String fullyQualifiedClassName = packageName != null ? packageName + scriptName: scriptName;
228 for(ClassNode cn : source.getAST().getClasses()){
229 if(fullyQualifiedClassName.equals(cn.getName())){
230 log.debug("found a matching class name {}, we can proceed", fullyQualifiedClassName);
231 match = true;
232 break;
233 }
234 }
235 if(!match){
236 String msg = fullyQualifiedClassName + " should declare at least one class named " + scriptName;
237 log.warn(msg);
238 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
239 }
240 }
241 }
242 }