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
015package org.apache.tapestry5.ioc.internal;
016
017import org.apache.tapestry5.internal.plastic.ClassLoaderDelegate;
018import org.apache.tapestry5.internal.plastic.PlasticClassLoader;
019import org.apache.tapestry5.internal.plastic.PlasticInternalUtils;
020import org.apache.tapestry5.internal.plastic.asm.ClassReader;
021import org.apache.tapestry5.internal.plastic.asm.ClassVisitor;
022import org.apache.tapestry5.internal.plastic.asm.Opcodes;
023import org.apache.tapestry5.ioc.Invokable;
024import org.apache.tapestry5.ioc.ObjectCreator;
025import org.apache.tapestry5.ioc.OperationTracker;
026import org.apache.tapestry5.ioc.ReloadAware;
027import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
028import org.apache.tapestry5.ioc.internal.util.InternalUtils;
029import org.apache.tapestry5.ioc.internal.util.URLChangeTracker;
030import org.apache.tapestry5.ioc.services.PlasticProxyFactory;
031import org.apache.tapestry5.services.UpdateListener;
032import org.slf4j.Logger;
033
034import java.io.ByteArrayInputStream;
035import java.io.ByteArrayOutputStream;
036import java.io.IOException;
037import java.io.InputStream;
038import java.net.URL;
039import java.util.Set;
040
041@SuppressWarnings("all")
042public 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}