/* * Copyright 2004-2009 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.compass.spring.support; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.compass.core.Compass; import org.compass.core.CompassContext; import org.compass.core.CompassSession; import org.compass.core.spi.InternalCompass; import org.compass.core.spi.InternalCompassSession; import org.compass.core.support.session.CompassSessionTransactionalProxy; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.util.ReflectionUtils; /** * BeanPostProcessor that processes {@link org.compass.core.CompassContext} * annotation for injection of Compass interfaces. Any such annotated fields * or methods in any Spring-managed object will automatically be injected. * <p/> * Will inject either a {@link Compass} or {@link CompassSession} instances. * * @author kimchy */ public class CompassContextBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements ApplicationContextAware { protected final Log logger = LogFactory.getLog(getClass()); private ApplicationContext applicationContext; private Map<Class<?>, List<AnnotatedMember>> classMetadata = new HashMap<Class<?>, List<AnnotatedMember>>(); private Map<String, Compass> compassesByName; private Compass uniqueCompass; public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } /** * Lazily initialize compass map. */ private synchronized void initMapsIfNecessary() { if (this.compassesByName == null) { this.compassesByName = new HashMap<String, Compass>(); // Look for named Compasses String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, Compass.class); for (String emfName : beanNames) { Compass compass = (Compass) this.applicationContext.getBean(emfName); compassesByName.put(((InternalCompass) compass).getName(), compass); } if (this.compassesByName.isEmpty()) { if (beanNames.length == 1) { this.uniqueCompass = (Compass) this.applicationContext.getBean(beanNames[0]); } } else if (this.compassesByName.size() == 1) { this.uniqueCompass = this.compassesByName.values().iterator().next(); } if (this.compassesByName.isEmpty() && this.uniqueCompass == null) { logger.warn("No named compass instances defined and not exactly one anonymous one: cannot inject"); } } } /** * Find a Compass with the given name in the current * application context * * @param compassName name of the EntityManagerFactory * @return the EntityManagerFactory or throw NoSuchBeanDefinitionException * @throws NoSuchBeanDefinitionException if there is no such EntityManagerFactory * in the context */ protected Compass findEntityManagerFactoryByName(String compassName) throws NoSuchBeanDefinitionException { initMapsIfNecessary(); if (compassName == null || "".equals(compassName)) { if (this.uniqueCompass != null) { return this.uniqueCompass; } else { throw new NoSuchBeanDefinitionException( "No Compass name given and factory contains several"); } } Compass namedCompass = this.compassesByName.get(compassName); if (namedCompass == null) { throw new NoSuchBeanDefinitionException("No Compass found for name [" + compassName + "]"); } return namedCompass; } @Override public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException { List<AnnotatedMember> metadata = findClassMetadata(bean.getClass()); for (AnnotatedMember member : metadata) { member.inject(bean); } return true; } private synchronized List<AnnotatedMember> findClassMetadata(Class<? extends Object> clazz) { List<AnnotatedMember> metadata = this.classMetadata.get(clazz); if (metadata == null) { final List<AnnotatedMember> newMetadata = new LinkedList<AnnotatedMember>(); ReflectionUtils.doWithFields(clazz, new ReflectionUtils.FieldCallback() { public void doWith(Field f) { addIfPresent(newMetadata, f); } }); // TODO is it correct to walk up the hierarchy for methods? Otherwise inheritance // is implied? CL to resolve ReflectionUtils.doWithMethods(clazz, new ReflectionUtils.MethodCallback() { public void doWith(Method m) { addIfPresent(newMetadata, m); } }); metadata = newMetadata; this.classMetadata.put(clazz, metadata); } return metadata; } private void addIfPresent(List<AnnotatedMember> metadata, AccessibleObject ao) { CompassContext compassContext = ao.getAnnotation(CompassContext.class); if (compassContext != null) { metadata.add(new AnnotatedMember(compassContext.name(), ao)); } } /** * Class representing injection information about an annotated field * or setter method. */ private class AnnotatedMember { private final String name; private final AccessibleObject member; public AnnotatedMember(String name, AccessibleObject member) { this.name = name; this.member = member; // Validate member type Class<?> memberType = getMemberType(); if (!(Compass.class.isAssignableFrom(memberType) || CompassSession.class.isAssignableFrom(memberType))) { throw new IllegalArgumentException("Cannot inject " + member + ": not a supported Compass type"); } } public void inject(Object instance) { Object value = resolve(); try { if (!this.member.isAccessible()) { this.member.setAccessible(true); } if (this.member instanceof Field) { ((Field) this.member).set(instance, value); } else if (this.member instanceof Method) { ((Method) this.member).invoke(instance, value); } else { throw new IllegalArgumentException("Cannot inject unknown AccessibleObject type " + this.member); } } catch (IllegalAccessException ex) { throw new IllegalArgumentException("Cannot inject member " + this.member, ex); } catch (InvocationTargetException ex) { // Method threw an exception throw new IllegalArgumentException("Attempt to inject setter method " + this.member + " resulted in an exception", ex); } } /** * Return the type of the member, whether it's a field or a method. */ public Class<?> getMemberType() { if (member instanceof Field) { return ((Field) member).getType(); } else if (member instanceof Method) { Method setter = (Method) member; if (setter.getParameterTypes().length != 1) { throw new IllegalArgumentException( "Supposed setter " + this.member + " must have 1 argument, not " + setter.getParameterTypes().length); } return setter.getParameterTypes()[0]; } else { throw new IllegalArgumentException( "Unknown AccessibleObject type " + this.member.getClass() + "; Can only inject settermethods or fields"); } } /** * Resolve the object against the application context. */ protected Object resolve() { // Resolves to Compass or CompassSession. Compass compass = findEntityManagerFactoryByName(this.name); if (Compass.class.isAssignableFrom(getMemberType())) { if (!getMemberType().isInstance(compass)) { throw new IllegalArgumentException("Cannot inject " + this.member + " with Compass [" + this.name + "]: type mismatch"); } return compass; } else { // We need to inject aa CompassSession. return Proxy.newProxyInstance( CompassContextBeanPostProcessor.class.getClassLoader(), new Class[]{InternalCompassSession.class}, new CompassSessionTransactionalProxy(compass)); } } } }