001    // Copyright 2010, 2011, 2012 The Apache Software Foundation
002    //
003    // Licensed under the Apache License, Version 2.0 (the "License");
004    // you may not use this file except in compliance with the License.
005    // You may obtain a copy of the License at
006    //
007    // http://www.apache.org/licenses/LICENSE-2.0
008    //
009    // Unless required by applicable law or agreed to in writing, software
010    // distributed under the License is distributed on an "AS IS" BASIS,
011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012    // See the License for the specific language governing permissions and
013    // limitations under the License.
014    
015    package org.apache.tapestry5.ioc.internal;
016    
017    import org.apache.tapestry5.internal.plastic.ClassLoaderDelegate;
018    import org.apache.tapestry5.internal.plastic.PlasticClassLoader;
019    import org.apache.tapestry5.internal.plastic.PlasticInternalUtils;
020    import org.apache.tapestry5.internal.plastic.asm.ClassReader;
021    import org.apache.tapestry5.internal.plastic.asm.ClassVisitor;
022    import org.apache.tapestry5.internal.plastic.asm.Opcodes;
023    import org.apache.tapestry5.ioc.Invokable;
024    import org.apache.tapestry5.ioc.ObjectCreator;
025    import org.apache.tapestry5.ioc.OperationTracker;
026    import org.apache.tapestry5.ioc.ReloadAware;
027    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
028    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
029    import org.apache.tapestry5.ioc.internal.util.URLChangeTracker;
030    import org.apache.tapestry5.ioc.services.PlasticProxyFactory;
031    import org.apache.tapestry5.services.UpdateListener;
032    import org.slf4j.Logger;
033    
034    import java.io.ByteArrayInputStream;
035    import java.io.ByteArrayOutputStream;
036    import java.io.IOException;
037    import java.io.InputStream;
038    import java.net.URL;
039    import java.util.Set;
040    
041    @SuppressWarnings("all")
042    public abstract class AbstractReloadableObjectCreator implements ObjectCreator, UpdateListener, ClassLoaderDelegate
043    {
044        private final ClassLoader baseClassLoader;
045    
046        private final String implementationClassName;
047    
048        private final Logger logger;
049    
050        private final OperationTracker tracker;
051    
052        private final URLChangeTracker changeTracker = new URLChangeTracker();
053    
054        private final PlasticProxyFactory proxyFactory;
055    
056        /**
057         * The set of class names that should be loaded by the class loader. This is necessary to support
058         * reloading the class when a base class changes, and to properly support access to protected methods.
059         */
060        private final Set<String> classesToLoad = CollectionFactory.newSet();
061    
062        private Object instance;
063    
064        private boolean firstTime = true;
065    
066        private PlasticClassLoader loader;
067    
068        protected AbstractReloadableObjectCreator(PlasticProxyFactory proxyFactory, ClassLoader baseClassLoader, String implementationClassName,
069                                                  Logger logger, OperationTracker tracker)
070        {
071            this.proxyFactory = proxyFactory;
072            this.baseClassLoader = baseClassLoader;
073            this.implementationClassName = implementationClassName;
074            this.logger = logger;
075            this.tracker = tracker;
076        }
077    
078        public synchronized void checkForUpdates()
079        {
080            if (instance == null || !changeTracker.containsChanges())
081            {
082                return;
083            }
084    
085            if (logger.isDebugEnabled())
086            {
087                logger.debug(String.format("Implementation class %s has changed and will be reloaded on next use.",
088                        implementationClassName));
089            }
090    
091            changeTracker.clear();
092    
093            loader = null;
094    
095            proxyFactory.clearCache();
096    
097            boolean reloadNow = informInstanceOfReload();
098    
099            instance = reloadNow ? createInstance() : null;
100        }
101    
102        private boolean informInstanceOfReload()
103        {
104            if (instance instanceof ReloadAware)
105            {
106                ReloadAware ra = (ReloadAware) instance;
107    
108                return ra.shutdownImplementationForReload();
109            }
110    
111            return false;
112        }
113    
114        public synchronized Object createObject()
115        {
116            if (instance == null)
117            {
118                instance = createInstance();
119            }
120    
121            return instance;
122        }
123    
124        private Object createInstance()
125        {
126            return tracker.invoke(String.format("Reloading class %s.", implementationClassName), new Invokable<Object>()
127            {
128                public Object invoke()
129                {
130                    Class reloadedClass = reloadImplementationClass();
131    
132                    return createInstance(reloadedClass);
133                }
134            });
135        }
136    
137        /**
138         * Invoked when an instance of the class is needed. It is the responsibility of this method (as implemented in a
139         * subclass) to instantiate the class and inject dependencies into the class.
140         *
141         * @see InternalUtils#findAutobuildConstructor(Class)
142         */
143        abstract protected Object createInstance(Class clazz);
144    
145        private Class reloadImplementationClass()
146        {
147            if (logger.isDebugEnabled())
148            {
149                logger.debug(String.format("%s class %s.", firstTime ? "Loading" : "Reloading", implementationClassName));
150            }
151    
152            loader = new PlasticClassLoader(baseClassLoader, this);
153    
154            classesToLoad.clear();
155    
156            add(implementationClassName);
157    
158            try
159            {
160                Class result = loader.loadClass(implementationClassName);
161    
162                firstTime = false;
163    
164                return result;
165            } catch (Throwable ex)
166            {
167                throw new RuntimeException(String.format("Unable to %s class %s: %s", firstTime ? "load" : "reload",
168                        implementationClassName, InternalUtils.toMessage(ex)), ex);
169            }
170        }
171    
172        private void add(String className)
173        {
174            if (!classesToLoad.contains(className))
175            {
176                logger.debug(String.format("Marking class %s to be (re-)loaded", className));
177    
178                classesToLoad.add(className);
179            }
180        }
181    
182        public boolean shouldInterceptClassLoading(String className)
183        {
184            return classesToLoad.contains(className);
185        }
186    
187        public Class<?> loadAndTransformClass(String className) throws ClassNotFoundException
188        {
189            logger.debug(String.format("BEGIN Analyzing %s", className));
190    
191            Class<?> result;
192    
193            try
194            {
195                result = doClassLoad(className);
196            } catch (IOException ex)
197            {
198                throw new ClassNotFoundException(String.format("Unable to analyze and load class %s: %s", className,
199                        InternalUtils.toMessage(ex)), ex);
200            }
201    
202            trackClassFileChanges(className);
203    
204            logger.debug(String.format("  END Analyzing %s", className));
205    
206            return result;
207        }
208    
209        public Class<?> doClassLoad(String className) throws IOException
210        {
211            ClassVisitor analyzer = new ClassVisitor(Opcodes.ASM4)
212            {
213                @Override
214                public void visit(int version, int access, String name, String signature, String superName, String[] interfaces)
215                {
216                    String path = superName + ".class";
217    
218                    URL url = baseClassLoader.getResource(path);
219    
220                    if (isFileURL(url))
221                    {
222                        add(PlasticInternalUtils.toClassName(superName));
223                    }
224                }
225    
226                @Override
227                public void visitInnerClass(String name, String outerName, String innerName, int access)
228                {
229                    // Anonymous inner classes show the outerName as null. Nested classes show the outer name as
230                    // the internal name of the containing class.
231                    if (outerName == null || classesToLoad.contains(PlasticInternalUtils.toClassName(outerName)))
232                    {
233                        add(PlasticInternalUtils.toClassName(name));
234                    }
235                }
236            };
237    
238    
239            String path = PlasticInternalUtils.toClassPath(className);
240    
241            InputStream stream = baseClassLoader.getResourceAsStream(path);
242    
243            assert stream != null;
244    
245            ByteArrayOutputStream classBuffer = new ByteArrayOutputStream(5000);
246            byte[] buffer = new byte[5000];
247    
248            while (true)
249            {
250                int length = stream.read(buffer);
251    
252                if (length < 0)
253                {
254                    break;
255                }
256    
257                classBuffer.write(buffer, 0, length);
258            }
259    
260            stream.close();
261    
262            byte[] bytecode = classBuffer.toByteArray();
263    
264            new ClassReader(new ByteArrayInputStream(bytecode)).accept(analyzer,
265                    ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
266    
267    
268            return loader.defineClassWithBytecode(className, bytecode);
269        }
270    
271        private void trackClassFileChanges(String className)
272        {
273            if (isInnerClassName(className))
274            {
275                return;
276            }
277    
278            String path = PlasticInternalUtils.toClassPath(className);
279    
280            URL url = baseClassLoader.getResource(path);
281    
282            if (isFileURL(url))
283            {
284                changeTracker.add(url);
285            }
286        }
287    
288        /**
289         * Returns true if the url is non-null, and is for the "file:" protocol.
290         */
291        private boolean isFileURL(URL url)
292        {
293            return url != null && url.getProtocol().equals("file");
294        }
295    
296        private boolean isInnerClassName(String className)
297        {
298            return className.indexOf('$') >= 0;
299        }
300    }