https://issues.apache.org/jira/browse/EXTSCRIPT-44

Save state of affairs, all beans kick through

git-svn-id: https://svn.apache.org/repos/asf/myfaces/extensions/scripting/trunk@1401723 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/extscript-core-root/extscript-spring/src/main/java/org/apache/myfaces/extensions/scripting/spring/bean/CompilationAwareRefreshableBeanFactory.java b/extscript-core-root/extscript-spring/src/main/java/org/apache/myfaces/extensions/scripting/spring/bean/CompilationAwareRefreshableBeanFactory.java
index 7631519..d4d8295 100644
--- a/extscript-core-root/extscript-spring/src/main/java/org/apache/myfaces/extensions/scripting/spring/bean/CompilationAwareRefreshableBeanFactory.java
+++ b/extscript-core-root/extscript-spring/src/main/java/org/apache/myfaces/extensions/scripting/spring/bean/CompilationAwareRefreshableBeanFactory.java
@@ -19,17 +19,26 @@
 
 package org.apache.myfaces.extensions.scripting.spring.bean;
 
-import org.apache.myfaces.extensions.scripting.core.common.util.ReflectUtil;
+import org.apache.myfaces.extensions.scripting.core.api.WeavingContext;
 import org.apache.myfaces.extensions.scripting.core.engine.ThrowAwayClassloader;
 import org.apache.myfaces.extensions.scripting.spring.bean.support.CompilationAwareInstantiationStrategy;
+import org.apache.myfaces.extensions.scripting.spring.util.ProxyAopUtils;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.FatalBeanException;
 import org.springframework.beans.factory.BeanFactory;
 import org.springframework.beans.factory.CannotLoadBeanClassException;
+import org.springframework.beans.factory.ObjectFactory;
+import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.beans.factory.support.DefaultListableBeanFactory;
 import org.springframework.beans.factory.support.RootBeanDefinition;
 import org.springframework.core.DecoratingClassLoader;
 import org.springframework.util.ClassUtils;
 
-import java.lang.reflect.Constructor;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
 
 /**
  * @author Werner Punz (latest modification by $Author$)
@@ -76,6 +85,50 @@
         return super.resolveBeanClass(mbd, beanName);
     }
 
+    // ------------------------------------------ AbstractBeanFactory methods
+
+    @Override
+    public Object getBean(final String name, final Class requiredType, final Object[] args) throws BeansException
+    {
+        Object bean = super.getBean(name, requiredType, args);
+
+        BeanDefinition beanDefinition = getMergedBeanDefinition(name);
+        //if (beanDefinition.hasAttribute(
+        //        RefreshableBeanAttribute.REFRESHABLE_BEAN_ATTRIBUTE)) { // Try to minimize synchronizing ..
+        // Note that the DefaultListableBeanFactory internally caches bean definitions,
+        // so I think it's safe to use the return value as monitor for the synchronized
+        // block.
+        synchronized (beanDefinition)
+        {
+            // Obtain the metadata attribute of the bean definition that contains the
+            // required information to determine wheter a refresh is required or not.
+            //RefreshableBeanAttribute refreshableAttribute =
+            //        (RefreshableBeanAttribute) beanDefinition.getAttribute(
+            //                RefreshableBeanAttribute.REFRESHABLE_BEAN_ATTRIBUTE);
+            if (requiresRefresh(bean, beanDefinition))
+            {
+                bean = refresh(bean, name, new ObjectFactory()
+                {
+                    public Object getObject() throws BeansException
+                    {
+                        return CompilationAwareRefreshableBeanFactory.super.getBean(name, requiredType, args);
+                    }
+                }, beanDefinition);
+                //refreshableAttribute.executedRefresh();
+            }
+        }
+        //}
+
+        return bean;
+    }
+
+    private boolean requiresRefresh(Object bean, BeanDefinition beanDefinition)
+    {
+        //TODO add the refreshable classes from the event handler there
+        Class clazz = ProxyAopUtils.resolveTargetClass(bean);
+        return WeavingContext.getInstance().isDynamic(clazz);
+    }
+
     @Override
     protected Class resolveBeanClass(RootBeanDefinition beanDefinition, String beanName, Class[] typesToMatch) throws CannotLoadBeanClassException
     {
@@ -105,19 +158,6 @@
             }
 
             Class retVal = beanDefinition.resolveBeanClass(getBeanClassLoader());
-           /* try
-            {
-                //spring caches the constructor, we have to set it anew
-                //this imposes a limitation to () constructors but
-                //I guess we can live with it
-                Constructor ctor = retVal.getConstructor();
-                ReflectUtil.setField(beanDefinition,"resolvedConstructorOrFactoryMethod", ctor, true);
-            }
-            catch (NoSuchMethodException e)
-            {
-                e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
-            }*/
-
             return retVal;
         }
         catch (ClassNotFoundException ex)
@@ -132,6 +172,117 @@
         }
     }
 
+    protected Object refresh(Object beanInstance, String beanName,
+                             ObjectFactory objectFactory, BeanDefinition beanDefinition)
+    {
+        // In the case of a prototype-scoped bean, we don't have to worry about copying
+        // properties, etc. as it's definitely always a newly created beanInstance instance
+        // and so it's safe to assume that we're using the most recent Class reference.
+        if (!BeanDefinition.SCOPE_PROTOTYPE.equals(beanDefinition.getScope()))
+        {
+            // At first destroy the previously created bean (e.g. execute the according callbacks
+            // if the bean implements the DisposableBean interface and remove it from the according
+            // scope).
+            if (beanDefinition.isSingleton())
+            {
+                destroySingleton(beanName);
+            } else
+            {
+                destroyScopedBean(beanName);
+            }
 
+            // Now that the bean has already been destroyed, we'll create a new instance of it.
+            Object newBeanInstance = objectFactory.getObject();
+
+            // Copy the properties only if we're not dealing with a proxy. In that case the
+            // BeanFactory will copy the properties anyway once the target bean gets recreated (in
+            // fact, otherwise we would have to face dozens of InvocationTargetExceptions as it would
+            // create a huge mess with different versions of different Class references).
+            if (!ProxyAopUtils.isProxyClass(newBeanInstance.getClass()))
+            {
+                copyProperties(beanName, beanDefinition, beanInstance, newBeanInstance);
+            }
+
+            return newBeanInstance;
+        } else
+        {
+            return beanInstance;
+        }
+    }
+
+    /**
+     * <p>Copies the property values of the given source bean into the given target bean. Note that
+     * this method, in contrast to the {@link org.springframework.beans.BeanUtils#copyProperties(Object, Object)} method,
+     * is aware of the possibility that copying properties from one bean to another could override newer versions of beans
+     * that Spring has injected. </p>
+     */
+    private void copyProperties(String beanName, BeanDefinition beanDefinition, Object source, Object target)
+    {
+        // Definitely we don't want to mess around with properties of
+        // any proxy objects, so we'll use its target class here.
+        PropertyDescriptor[] targetDescriptors = BeanUtils.getPropertyDescriptors(
+                ProxyAopUtils.resolveTargetClass(target));
+
+        for (int i = 0; i < targetDescriptors.length; i++)
+        {
+            PropertyDescriptor targetDescriptor = targetDescriptors[i];
+            if (targetDescriptor.getWriteMethod() != null)
+            {
+                PropertyDescriptor sourceDescriptor =
+                        BeanUtils.getPropertyDescriptor(source.getClass(), targetDescriptor.getName());
+                if (sourceDescriptor != null && sourceDescriptor.getReadMethod() != null)
+                {
+                    try
+                    {
+                        Object value = invokeMethod(sourceDescriptor.getReadMethod(), source, new Object[0]);
+                        if (value instanceof ThrowAwayClassloader)
+                        {
+                            //spring let spring handle the property assignment for a dynamic class
+                            //so that property reloads work
+                        } else
+                        {
+                            invokeMethod(targetDescriptor.getWriteMethod(), target, new Object[]{value});
+                        }
+
+                        /*if (getReloadingClassLoader().isOutdated(value.getClass()) &&
+                                beanDefinition.getPropertyValues().contains(targetDescriptor.getName())) {
+                            // If the currenty property has been injected by Spring already and the
+                            // source object returned an outdated object, keep the one that has been
+                            // injected by Spring as it's more likely to be the most recent one.
+
+                            if (logger.isWarnEnabled()) {
+                                logger.warn(" This factory will not copy the property '" + targetDescriptor.getName() + "' of the bean '" +
+                                        beanName + "' as the source object '" + source + "' only returns an outdated object '" + value + "'.");
+                            }
+                        }
+                        else {
+                        invokeMethod(targetDescriptor.getWriteMethod(), target, new Object[]{value});
+                        /*}*/
+                    }
+                    catch (Throwable ex)
+                    {
+                        throw new FatalBeanException("Could not copy properties from source to target", ex);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * <p>Invokes the given method on the given target with the given arguments. Note that
+     * this method assumes that the method itself is already accessible (which should be the
+     * case as it's sole purpose is to invoke getters and setters which have to be public
+     * methods anyway).</p>
+     */
+    private static Object invokeMethod(Method method, Object target, Object[] args)
+            throws InvocationTargetException, IllegalAccessException
+    {
+        if (!Modifier.isPublic(method.getDeclaringClass().getModifiers()))
+        {
+            method.setAccessible(true);
+        }
+
+        return method.invoke(target, args);
+    }
 
 }
diff --git a/extscript-core-root/extscript-spring/src/main/java/org/apache/myfaces/extensions/scripting/spring/bean/SpringWeavingContext.java b/extscript-core-root/extscript-spring/src/main/java/org/apache/myfaces/extensions/scripting/spring/bean/SpringWeavingContext.java
new file mode 100644
index 0000000..e2e8687
--- /dev/null
+++ b/extscript-core-root/extscript-spring/src/main/java/org/apache/myfaces/extensions/scripting/spring/bean/SpringWeavingContext.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.extensions.scripting.spring.bean;
+
+/**
+ * @author Werner Punz (latest modification by $Author$)
+ * @version $Revision$ $Date$
+ *
+ * A helper with additional meta information regarding spring
+ */
+
+public class SpringWeavingContext
+{
+
+
+}
diff --git a/extscript-core-root/extscript-spring/src/main/java/org/apache/myfaces/extensions/scripting/spring/util/ProxyAopUtils.java b/extscript-core-root/extscript-spring/src/main/java/org/apache/myfaces/extensions/scripting/spring/util/ProxyAopUtils.java
new file mode 100644
index 0000000..6a4d7fb
--- /dev/null
+++ b/extscript-core-root/extscript-spring/src/main/java/org/apache/myfaces/extensions/scripting/spring/util/ProxyAopUtils.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.extensions.scripting.spring.util;
+
+import org.springframework.aop.scope.ScopedObject;
+import org.springframework.aop.support.AopUtils;
+
+import java.lang.reflect.Proxy;
+
+/**
+ *
+ */
+public class ProxyAopUtils {
+
+    /**
+     * <p>Returns true if and only if the given Class reference is either a JDK
+     * dynamic proxy or a CGLIB proxy.</p>
+     *
+     * @param clazz the class to check
+     * @see #isJdkProxyClass(Class)
+     * @see #isCglibProxyClass(Class)
+     */
+    public static boolean isProxyClass(Class clazz) {
+        return isJdkProxyClass(clazz) || isCglibProxyClass(clazz);
+    }
+
+    /**
+     * <p>Returns true if and only if the specified class was dynamically
+     * generated to be a proxy class using the builtin JDK dynamic proxy
+     * facilities.</p>
+     *
+     * @param clazz the class to check
+     */
+    public static boolean isJdkProxyClass(Class clazz) {
+        return clazz != null && Proxy.isProxyClass(clazz);
+    }
+
+    /**
+     * <p>Returns true if and only if the specified class was dynamically
+     * generated to be a proxy class using the CGLIB dynamic proxy
+     * facilities.</p>
+     *
+     * @param clazz the class to check
+     */
+    public static boolean isCglibProxyClass(Class clazz) {
+        return clazz != null && clazz.getName().contains("$$");
+    }
+
+    public static boolean isScopedProxy(Object obj) {
+        if (isProxyClass(obj.getClass())) {
+            return ProxyAopUtilities.isScopedObject(obj);
+        }
+        else {
+            return false;
+        }
+    }
+
+    /**
+     * <p>Returns the target class of the given Class reference, i.e. if the
+     * given Class reference is a dynamiccaly generated proxy class, its target
+     * class will be returned (that is, the Class reference being proxied by
+     * this one). Otherwise the given Class reference will be returned.</p>
+     */
+    public static Class resolveTargetClass(Object proxy) {
+        if (isProxyClass(proxy.getClass())) {
+            return ProxyAopUtilities.resolveTargetClass(proxy);
+        }
+        else {
+            return proxy.getClass();
+        }
+    }
+
+    // ------------------------------------------ Private static classes
+
+    /**
+     * <p>Private static class that knows how to deal with dynamically generated proxy.
+     * Note that the implementation requires that Spring AOP is present on the classpath,
+     * which is why a private static class has been introduced. In doing so, Spring AOP
+     * is only required if we're definitely dealing with a proxy.</p>
+     */
+    private static class ProxyAopUtilities {
+
+        /**
+         * <p>Returns whether the given object is a scoped object, i.e. a proxy
+         * delegating to a bean that resides within a certain scope.</p>
+         */
+        public static boolean isScopedObject(Object obj) {
+            return obj instanceof ScopedObject;
+        }
+
+        /**
+         * <p>Returns the target class of the given proxy Class reference, i.e.
+         * the Class reference being proxied by this one.</p>
+         */
+        public static Class resolveTargetClass(Object proxy) {
+            return AopUtils.getTargetClass(proxy);
+        }
+
+    }
+
+}
diff --git a/extscript-core-root/extscript-spring/src/main/java/org/apache/myfaces/extensions/scripting/spring/util/ResourceUtils.java b/extscript-core-root/extscript-spring/src/main/java/org/apache/myfaces/extensions/scripting/spring/util/ResourceUtils.java
new file mode 100644
index 0000000..aff9ba5
--- /dev/null
+++ b/extscript-core-root/extscript-spring/src/main/java/org/apache/myfaces/extensions/scripting/spring/util/ResourceUtils.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.extensions.scripting.spring.util;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.core.io.Resource;
+
+import java.io.IOException;
+import java.net.JarURLConnection;
+import java.net.URLConnection;
+
+/**
+ * <p>Utility methods for dealing with resource files in the
+ * file system. Mainly for internal use within the module.</p>
+ */
+public class ResourceUtils {
+
+    /**
+     * The logger instance for this class.
+     */
+    private static final Log logger = LogFactory.getLog(ResourceUtils.class);
+
+    // ------------------------------------------ Public static methods
+
+    /**
+     * <p>Determines whether the given resource file is already outdated compared
+     * to the given timestamp, i.e. it compares the given timestamp to the last
+     * modified timestamp of the given resource and returns <code>true</code>
+     * if the given timestamp is less than the timestamp of the given resource.</p>
+     * <p/>
+     * <p>Note that this methods assumes that the given resource exists, otherwise
+     * an exception will be thrown.</p>
+     *
+     * @param timestamp the timestamp that you want to compare the resource to
+     * @param resource  the resource that you want to check
+     * @return <code>true</code>, if the given resource is outdated, otherwise
+     *         <code>false</code>
+     * @throws java.io.IOException if an I/O error occurs
+     */
+    public static boolean isOutdated(long timestamp, Resource resource) throws IOException {
+        long lastModified;
+
+        if (resource instanceof FileSystemResource) {
+            lastModified = resource.getFile().lastModified();
+        }
+        else {
+            URLConnection connection = resource.getURL().openConnection();
+            connection.setUseCaches(false);
+
+            // The problem with JarURLConnections is that they seemingly don't care
+            // much about the last modified timestamp, they always return 0.
+            if (connection instanceof JarURLConnection) {
+                connection = ((JarURLConnection) connection).getJarFileURL().openConnection();
+                connection.setUseCaches(false);
+            }
+
+            try {
+                lastModified = connection.getLastModified();
+            } finally {
+                try {
+                    connection.getInputStream().close();
+                } catch (IOException ex) {
+                    if (logger.isErrorEnabled()) {
+                        logger.error("An I/O error occured while closing the URL connection for the resource '" + resource + "'.", ex);
+                    }
+                }
+            }
+        }
+
+        return timestamp < lastModified;
+    }
+
+}