View Javadoc
1   /**
2    * Copyright 2005-2015 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.uif.util;
17  
18  import java.lang.reflect.InvocationHandler;
19  import java.lang.reflect.InvocationTargetException;
20  import java.lang.reflect.Method;
21  import java.lang.reflect.Proxy;
22  import java.util.ArrayList;
23  import java.util.Collections;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.WeakHashMap;
27  
28  import org.kuali.rice.krad.datadictionary.Copyable;
29  
30  /**
31   * Proxy invocation handler for delaying deep copy for framework objects that may not need to be
32   * fully traversed by each transaction.
33   * 
34   * <p>
35   * Proxied objects served by this handler will refer to the original source object until a
36   * potentially read-write method is invoked. Once such a method is invoked, then the original source
37   * is copied to a new object on the fly and the call is forwarded to the copy.
38   * </p>
39   * 
40   * @author Kuali Rice Team (rice.collab@kuali.org)
41   */
42  public class DelayedCopyableHandler implements InvocationHandler {
43  
44      private static final String COPY = "copy";
45  
46      private final Copyable original;
47      private Copyable copy;
48  
49      DelayedCopyableHandler(Copyable original) {
50          this.original = original;
51      }
52  
53      /**
54       * Intercept method calls, and copy the original source object as needed. The determination that
55       * a method is read-write is made based on the method name and/or return type as follows:
56       * 
57       * <ul>
58       * <li>Methods starting with "get" or "is", are considered read-only</li>
59       * <li>Methods returning Copyable, List, Map, or an array, are considered read-write regardless
60       * of name</li>
61       * </ul>
62       * 
63       * {@inheritDoc}
64       */
65      @Override
66      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
67          String methodName = method.getName();
68          Class<?> returnType = method.getReturnType();
69          boolean atomic = copy == null && (COPY.equals(methodName) ||
70                  ((methodName.startsWith("get") || methodName.startsWith("is"))
71                          && !Copyable.class.isAssignableFrom(returnType)
72                          && !List.class.isAssignableFrom(returnType)
73                          && !Map.class.isAssignableFrom(returnType)
74                          && !returnType.isArray()));
75          ProcessLogger.ntrace("delay-" + (copy != null ? "dup" : atomic ? "atomic" : "copy") +
76                  ":", ":" + methodName + ":" + original.getClass().getSimpleName(), 1000);
77  
78          if (copy == null && !atomic) {
79              copy = CopyUtils.copy(original);
80          }
81  
82          try {
83              return method.invoke(copy == null ? original : copy, args);
84          } catch (InvocationTargetException e) {
85              if (e.getCause() != null) {
86                  throw e.getCause();
87              } else {
88                  throw e;
89              }
90          }
91      }
92  
93      /**
94       * Copy a source object if needed, and unwrap from the proxy.
95       *
96       * @param source The object to unwrap.
97       * @return The non-proxied bean represented by source, copied if needed. When source is not
98       *         copyable, or not proxied, it is returned as-is.
99       */
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 }