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