View Javadoc
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 }