1 /*
2 Copyright (C) Simuline Inc, Ernst Rei3ner
3
4 This program is free software; you can redistribute it and/or
5 modify it under the terms of the GNU General Public License
6 as published by the Free Software Foundation; either version 2
7 of the License, or (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the
16 Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor,
18 Boston, MA 02110-1301, USA.
19 */
20
21 package eu.simuline.testhelpers;
22
23 import eu.simuline.util.JavaPath;
24
25 import java.util.List;
26 import java.util.ArrayList;
27 import java.util.Properties;
28 import java.util.Enumeration;
29
30 import java.io.InputStream;
31 import java.io.ByteArrayOutputStream;
32 import java.io.IOException;
33
34 import java.net.URL;
35
36
37 /**
38 * A custom class loader which allows to reload classes for each test run.
39 * The class loader can be configured with a list of package paths
40 * that should be excluded from loading.
41 * The loading of these packages is delegated to the system class loader.
42 * They will be shared across test runs.
43 * <p>
44 * The list of excluded package paths
45 * is either hardcoded in {@link #defaultExclusions},
46 * specified as property with key {@link #PROP_KEY_NO_CLS_RELOAD}.
47 * in a properties file {@link #EXCLUDED_FILE}
48 * that is located in the same place as the TestCaseClassLoader class.
49 * <p>
50 * <b>Known limitation:</b>
51 * <ul>
52 * <li>
53 * the TestCaseClassLoader cannot load classes from jar files.
54 * <li>
55 * service providers must be excluded from reloading.
56 * </ul>
57 *
58 * @author <a href="mailto:ernst.reissner@simuline.eu">Ernst Reissner</a>
59 * @version 1.0
60 */
61 public final class TestCaseClassLoader extends ClassLoader {
62
63 /* -------------------------------------------------------------------- *
64 * class constants. *
65 * -------------------------------------------------------------------- */
66
67 /**
68 * Key of system property
69 * containing a ":"-separated list of packages or classes
70 * to be excluded from reloading.
71 * Each is read into {@link #excluded}.
72 */
73 private static final String PROP_KEY_NO_CLS_RELOAD = "noClsReload";
74
75 /**
76 * Name of excluded properties file.
77 * This shall be located in the same folder as this class loader.
78 * The property keys shall have the form <code>exlude.xxx</code>
79 * and are read in {@link #excluded}.
80 */
81 static final String EXCLUDED_FILE = "noClsReload.properties";
82
83 /**
84 * The initial length of a stream to read class files from.
85 */
86 private static final int LEN_CLS_STREAM = 1000;
87
88 /* -------------------------------------------------------------------- *
89 * fields. *
90 * -------------------------------------------------------------------- */
91
92 /**
93 * The scanned class path.
94 */
95 private JavaPath jPath;
96
97 /**
98 * Holds the excluded paths.
99 * This is initialized by {@link #readExcludedPackages}
100 * and used by {@link #isExcluded}.
101 */
102 private List<String> excluded;
103
104 /**
105 * Default excluded paths.
106 * @see #isExcluded
107 */
108 private final String[] defaultExclusions = {
109 "junit.",
110 "org.",
111 "java.",
112 "javax.",
113 "com.",
114 "sun."
115 };
116
117 /* -------------------------------------------------------------------- *
118 * constructor. *
119 * -------------------------------------------------------------------- */
120
121 /**
122 * Constructs a TestCaseLoader.
123 * It scans the class path and the excluded package paths.
124 */
125 public TestCaseClassLoader() {
126 this(System.getProperty("java.class.path"));
127 }
128
129 /**
130 * Constructs a TestCaseLoader.
131 * It scans the class path and the excluded package paths.
132 *
133 * @param classPath
134 * the classpath.
135 */
136 private TestCaseClassLoader(String classPath) {
137 this.jPath = new JavaPath(classPath);
138 readExcludedPackages();
139 }
140
141 /* -------------------------------------------------------------------- *
142 * methods. *
143 * -------------------------------------------------------------------- */
144
145 public URL getResource(String name) {
146 return ClassLoader.getSystemResource(name);
147 }
148
149 public InputStream getResourceAsStream(String name) {
150 return ClassLoader.getSystemResourceAsStream(name);
151 }
152
153 /**
154 * Returns whether the name with the given name
155 * is excluded from being loaded.
156 *
157 *
158 * @param name
159 * the fully qualified name of a class as a <code>String</code>.
160 * @return
161 * a <code>boolean</code> which shows whether <code>name</code>
162 * starts with one of the prefixes given by {@link #excluded}.
163 * In this case the corresponding class is not loaded.
164 */
165 private boolean isExcluded(String name) {
166 for (int i = 0; i < this.excluded.size(); i++) {
167 if (name.startsWith(this.excluded.get(i))) {
168 return true;
169 }
170 }
171 return false;
172 }
173
174 Class<?> defineClass(String name)
175 throws ClassNotFoundException {
176 byte[] data = lookupClassData(name);
177 return defineClass(name, data, 0, data.length);
178 }
179
180 public synchronized Class<?> loadClass(String name, boolean resolve)
181 throws ClassNotFoundException {
182
183 Class<?> cls = findLoadedClass(name);
184 if (cls != null) {
185 return cls;
186 }
187 //
188 // Delegate the loading of excluded classes to the
189 // standard class loader.
190 //
191 if (isExcluded(name)) {
192 try {
193 cls = findSystemClass(name);
194 return cls;
195 } catch (ClassNotFoundException e) { // NOPMD
196 System.out.println("keep searching **** although excluded. ");
197 // keep searching **** although excluded.
198 }
199 }
200 byte[] data = lookupClassData(name);
201 if (data == null) {
202 throw new ClassNotFoundException();
203 }
204 cls = defineClass(name, data, 0, data.length);
205 //System.out.println("loaded: "+name);
206 if (resolve) {
207 resolveClass(cls);
208 }
209 return cls;
210 }
211
212 private byte[] lookupClassData(String className)
213 throws ClassNotFoundException {
214 try {
215 InputStream stream = this.jPath.getInputStream(className);
216 if (stream == null) {
217 throw new ClassNotFoundException();
218 }
219
220 ByteArrayOutputStream out =
221 new ByteArrayOutputStream(LEN_CLS_STREAM);
222 byte[] data = new byte[LEN_CLS_STREAM];
223 int numRead;
224 while ((numRead = stream.read(data)) != -1) {
225 out.write(data, 0, numRead);
226 }
227 stream.close();
228 out.close();
229 return out.toByteArray();
230 } catch (IOException e) {
231 throw new ClassNotFoundException(); // NOPMD
232 }
233 }
234
235 /**
236 * Initializes {@link #excluded} using {@link #defaultExclusions},
237 * {@link #PROP_KEY_NO_CLS_RELOAD} and {@link #EXCLUDED_FILE}.
238 * <ol>
239 * <li>
240 * First the entries of {@link #defaultExclusions}
241 * are added to {@link #excluded}.
242 * <li>
243 * Then {@link #PROP_KEY_NO_CLS_RELOAD} is interpreted
244 * as a ":"-seprated list of entries
245 * each of which is added to {@link #excluded}.
246 * <li>
247 * Finally, {@link #EXCLUDED_FILE} is interpreted as filename
248 * and the file is interpreted as Properties File with property keys
249 * starting with <code>excluded.</code>;
250 * all the other properties are ignored.
251 * The values are trimmed
252 * and a trailing <code>*</code> is removed if present.
253 * If the remaining path is nontrivial, it is added to {@link #excluded}.
254 * </ol>
255 */
256 @SuppressWarnings("PMD.NPathComplexity")
257 private void readExcludedPackages() {
258 this.excluded = new ArrayList<String>();
259 for (int i = 0; i < this.defaultExclusions.length; i++) {
260 this.excluded.add(defaultExclusions[i]);
261 }
262 Properties prop;
263
264 String excludesPathProp = System.getProperty(PROP_KEY_NO_CLS_RELOAD);
265 if (excludesPathProp != null) {
266 String[] excludesProp = excludesPathProp.split(":");
267 for (int i = 0; i < excludesProp.length; i++) {
268 this.excluded.add(excludesProp[i]);
269 }
270 }
271
272 InputStream inStream = getClass().getResourceAsStream(EXCLUDED_FILE);
273 System.out.println("URL" + getClass().getResource(EXCLUDED_FILE));
274 if (inStream == null) {
275 return;
276 }
277 //assert false;
278 prop = new Properties();
279 try {
280 prop.load(inStream);
281 } catch (IOException e) {
282 return;
283 } finally {
284 try {
285 inStream.close();
286 } catch (IOException e) { // NOPMD
287 // already closed *****?
288 }
289 }
290
291 for (Enumeration<?> e = prop.propertyNames();
292 e.hasMoreElements();) {
293 String key = (String) e.nextElement();
294 if (key.startsWith("excluded.")) {
295 String path = prop.getProperty(key);
296 path = path.trim();
297 if (path.endsWith("*")) {
298 path = path.substring(0, path.length() - 1);
299 }
300
301 if (path.length() > 0) {
302 this.excluded.add(path);
303 }
304 } // if
305 } // for
306 } // readExcludedPackages()
307 }