/******************************************************************************* * Copyright (c) 2014 BestSolution.at and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Tom Schindl <tom.schindl@bestsolution.at> - initial API and implementation *******************************************************************************/ package at.bestsolution.persistence.java.cglib; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EReference; import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.emf.ecore.util.EcoreUtil; import at.bestsolution.persistence.model.LazyEObject; import at.bestsolution.persistence.model.ResolveDelegate; public class CGLibObjectProxyInterceptor implements MethodInterceptor { private static Map<EClass, Map<String,EReference>> INTERCEPTED_METHODS = new HashMap<EClass, Map<String,EReference>>(); private static Map<ClassLoader, ClassLoader> HOOKED_CLASSLOADERS = new HashMap<ClassLoader, ClassLoader>(); private boolean intercepting; private Object proxyData; private ResolveDelegate proxyDelegate; private Map<EStructuralFeature,Boolean> resolvedAttributes = new HashMap<EStructuralFeature,Boolean>(); private boolean inverseAddRunning; public static EObject newInstance(EClass eClass) { CGLibObjectProxyInterceptor interceptor = new CGLibObjectProxyInterceptor(); Enhancer h = new Enhancer(); ClassLoader c = HOOKED_CLASSLOADERS.get(eClass.getInstanceClass().getClassLoader()); if( c == null ) { c = new MultiParentClassloader(eClass.getInstanceClass().getClassLoader(), CGLibObjectProxyInterceptor.class.getClassLoader()); HOOKED_CLASSLOADERS.put(eClass.getInstanceClass().getClassLoader(), c); } h.setClassLoader(c); //FIXME Can we get away without that??? EObject eo = EcoreUtil.create(eClass); // System.err.println(eo.getClass()); h.setSuperclass(eo.getClass()); h.setInterfaces(new Class[] { LazyEObject.class }); h.setCallback(interceptor); h.setUseCache(true); EObject o = (EObject) h.create(); Map<String,EReference> methods = INTERCEPTED_METHODS.get(eClass); if( methods == null ) { methods = new HashMap<String,EReference>(); for( EReference r : eClass.getEAllReferences() ) { String name = Character.toUpperCase(r.getName().charAt(0))+ r.getName().substring(1); methods.put("get" + name, r); if( ! r.isMany() ) { methods.put("set" + name, r); } } INTERCEPTED_METHODS.put(eClass, methods); } return o; // return object; } private EReference getInterceptedReference(EClass eClass, String methodName) { final Map<String, EReference> map = INTERCEPTED_METHODS.get(eClass); if( map != null ) { return map.get(methodName); } return null; } private EReference getInterceptedReference(EStructuralFeature feature) { if (feature instanceof EReference && !feature.isTransient()) { return (EReference) feature; } return null; } private boolean isResolved(EReference reference) { return resolvedAttributes.get(reference) == Boolean.TRUE; } private void markResolved(EReference reference) { resolvedAttributes.put(reference, Boolean.TRUE); } private void interceptResolve(LazyEObject object, EReference reference) { intercepting = true; try { if( proxyDelegate.resolve(object, proxyData, reference) ) { markResolved(reference); } } finally { intercepting = false; } } private boolean matches(Method method, String prefix, int numArgs) { return method.getName().startsWith(prefix) && method.getParameterTypes().length == numArgs; } private boolean matches(Method method, String name, Class<?>... argTypes) { return method.getName().equals(name) && argsEquals(method.getParameterTypes(), argTypes); } private boolean matchesNoArgCheck(Method method, String name) { return method.getName().equals(name); } private boolean argsEquals(Class<?>[] a, Class<?>[] b) { if (a.length == b.length) { for (int i = 0; i < a.length; i++) { if (a[i] != b[i]) { return false; } } return true; } else { return false; } } @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { // System.out.println("PROXY " + obj.getClass().getSimpleName() + " " + method.getName() + " " + Arrays.toString(method.getParameterTypes())); //FIXME We need to detect calls from outside and only resolve if the call // is not from EMF-internal // stop eagerly resolving if( inverseAddRunning ) { return proxy.invokeSuper(obj, args); } if (matches(method, "isResolved", EReference.class)) { final EReference reference = (EReference) args[0]; return isResolved(reference); } else if (matches(method, "setProxyData", Object.class)) { proxyData = args[0]; return null; } else if (matches(method, "setProxyDelegate", ResolveDelegate.class)) { proxyDelegate = (ResolveDelegate) args[0]; return null; } else if (matchesNoArgCheck(method, "eInverseAdd")) { try { inverseAddRunning = true; return proxy.invokeSuper(obj, args); } finally { inverseAddRunning = false; } } else if (matches(method, "get", 0)) { final EReference reference = getInterceptedReference(((EObject)obj).eClass(), method.getName()); if ( reference != null && ! reference.isTransient() && !isResolved(reference)) { if( intercepting ) { return proxy.invokeSuper(obj, args); } interceptResolve((LazyEObject) obj, reference); } return proxy.invokeSuper(obj, args); } else if (matches(method, "set", 1)) { final EReference reference = getInterceptedReference(((EObject)obj).eClass(), method.getName()); if ( reference != null && ! reference.isTransient() && !isResolved(reference)) { final Object rv = proxy.invokeSuper(obj, args); markResolved(reference); return rv; } return proxy.invokeSuper(obj, args); } else if (matches(method, "eSet", EStructuralFeature.class, Object.class)) { final EReference reference = getInterceptedReference((EStructuralFeature) args[0]); if( reference != null && ! reference.isTransient() && !isResolved(reference) ) { final Object rv = proxy.invokeSuper(obj, args); markResolved(reference); return rv; } return proxy.invokeSuper(obj, args); } else if (matches(method, "eIsSet", EStructuralFeature.class)) { final EReference reference = getInterceptedReference((EStructuralFeature) args[0]); if( reference != null && ! reference.isTransient() && !isResolved(reference) ) { if( intercepting ) { return proxy.invokeSuper(obj, args); } interceptResolve((LazyEObject) obj, reference); } return proxy.invokeSuper(obj, args); } else if (matches(method, "eGet", EStructuralFeature.class)) { final EReference reference = getInterceptedReference((EStructuralFeature) args[0]); if( reference != null && ! reference.isTransient() && !isResolved(reference) ) { if( intercepting ) { return proxy.invokeSuper(obj, args); } interceptResolve((LazyEObject) obj, reference); } return proxy.invokeSuper(obj, args); } else if (matches(method, "eGet", EStructuralFeature.class, Boolean.class)) { final EReference reference = getInterceptedReference((EStructuralFeature) args[0]); if ( reference != null && ! reference.isTransient() && !isResolved(reference) ) { if( intercepting ) { return proxy.invokeSuper(obj, args); } interceptResolve((LazyEObject) obj, reference); } return proxy.invokeSuper(obj, args); } else { return proxy.invokeSuper(obj, args); } } public static class MultiParentClassloader extends ClassLoader { private ClassLoader[] additionalClassloader; public MultiParentClassloader(ClassLoader primaryClassloader, ClassLoader... additionalClassloader) { super(primaryClassloader); this.additionalClassloader = additionalClassloader; } @Override public URL getResource(String name) { URL url; for( ClassLoader c : additionalClassloader ) { url = c.getResource(name); if( url != null ) { return url; } } return super.getResource(name); } @Override public InputStream getResourceAsStream(String name) { InputStream in; for( ClassLoader c : additionalClassloader ) { in = c.getResourceAsStream(name); if( in != null ) { return in; } } return super.getResourceAsStream(name); } @Override public Enumeration<URL> getResources(String name) throws IOException { // TODO We need to make this more efficient guava???? List<URL> list = new ArrayList<URL>(); Enumeration<URL> e = super.getResources(name); while( e.hasMoreElements() ) { list.add(e.nextElement()); } for( ClassLoader c : additionalClassloader ) { e = c.getResources(name); while( e.hasMoreElements() ) { list.add(e.nextElement()); } } return Collections.enumeration(list); } @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try { return super.loadClass(name); } catch(ClassNotFoundException e) { for( ClassLoader c : additionalClassloader ) { try { return c.loadClass(name); } catch(ClassNotFoundException t) { } } throw new ClassNotFoundException(e.getMessage(), e); } } } }