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