/** ProxyHelper.java.
Purpose:
Description:
History:
12:03:06 PM Dec 25, 2014, Created by jumperchen
Copyright (C) 2014 Potix Corporation. All Rights Reserved.
*/
package org.zkoss.bind.proxy;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javassist.Modifier;
import javassist.util.proxy.Proxy;
import javassist.util.proxy.ProxyFactory;
import javassist.util.proxy.ProxyObject;
import org.zkoss.bind.Form;
import org.zkoss.bind.annotation.Immutable;
import org.zkoss.bind.annotation.ImmutableElements;
import org.zkoss.bind.annotation.ImmutableFields;
import org.zkoss.bind.sys.SavePropertyBinding;
import org.zkoss.bind.xel.zel.BindELContext;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Library;
import org.zkoss.lang.SystemException;
import org.zkoss.util.Pair;
import org.zkoss.zk.ui.UiException;
/**
* A proxy helper class to create a proxy cache mechanism for Set, List, Collection,
* Map, and POJO.
* @author jumperchen
* @since 8.0.0
*/
public class ProxyHelper {
private static Map<Class<?>, Boolean> _ignoredClasses = new ConcurrentHashMap<Class<?>, Boolean>();
private static List<ProxyTargetHandler> _proxyTargetHandlers;
private static ProxyDecorator _proxyDecorator;
static {
List<String> classes = Library.getProperties("org.zkoss.bind.proxy.IgnoredProxyClasses");
if (classes != null && classes.size() != 0) {
for (String className : classes) {
try {
addIgnoredProxyClass(Classes.forNameByThread(className.trim()));
} catch (ClassNotFoundException ex) {
throw new SystemException("Failed to load class " + className);
}
}
}
_proxyTargetHandlers = new LinkedList<ProxyTargetHandler>(ZKProxyTargetHandlers.getSystemProxyTargetHandlers());
String cls = Library.getProperty("org.zkoss.bind.proxy.ProxyDecoratorClass");
if (cls != null) {
try {
_proxyDecorator = (ProxyDecorator) Classes.newInstanceByThread(cls.trim());
} catch (Exception e) {
throw new SystemException("Failed to load class " + cls);
}
}
}
/**
* Creates a proxy object from the given origin object, if any.
* @param origin
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public static <T extends Object> T createProxyIfAny(T origin) {
return createProxyIfAny(origin, null);
}
/**
* Creates a proxy object from the given origin object, if any.
* @param origin
* @param annotations the annotations of the caller method to indicate whether
* the elements of the collection or Map type can proxy deeply, if any. (Optional)
* Like {@link ImmutableElements}
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public static <T extends Object> T createProxyIfAny(T origin, Annotation[] annotations) {
if (origin == null)
return null;
if (origin instanceof FormProxyObject) {
return origin;
}
origin = getOriginObject(origin);
boolean hasImmutableFields = false;
if (annotations != null) {
for (Annotation annot : annotations) {
if (annot.annotationType().isAssignableFrom(Immutable.class))
return origin;
if (annot.annotationType().isAssignableFrom(ImmutableFields.class))
hasImmutableFields = true;
}
}
if (isImmutable(origin))
return origin;
ProxyFactory factory = new ProxyFactory();
if (origin instanceof List) {
return (T) new ListProxy((List) origin, annotations);
} else if (origin instanceof Set) {
return (T) new SetProxy((Set) origin, annotations);
} else if (origin instanceof Map) {
return (T) new MapProxy((Map) origin, annotations);
} else if (origin instanceof Collection) {
return (T) new ListProxy((Collection) origin, annotations);
} else if (origin.getClass().isArray()) {
throw new UnsupportedOperationException("Array cannot be a proxy object!");
} else {
factory.setFilter(BeanProxyHandler.BEAN_METHOD_FILTER);
factory.setSuperclass(getTargetClassIfProxied(origin.getClass()));
if (hasImmutableFields) {
factory.setInterfaces(new Class[] { FormProxyObject.class, ImmutableFields.class });
} else {
factory.setInterfaces(new Class[] { FormProxyObject.class });
}
Class<?> proxyClass = factory.createClass();
Object p1 = null;
try {
p1 = proxyClass.newInstance();
} catch (Exception e) {
throw UiException.Aide.wrap(e,
"Cannot create a proxy object:[" + origin.getClass() + "], an empty constructor is needed.");
}
((Proxy) p1).setHandler(new BeanProxyHandler<T>(origin));
return _proxyDecorator != null ? (T) _proxyDecorator.decorate((ProxyObject) p1) : (T) p1;
}
}
/**
* Adds an ignored proxy class type. Once the data binder try to create a proxy
* object for the form binding, it will check whether the origin class type
* should be ignored.
* <p>Default it will apply these classes from the library property
* <code>org.zkoss.bind.proxy.IgnoredProxyClasses</code>
* </p>
*/
public static void addIgnoredProxyClass(Class<?> type) {
_ignoredClasses.put(type, Boolean.TRUE);
}
/**
* Returns whether the given origin object is immutable.
*/
public static boolean isImmutable(Object origin) {
if (BindELContext.isImmutable(origin))
return true;
return checkImmutable(origin.getClass());
}
/**
* Checks if the target class is already proxied
*/
private static <T> Class<? extends Object> getTargetClassIfProxied(Class<T> clazz) {
if (ProxyFactory.isProxyClass(clazz))
clazz = (Class<T>) clazz.getSuperclass();
return clazz;
}
/**
* Internal use only.
*/
public static <T extends Object> T getOriginObject(T origin) {
for (ProxyTargetHandler handlers : _proxyTargetHandlers) {
if (handlers != null) {
origin = handlers.getOriginObject(origin);
}
}
return origin;
}
private static boolean checkImmutable(Class<?> type) {
if (_ignoredClasses.containsKey(type))
return true;
if (Modifier.isFinal(type.getModifiers())) {
_ignoredClasses.put(type, Boolean.TRUE);
return true;
}
return false;
}
/**
* Creates a proxy form object from the given origin object, if any.
* @param origin the origin data object
* @param type the class type of the data object
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public static <T extends Object> T createFormProxy(T origin, Class<?> type) {
return createFormProxy(origin, type, null);
}
/**
* Creates a proxy form object from the given origin object, if any.
* @param origin the origin data object
* @param type the class type of the data object
* @param interfaces the interface type of the data object, if any.
* @since 8.0.1
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public static <T extends Object> T createFormProxy(T origin, Class<?> type, Class[] interfaces) {
if (origin instanceof Form)
return origin;
origin = getOriginObject(origin);
ProxyFactory factory = new ProxyFactory();
factory.setFilter(FormProxyHandler.FORM_METHOD_FILTER);
if (origin instanceof FormProxyObject)
type = ((FormProxyObject) origin).getOriginObject().getClass();
else if (origin != null)
type = getTargetClassIfProxied(origin.getClass());
factory.setSuperclass(type);
if (interfaces == null) {
factory.setInterfaces(new Class[] { FormProxyObject.class, Form.class, FormFieldCleaner.class });
} else {
int len0 = interfaces.length;
Class[] newArray = new Class[len0 + 3];
System.arraycopy(interfaces, 0, newArray, 0, len0);
newArray[len0] = FormProxyObject.class;
newArray[len0 + 1] = Form.class;
newArray[len0 + 2] = FormFieldCleaner.class;
factory.setInterfaces(newArray);
}
Class<?> proxyClass = factory.createClass();
Object p1 = null;
try {
p1 = proxyClass.newInstance();
} catch (Exception e) {
throw UiException.Aide.wrap(e,
"Cannot create a proxy object:[" + origin.getClass() + "], an empty constructor is needed.");
}
((Proxy) p1).setHandler(new FormProxyHandler<T>(origin));
return _proxyDecorator != null ? (T) _proxyDecorator.decorate((ProxyObject) p1) : (T) p1;
}
/**
* Internal use only.
*/
/* package */static void cacheSavePropertyBinding(ProxyNode node, String property, SavePropertyBinding savePropertyBinding) {
while (node != null) {
ProxyNode parent = node.getParent();
if (parent == null) {
node.getCachedSavePropertyBinding().add(new Pair<String, SavePropertyBinding>(property, savePropertyBinding));
break;
} else {
String parentProperty = parent.getProperty();
if (!property.startsWith("[") && parentProperty != null && parentProperty.length() != 0)
parentProperty += ".";
property = parentProperty + property;
node = parent;
}
}
}
/**
* Internal use only.
*/
/* package */static void callOnDataChange(ProxyNode node, Object value) {
while (node != null) {
ProxyNode parent = node.getParent();
if (parent == null && node.getOnDataChangeCallback() != null) {
node.getOnDataChangeCallback().call(value);
break;
} else {
node = parent;
}
}
}
/**
* Internal use only.
*/
/* package */static void callOnDirtyChange(ProxyNode node) {
while (node != null) {
ProxyNode parent = node.getParent();
if (parent == null && node.getOnDirtyChangeCallback() != null) {
node.getOnDirtyChangeCallback().call(true);
break;
} else {
node = parent;
}
}
}
/**
* An interface to decorate the {@link ProxyObject} for some purposes, like
* providing custom validation logic or adding extra handler on it.
*
* <p>To specify the custom proxy decorator class, you can specify the library
* property <code>org.zkoss.bind.proxy.ProxyDecoratorClass</code> in zk.xml
* </p>
* since 8.0.3
*/
public interface ProxyDecorator {
public ProxyObject decorate(ProxyObject proxyObject);
}
}