    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}