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}