001/**
002 * Copyright 2005-2018 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.krad.uif.util;
017
018import java.lang.reflect.InvocationHandler;
019import java.lang.reflect.InvocationTargetException;
020import java.lang.reflect.Method;
021import java.lang.reflect.Proxy;
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.List;
025import java.util.Map;
026import java.util.WeakHashMap;
027
028import org.kuali.rice.krad.datadictionary.Copyable;
029
030/**
031 * Proxy invocation handler for delaying deep copy for framework objects that may not need to be
032 * fully traversed by each transaction.
033 * 
034 * <p>
035 * Proxied objects served by this handler will refer to the original source object until a
036 * potentially read-write method is invoked. Once such a method is invoked, then the original source
037 * is copied to a new object on the fly and the call is forwarded to the copy.
038 * </p>
039 * 
040 * @author Kuali Rice Team (rice.collab@kuali.org)
041 */
042public class DelayedCopyableHandler implements InvocationHandler {
043
044    private static final String COPY = "copy";
045
046    private final Copyable original;
047    private Copyable copy;
048
049    DelayedCopyableHandler(Copyable original) {
050        this.original = original;
051    }
052
053    /**
054     * Intercept method calls, and copy the original source object as needed. The determination that
055     * a method is read-write is made based on the method name and/or return type as follows:
056     * 
057     * <ul>
058     * <li>Methods starting with "get" or "is", are considered read-only</li>
059     * <li>Methods returning Copyable, List, Map, or an array, are considered read-write regardless
060     * of name</li>
061     * </ul>
062     * 
063     * {@inheritDoc}
064     */
065    @Override
066    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
067        String methodName = method.getName();
068        Class<?> returnType = method.getReturnType();
069        boolean atomic = copy == null && (COPY.equals(methodName) ||
070                ((methodName.startsWith("get") || methodName.startsWith("is"))
071                        && !Copyable.class.isAssignableFrom(returnType)
072                        && !List.class.isAssignableFrom(returnType)
073                        && !Map.class.isAssignableFrom(returnType)
074                        && !returnType.isArray()));
075        ProcessLogger.ntrace("delay-" + (copy != null ? "dup" : atomic ? "atomic" : "copy") +
076                ":", ":" + methodName + ":" + original.getClass().getSimpleName(), 1000);
077
078        if (copy == null && !atomic) {
079            copy = CopyUtils.copy(original);
080        }
081
082        try {
083            return method.invoke(copy == null ? original : copy, args);
084        } catch (InvocationTargetException e) {
085            if (e.getCause() != null) {
086                throw e.getCause();
087            } else {
088                throw e;
089            }
090        }
091    }
092
093    /**
094     * Copy a source object if needed, and unwrap from the proxy.
095     *
096     * @param source The object to unwrap.
097     * @return The non-proxied bean represented by source, copied if needed. When source is not
098     *         copyable, or not proxied, it is returned as-is.
099     */
100    static <T> T unwrap(T source) {
101        if (!(source instanceof Copyable)) {
102            return source;
103        }
104
105        Class<?> sourceClass = source.getClass();
106        if (!Proxy.isProxyClass(sourceClass)) {
107            return source;
108        }
109
110        InvocationHandler handler = Proxy.getInvocationHandler(source);
111        if (!(handler instanceof DelayedCopyableHandler)) {
112            return source;
113        }
114
115        DelayedCopyableHandler sourceHandler = (DelayedCopyableHandler) handler;
116        if (sourceHandler.copy == null) {
117            sourceHandler.copy = CopyUtils.copy(sourceHandler.original);
118        }
119
120        @SuppressWarnings("unchecked")
121        T rv = (T) sourceHandler.copy;
122        return unwrap(rv);
123    }
124
125    /**
126     * Determins if a source object is a delayed copy proxy that hasn't been copied yet.
127     *
128     * @param source The object to check.
129     * @return True if source is a delayed copy proxy instance, and hasn't been copied yet.
130     */
131    public static boolean isPendingDelayedCopy(Copyable source) {
132        Class<?> sourceClass = source.getClass();
133
134        // Unwrap proxied source objects from an existing delayed copy handler, if applicable
135        if (Proxy.isProxyClass(sourceClass)) {
136            InvocationHandler handler = Proxy.getInvocationHandler(source);
137            if (handler instanceof DelayedCopyableHandler) {
138                DelayedCopyableHandler sourceHandler = (DelayedCopyableHandler) handler;
139                return sourceHandler.copy == null;
140            }
141        }
142
143        return false;
144    }
145
146    /**
147     * Get a proxy instance providing delayed copy behavior on a source component.
148     * @param source The source object
149     * @return proxy instance wrapping the object
150     */
151    public static Copyable getDelayedCopy(Copyable source) {
152        Class<?> sourceClass = source.getClass();
153
154        // Unwrap proxied source objects from an existing delayed copy handler, if applicable
155        if (Proxy.isProxyClass(sourceClass)) {
156            InvocationHandler handler = Proxy.getInvocationHandler(source);
157            if (handler instanceof DelayedCopyableHandler) {
158                DelayedCopyableHandler sourceHandler = (DelayedCopyableHandler) handler;
159                return getDelayedCopy(sourceHandler.copy == null
160                        ? sourceHandler.original : sourceHandler.copy);
161            }
162        }
163
164        return (Copyable) Proxy.newProxyInstance(sourceClass.getClassLoader(),
165                getMetadata(sourceClass).interfaces, new DelayedCopyableHandler(source));
166    }
167
168    /**
169     * Internal field cache meta-data node, for reducing interface lookup overhead.
170     * 
171     * @author Kuali Rice Team (rice.collab@kuali.org)
172     */
173    private static class ClassMetadata {
174
175        /**
176         * All interfaces implemented by the class.
177         */
178        private final Class<?>[] interfaces;
179
180        /**
181         * Create a new field reference for a target class.
182         * 
183         * @param targetClass The class to inspect for meta-data.
184         */
185        private ClassMetadata(Class<?> targetClass) {
186            List<Class<?>> interfaceList = new ArrayList<Class<?>>();
187
188            Class<?> currentClass = targetClass;
189            while (currentClass != Object.class && currentClass != null) {
190                for (Class<?> ifc : currentClass.getInterfaces()) {
191                    if (!interfaceList.contains(ifc)) {
192                        interfaceList.add(ifc);
193                    }
194                }
195                currentClass = currentClass.getSuperclass();
196            }
197
198            // Seal index collections to prevent external modification.
199            interfaces = interfaceList.toArray(new Class<?>[interfaceList.size()]);
200        }
201    }
202
203    /**
204     * Static cache for reducing annotated field lookup overhead.
205     */
206    private static final Map<Class<?>, ClassMetadata> CLASS_META_CACHE =
207            Collections.synchronizedMap(new WeakHashMap<Class<?>, ClassMetadata>());
208
209    /**
210     * Get copy metadata for a class.
211     * @param targetClass The class.
212     * @return Copy metadata for the class.
213     */
214    private static final ClassMetadata getMetadata(Class<?> targetClass) {
215        ClassMetadata metadata = CLASS_META_CACHE.get(targetClass);
216
217        if (metadata == null) {
218            CLASS_META_CACHE.put(targetClass, metadata = new ClassMetadata(targetClass));
219        }
220
221        return metadata;
222    }
223
224}