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.protocol.http;
018
019import java.net.URL;
020
021import jakarta.servlet.FilterConfig;
022import jakarta.servlet.ServletException;
023
024import org.apache.wicket.Session;
025import org.apache.wicket.application.ReloadingClassLoader;
026import org.apache.wicket.util.listener.IChangeListener;
027
028
029/**
030 * Custom {@link WicketFilter} that reloads the web applications when classes are modified. In order
031 * to monitor changes to your own classes, subclass {@link ReloadingWicketFilter} and use include
032 * and exclude patterns using wildcards. And in web.xml, point to your custom
033 * {@link ReloadingWicketFilter} instead of the original {@link WicketFilter}.
034 * 
035 * <p>
036 * The built-in patterns are:
037 * </p>
038 * 
039 * <pre>
040 * ReloadingClassLoader.excludePattern(&quot;org.apache.wicket.*&quot;);
041 * ReloadingClassLoader.includePattern(&quot;org.apache.wicket.examples.*&quot;);
042 * </pre>
043 * 
044 * <p>
045 * <b>Example. </b> Defining custom patterns
046 * </p>
047 * 
048 * <pre>
049 * public class MyReloadingFilter extends ReloadingWicketFilter
050 * {
051 *      static
052 *      {
053 *              ReloadingClassLoader.includePattern(&quot;com.company.*&quot;);
054 *              ReloadingClassLoader.excludePattern(&quot;com.company.spring.beans.*&quot;);
055 *              ReloadingClassLoader.includePattern(&quot;some.other.package.with.wicket.components.*&quot;);
056 *      }
057 * }
058 * </pre>
059 * 
060 * <p>
061 * <b>NOTE</b> If you wish to reload <em>com.company.search.Form</em>, you have to make sure to
062 * include all classes that <b>reference</b> <em>com.company.search.Form</em>. In particular, if it
063 * is referenced in com.company.Application, you will also have to include the latter. And this is
064 * viral, as for every class you include, you must check that all classes that reference it are also
065 * included.
066 * </p>
067 * 
068 * <p>
069 * It is also possible to add an extra URL to watch for changes using
070 * <em>ReloadingClassLoader.addLocation()</em> . By default, all the URLs returned by the parent
071 * class loader (ie all {@link URL} returned by {@link ClassLoader#getResources(String)} with empty
072 * {@link String}) are registered.
073 * </p>
074 * <hr>
075 * <p>
076 * <b>Important. </b> It can be quite tricky to setup the reloading mechanism correctly. Here are
077 * the general guidelines:
078 * </p>
079 * <ul>
080 * <li>The order of include and exclude patterns is significant, the patterns are processed
081 * sequentially in the order they are defined</li>
082 * <li>Don't forget that inner classes are named after YourClass$1, so take that into account when
083 * setting up the patterns, eg include <tt>YourClass*</tt>, not just <tt>YourClass</tt></li>
084 * <li>To enable back-button support for the reloading mechanism, be sure to put
085 * <em>Objects.setObjectStreamFactory(new WicketObjectStreamFactory());</em> in your application's
086 * {@link WebApplication#init()} method. <b>Native JDK object serialization will break the reloading
087 * mechanism when navigating in the browser's history.</b></li>
088 * <li>It is advisable to <b>exclude</b> subclasses of {@link Session} from the the reloading
089 * classloader, because this is the only object that remains across application restarts</li>
090 * <li>Last but not least, make sure to clear your session cookie before reloading the application
091 * home page (or any other bookmarkable page) to get rid of old pages stored in session</li>
092 * </ul>
093 * 
094 * <p>
095 * <b>Be sure to carefully read the following information if you also use Spring:</b>
096 * </p>
097 * 
098 * <p>
099 * When using org.apache.wicket.spring.SpringWebApplicationFactory, the application must be a bean
100 * with "prototype" scope and the "applicationBean" init parameter must be explicitly set, otherwise
101 * the reloading mechanism won't be able to recreate the application.
102 * </p>
103 * 
104 * <p>
105 * <b>WARNING. </b> Be careful that when using Spring or other component managers, you will get
106 * <em>ClassCastException</em> if a given class is loaded two times, one time by the normal
107 * classloader, and another time by the reloading classloader. You need to ensure that your Spring
108 * beans are properly excluded from the reloading class loader and that only the Wicket components
109 * are included. When getting a cryptic error with regard to class loading, class instantiation or
110 * class comparison, first <b>disable the reloading class loader</b> to rule out the possibility of
111 * a classloader conflict. Please keep in mind that two classes are equal if and only if they have
112 * the same name <b>and are loaded in the same classloader</b>. Same goes for errors like
113 * <em>LinkageError: Class FooBar violates loader constraints</em>, better be safe and disable the
114 * reloading feature.
115 * </p>
116 * 
117 * @see WicketFilter
118 * 
119 * @author <a href="mailto:jbq@apache.org">Jean-Baptiste Quenot</a>
120 */
121public class ReloadingWicketFilter extends WicketFilter
122{
123        private ReloadingClassLoader reloadingClassLoader;
124
125        /**
126         * Instantiate the reloading class loader
127         */
128        public ReloadingWicketFilter()
129        {
130                // Create a reloading classloader
131                reloadingClassLoader = new ReloadingClassLoader(getClass().getClassLoader());
132        }
133
134        /**
135         * @see org.apache.wicket.protocol.http.WicketFilter#getClassLoader()
136         */
137        @Override
138        protected ClassLoader getClassLoader()
139        {
140                return reloadingClassLoader;
141        }
142
143        /**
144         * @see org.apache.wicket.protocol.http.WicketFilter#init(boolean, jakarta.servlet.FilterConfig)
145         */
146        @Override
147        public void init(final boolean isServlet, final FilterConfig filterConfig)
148                throws ServletException
149        {
150                reloadingClassLoader.setListener(new IChangeListener<Class<?>>()
151                {
152                        @Override
153                        public void onChange(Class<?> clz)
154                        {
155                                destroy();
156
157                                // Remove the ModificationWatcher from the current reloading class loader
158                                reloadingClassLoader.destroy();
159
160                                /*
161                                 * Create a new classloader, as there is no way to clear a ClassLoader's cache. This
162                                 * supposes that we don't share objects across application instances, this is almost
163                                 * true, except for Wicket's Session object.
164                                 */
165                                reloadingClassLoader = new ReloadingClassLoader(getClass().getClassLoader());
166                                try
167                                {
168                                        init(filterConfig);
169                                }
170                                catch (ServletException e)
171                                {
172                                        throw new RuntimeException(e);
173                                }
174                        }
175                });
176
177                super.init(isServlet, filterConfig);
178        }
179}