001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.application; 018 019import java.io.IOException; 020import java.net.URL; 021import java.net.URLClassLoader; 022import java.util.ArrayList; 023import java.util.Enumeration; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Set; 027import java.util.TreeSet; 028 029import org.apache.wicket.util.collections.UrlExternalFormComparator; 030import org.apache.wicket.util.file.File; 031import org.apache.wicket.util.listener.IChangeListener; 032import java.time.Duration; 033import org.apache.wicket.util.watch.IModifiable; 034import org.apache.wicket.util.watch.IModificationWatcher; 035import org.apache.wicket.util.watch.ModificationWatcher; 036import org.slf4j.Logger; 037import org.slf4j.LoggerFactory; 038 039 040/** 041 * Custom ClassLoader that reverses the classloader lookups, and that is able to notify a listener 042 * when a class file is changed. 043 * 044 * @author <a href="mailto:jbq@apache.org">Jean-Baptiste Quenot</a> 045 */ 046public class ReloadingClassLoader extends URLClassLoader 047{ 048 private static final Logger log = LoggerFactory.getLogger(ReloadingClassLoader.class); 049 050 private static final Set<URL> urls = new TreeSet<>(new UrlExternalFormComparator()); 051 052 private static final List<String> patterns = new ArrayList<>(); 053 054 private IChangeListener<Class<?>> listener; 055 056 private final IModificationWatcher watcher; 057 058 static 059 { 060 addClassLoaderUrls(ReloadingClassLoader.class.getClassLoader()); 061 excludePattern("org.apache.wicket.*"); 062 includePattern("org.apache.wicket.examples.*"); 063 } 064 065 /** 066 * 067 * @param name 068 * @return true if class if found, false otherwise 069 */ 070 protected boolean tryClassHere(String name) 071 { 072 // don't include classes in the java or javax.servlet package 073 if (name != null && (name.startsWith("java.") || name.startsWith("jakarta.servlet"))) 074 { 075 return false; 076 } 077 // Scan includes, then excludes 078 boolean tryHere; 079 080 // If no explicit includes, try here 081 if (patterns == null || patterns.size() == 0) 082 { 083 tryHere = true; 084 } 085 else 086 { 087 // See if it matches include patterns 088 tryHere = false; 089 for (String rawpattern : patterns) 090 { 091 if (rawpattern.length() <= 1) 092 { 093 continue; 094 } 095 // FIXME it seems that only "includes" are handled. "Excludes" are ignored 096 boolean isInclude = rawpattern.substring(0, 1).equals("+"); 097 String pattern = rawpattern.substring(1); 098 if (WildcardMatcherHelper.match(pattern, name) != null) 099 { 100 tryHere = isInclude; 101 } 102 } 103 } 104 105 return tryHere; 106 } 107 108 /** 109 * Include a pattern 110 * 111 * @param pattern 112 * the pattern to include 113 */ 114 public static void includePattern(String pattern) 115 { 116 patterns.add("+" + pattern); 117 } 118 119 /** 120 * Exclude a pattern 121 * 122 * @param pattern 123 * the pattern to exclude 124 */ 125 public static void excludePattern(String pattern) 126 { 127 patterns.add("-" + pattern); 128 } 129 130 /** 131 * Returns the list of all configured inclusion or exclusion patterns 132 * 133 * @return list of patterns as String 134 */ 135 public static List<String> getPatterns() 136 { 137 return patterns; 138 } 139 140 /** 141 * Add the location of a directory containing class files 142 * 143 * @param url 144 * the URL for the directory 145 */ 146 public static void addLocation(URL url) 147 { 148 urls.add(url); 149 } 150 151 /** 152 * Returns the list of all configured locations of directories containing class files 153 * 154 * @return list of locations as URL 155 */ 156 public static Set<URL> getLocations() 157 { 158 return urls; 159 } 160 161 /** 162 * Add all the url locations we can find for the provided class loader 163 * 164 * @param loader 165 * class loader 166 */ 167 private static void addClassLoaderUrls(ClassLoader loader) 168 { 169 if (loader != null) 170 { 171 final Enumeration<URL> resources; 172 try 173 { 174 resources = loader.getResources(""); 175 } 176 catch (IOException e) 177 { 178 throw new RuntimeException(e); 179 } 180 while (resources.hasMoreElements()) 181 { 182 URL location = resources.nextElement(); 183 ReloadingClassLoader.addLocation(location); 184 } 185 } 186 } 187 188 /** 189 * Create a new reloading ClassLoader from a list of URLs, and initialize the 190 * ModificationWatcher to detect class file modifications 191 * 192 * @param parent 193 * the parent classloader in case the class file cannot be loaded from the above 194 * locations 195 */ 196 public ReloadingClassLoader(ClassLoader parent) 197 { 198 super(new URL[] { }, parent); 199 // probably doubles from this class, but just in case 200 addClassLoaderUrls(parent); 201 202 for (URL url : urls) 203 { 204 addURL(url); 205 } 206 Duration pollFrequency = Duration.ofSeconds(3); 207 watcher = new ModificationWatcher(pollFrequency); 208 } 209 210 /** 211 * Gets a resource from this <code>ClassLoader</class>. If the 212 * resource does not exist in this one, we check the parent. 213 * Please note that this is the exact opposite of the 214 * <code>ClassLoader</code> spec. We use it to work around inconsistent class loaders from third 215 * party vendors. 216 * 217 * @param name 218 * of resource 219 */ 220 @Override 221 public final URL getResource(final String name) 222 { 223 URL resource = findResource(name); 224 ClassLoader parent = getParent(); 225 if (resource == null && parent != null) 226 { 227 resource = parent.getResource(name); 228 } 229 230 return resource; 231 } 232 233 /** 234 * Loads the class from this <code>ClassLoader</class>. If the 235 * class does not exist in this one, we check the parent. Please 236 * note that this is the exact opposite of the 237 * <code>ClassLoader</code> spec. We use it to load the class from the same classloader as 238 * WicketFilter or WicketServlet. When found, the class file is watched for modifications. 239 * 240 * @param name 241 * the name of the class 242 * @param resolve 243 * if <code>true</code> then resolve the class 244 * @return the resulting <code>Class</code> object 245 * @exception ClassNotFoundException 246 * if the class could not be found 247 */ 248 @Override 249 public final Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException 250 { 251 // First check if it's already loaded 252 Class<?> clazz = findLoadedClass(name); 253 254 if (clazz == null) 255 { 256 final ClassLoader parent = getParent(); 257 258 if (tryClassHere(name)) 259 { 260 try 261 { 262 clazz = findClass(name); 263 watchForModifications(clazz); 264 } 265 catch (ClassNotFoundException cnfe) 266 { 267 if (parent == null) 268 { 269 // Propagate exception 270 throw cnfe; 271 } 272 } 273 } 274 275 if (clazz == null) 276 { 277 if (parent == null) 278 { 279 throw new ClassNotFoundException(name); 280 } 281 else 282 { 283 // Will throw a CFNE if not found in parent 284 // see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6500212 285 // clazz = parent.loadClass(name); 286 clazz = Class.forName(name, false, parent); 287 } 288 } 289 } 290 291 if (resolve) 292 { 293 resolveClass(clazz); 294 } 295 296 return clazz; 297 } 298 299 /** 300 * Sets the listener that will be notified when a class changes 301 * 302 * @param listener 303 * the listener to notify upon class change 304 */ 305 public void setListener(IChangeListener<Class<?>> listener) 306 { 307 this.listener = listener; 308 } 309 310 /** 311 * Watch changes of a class file by locating it in the list of location URLs and adding the 312 * corresponding file to the ModificationWatcher 313 * 314 * @param clz 315 * the class to watch 316 */ 317 private void watchForModifications(final Class<?> clz) 318 { 319 // Watch class in the future 320 Iterator<URL> locationsIterator = urls.iterator(); 321 File clzFile = null; 322 while (locationsIterator.hasNext()) 323 { 324 // FIXME only works for directories, but JARs etc could be checked 325 // as well 326 URL location = locationsIterator.next(); 327 String clzLocation = location.getFile() + clz.getName().replaceAll("\\.", "/") + 328 ".class"; 329 log.debug("clzLocation=" + clzLocation); 330 clzFile = new File(clzLocation); 331 final File finalClzFile = clzFile; 332 if (clzFile.exists()) 333 { 334 log.info("Watching changes of class " + clzFile); 335 watcher.add(clzFile, new IChangeListener<IModifiable>() 336 { 337 @Override 338 public void onChange(IModifiable modifiable) 339 { 340 log.info("Class file " + finalClzFile + " has changed, reloading"); 341 try 342 { 343 listener.onChange(clz); 344 } 345 catch (Exception e) 346 { 347 log.error("Could not notify listener", e); 348 // If an error occurs when the listener is notified, 349 // remove the watched object to avoid rethrowing the 350 // exception at next check 351 // FIXME check if class file has been deleted 352 watcher.remove(finalClzFile); 353 } 354 } 355 }); 356 break; 357 } 358 else 359 { 360 log.debug("Class file does not exist: " + clzFile); 361 } 362 } 363 if (clzFile != null && !clzFile.exists()) 364 { 365 log.debug("Could not locate class " + clz.getName()); 366 } 367 } 368 369 /** 370 * Remove the ModificationWatcher from the current reloading class loader 371 */ 372 public void destroy() 373 { 374 watcher.destroy(); 375 } 376}