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 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.module.groovy.support.");
162 }
163 }
164
165
166
167
168
169
170
171
172
173
174
175 private static final class PackageAndClassNameConsistencyOperation extends SourceUnitOperation {
176 private HierarchyManagerProvider hmp;
177 private String mgnlPath;
178 public PackageAndClassNameConsistencyOperation(HierarchyManagerProvider hmp, String mgnlPath) {
179 this.hmp = hmp;
180 this.mgnlPath = mgnlPath;
181 }
182 public void call(SourceUnit source) throws CompilationFailedException {
183 final String parentPath = StringUtils.defaultIfEmpty(StringUtils.substringBeforeLast(mgnlPath, "/"), "/");
184 final String scriptName = StringUtils.substringAfterLast(mgnlPath, "/");
185 final boolean isParentRoot = "/".equals(parentPath);
186 String packageName = null;
187
188 log.debug("parent path is {}, script name is {}, isParentRoot? {}", new Object[]{parentPath, scriptName, isParentRoot});
189
190
191 if (!isParentRoot) {
192 if(!source.getAST().hasPackageName()){
193 String msg = scriptName + " compilation failed: you must specify a package for your class.";
194 log.warn(msg);
195 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
196 }
197 packageName = source.getAST().getPackageName();
198 final String path = "/" + packageName.replace('.', '/');
199 if (!path.equals(parentPath + "/")) {
200 String msg = scriptName
201 + " compilation failed: class package '"
202 + packageName
203 + "' does not match parent node '"
204 + parentPath
205 + "' path in the scripts repository";
206 log.warn(msg);
207 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
208 }
209
210 final HierarchyManager hm = hmp.getHierarchyManager(SCRIPTS);
211 if (!hm.isExist(path)) {
212 String msg = scriptName
213 + " compilation failed: class package '"
214 + packageName
215 + "' does not match an existing '"
216 + path
217 + "' path in the scripts repository";
218 log.warn(msg);
219 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
220 }
221 }
222
223 boolean match = false;
224 final String fullyQualifiedClassName = packageName != null ? packageName + scriptName: scriptName;
225 for(ClassNode cn : source.getAST().getClasses()){
226 if(fullyQualifiedClassName.equals(cn.getName())){
227 log.debug("found a matching class name {}, we can proceed", fullyQualifiedClassName);
228 match = true;
229 break;
230 }
231 }
232 if(!match){
233 String msg = fullyQualifiedClassName + " should declare at least one class named " + scriptName;
234 log.warn(msg);
235 throw new CompilationFailedException(source.getPhase(), source, new Throwable(msg));
236 }
237 }
238 }
239 }