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("javax.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}