package be.selckin.swu.pmodel; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.apache.wicket.model.IModel; import org.apache.wicket.model.PropertyModel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.io.Serializable; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.List; import java.util.Map; public class PModel { private PModel() { } private static final Logger log = LoggerFactory.getLogger(PModel.class); private static final ImmutableSet<Class<?>> NO_FINAL_WARN = ImmutableSet.<Class<?>>of(String.class, Integer.class, Double.class, Long.class, Float.class, Boolean.class, Byte.class); private static final Joiner propertyJoiner = Joiner.on("."); private static final ProxyFactory proxyFactory = new ProxyFactory(); private static final Map<Class<?>, Object> proxyCache = Maps.newConcurrentMap(); private static final ThreadLocal<IModel<?>> model = new ThreadLocal<IModel<?>>(); private static final ThreadLocal<List<String>> expression = new ThreadLocal<List<String>>(); @SuppressWarnings("UnusedParameters") public static <T> IModel<T> model(@Nullable T object) { log.trace("model called"); try { if (model.get() == null) throw new PModelException("No model owner, did you forget bean()?"); if (expression.get() == null) throw new PModelException("No model expression, did you forget the expression on bean()'s return value? Model owner is: " + model.get()); return new PropertyModel<T>(model.get(), propertyJoiner.join(expression.get())); } finally { reset(); } } @SuppressWarnings("unchecked") public static <T> T bean(Class<T> type, IModel<? extends T> object) { log.trace("bean called: {}", type); if (type == null) throw new PModelException("Missing type"); if (model.get() != null) { reset(); // otherwise all future calls will fail throw new PModelException("Model owner already set, did you forget model() for a previous of()? Previous owner is: " + model.get()); } reset(); model.set(object); expression.set(Lists.<String>newArrayList()); return (T) createProxy(type); } protected static void reset() { model.remove(); expression.remove(); } @Nullable private static <T> T createProxy(Class<T> type) { if (type.isPrimitive()) return null; if (Modifier.isFinal(type.getModifiers())) { final Class<?> warnType; if (type.isArray()) warnType = type.getComponentType(); else warnType = type; if (!NO_FINAL_WARN.contains(warnType) && !warnType.isEnum()) log.warn("Can't proxy {} because the class is final.", type); return null; } T proxy = (T) proxyCache.get(type); if (proxy == null) { proxy = proxyFactory.createProxy(type, new PModelInvocationHandler()); proxyCache.put(type, proxy); } return proxy; } // placeholder public static <T> T read(Class<T> type, IModel<? extends T> object) { return bean(type, object); } // placeholder public static <T> T write(Class<T> type, IModel<? extends T> object) { return bean(type, object); } protected static String getPropertyName(Method method) { String name = method.getName(); if (name.startsWith("get") && name.length() > 3) { return name.substring(3); } else if (name.startsWith("is") && name.length() > 2) { return name.substring(2); } throw new PModelException("Not a standard javabean-type method: " + method); } public static <T> PModeler<T> modeler(Class<T> type, IModel<? extends T> object) { return new PModeler<T>(type, object); } private static class PModelInvocationHandler implements InvocationHandler { @Override @SuppressWarnings("ProhibitedExceptionDeclared") public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (model.get() == null || expression.get() == null) throw new PModelException("Proxy called outside of normal cycle"); expression.get().add(getPropertyName(method)); return createProxy(method.getReturnType()); } } public static class PModeler<T> implements Serializable { private final Class<T> type; private final IModel<? extends T> model; public PModeler(Class<T> type, IModel<? extends T> model) { this.type = type; this.model = model; } @SuppressWarnings("unchecked") public T bean() { return PModel.bean(type, model); } public T read() { return PModel.bean(type, model); } public T write() { return PModel.bean(type, model); } } }