    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.ASM7)
    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);
    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}