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.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.ioc.util.ExceptionUtils;
032import org.apache.tapestry5.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    public synchronized void checkForUpdates()
080    {
081        if (instance == null || !changeTracker.containsChanges())
082        {
083            return;
084        }
085
086        if (logger.isDebugEnabled())
087        {
088            logger.debug(String.format("Implementation class %s has changed and will be reloaded on next use.",
089                    implementationClassName));
090        }
091
092        changeTracker.clear();
093
094        loader = null;
095
096        proxyFactory.clearCache();
097
098        boolean reloadNow = informInstanceOfReload();
099
100        instance = reloadNow ? createInstance() : null;
101    }
102
103    private boolean informInstanceOfReload()
104    {
105        if (instance instanceof ReloadAware)
106        {
107            ReloadAware ra = (ReloadAware) instance;
108
109            return ra.shutdownImplementationForReload();
110        }
111
112        return false;
113    }
114
115    public synchronized Object createObject()
116    {
117        if (instance == null)
118        {
119            instance = createInstance();
120        }
121
122        return instance;
123    }
124
125    private Object createInstance()
126    {
127        return tracker.invoke(String.format("Reloading class %s.", implementationClassName), new Invokable<Object>()
128        {
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(String.format("%s class %s.", 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(String.format("Marking class %s to be (re-)loaded", className));
178
179            classesToLoad.add(className);
180        }
181    }
182
183    public boolean shouldInterceptClassLoading(String className)
184    {
185        return classesToLoad.contains(className);
186    }
187
188    public Class<?> loadAndTransformClass(String className) throws ClassNotFoundException
189    {
190        logger.debug(String.format("BEGIN Analyzing %s", className));
191
192        Class<?> result;
193
194        try
195        {
196            result = doClassLoad(className);
197        } catch (IOException ex)
198        {
199            throw new ClassNotFoundException(String.format("Unable to analyze and load class %s: %s", className,
200                    ExceptionUtils.toMessage(ex)), ex);
201        }
202
203        trackClassFileChanges(className);
204
205        logger.debug(String.format("  END Analyzing %s", className));
206
207        return result;
208    }
209
210    public Class<?> doClassLoad(String className) throws IOException
211    {
212        ClassVisitor analyzer = new ClassVisitor(Opcodes.ASM4)
213        {
214            @Override
215            public void visit(int version, int access, String name, String signature, String superName, String[] interfaces)
216            {
217                String path = superName + ".class";
218
219                URL url = baseClassLoader.getResource(path);
220
221                if (isFileURL(url))
222                {
223                    add(PlasticInternalUtils.toClassName(superName));
224                }
225            }
226
227            @Override
228            public void visitInnerClass(String name, String outerName, String innerName, int access)
229            {
230                // Anonymous inner classes show the outerName as null. Nested classes show the outer name as
231                // the internal name of the containing class.
232                if (outerName == null || classesToLoad.contains(PlasticInternalUtils.toClassName(outerName)))
233                {
234                    add(PlasticInternalUtils.toClassName(name));
235                }
236            }
237        };
238
239
240        String path = PlasticInternalUtils.toClassPath(className);
241
242        InputStream stream = baseClassLoader.getResourceAsStream(path);
243
244        assert stream != null;
245
246        ByteArrayOutputStream classBuffer = new ByteArrayOutputStream(5000);
247        byte[] buffer = new byte[5000];
248
249        while (true)
250        {
251            int length = stream.read(buffer);
252
253            if (length < 0)
254            {
255                break;
256            }
257
258            classBuffer.write(buffer, 0, length);
259        }
260
261        stream.close();
262
263        byte[] bytecode = classBuffer.toByteArray();
264
265        new ClassReader(new ByteArrayInputStream(bytecode)).accept(analyzer,
266                ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
267
268
269        return loader.defineClassWithBytecode(className, bytecode);
270    }
271
272    private void trackClassFileChanges(String className)
273    {
274        if (isInnerClassName(className))
275        {
276            return;
277        }
278
279        String path = PlasticInternalUtils.toClassPath(className);
280
281        URL url = baseClassLoader.getResource(path);
282
283        if (isFileURL(url))
284        {
285            changeTracker.add(url);
286        }
287    }
288
289    /**
290     * Returns true if the url is non-null, and is for the "file:" protocol.
291     */
292    private boolean isFileURL(URL url)
293    {
294        return url != null && url.getProtocol().equals("file");
295    }
296
297    private boolean isInnerClassName(String className)
298    {
299        return className.indexOf('$') >= 0;
300    }
301}