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