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