/* * 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; import java.io.File; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.HashSet; import java.util.Map; import java.util.Properties; import java.util.Set; import javax.sql.DataSource; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.compass.core.Compass; import org.compass.core.CompassException; import org.compass.core.config.CompassConfiguration; import org.compass.core.config.CompassConfigurationFactory; import org.compass.core.config.CompassEnvironment; import org.compass.core.config.InputStreamMappingResolver; import org.compass.core.converter.Converter; import org.compass.core.lucene.LuceneEnvironment; import org.compass.core.lucene.engine.store.jdbc.ExternalDataSourceProvider; import org.compass.core.spi.InternalCompass; import org.compass.core.util.ClassUtils; import org.compass.spring.transaction.SpringSyncTransactionFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.io.Resource; import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; import org.springframework.transaction.PlatformTransactionManager; /** * @author kimchy */ public class LocalCompassBean implements FactoryBean, InitializingBean, DisposableBean, BeanNameAware, ApplicationContextAware, BeanClassLoaderAware { protected static final Log log = LogFactory.getLog(LocalCompassBean.class); private Resource connection; private Resource configLocation; private String mappingScan; private Resource[] configLocations; private Resource[] resourceLocations; private Resource[] resourceJarLocations; private Resource[] resourceDirectoryLocations; private String[] classMappings; private InputStreamMappingResolver[] mappingResolvers; private Properties compassSettings; private Map<String, Object> settings; private DataSource dataSource; private PlatformTransactionManager transactionManager; private Map<String, Converter> convertersByName; private Compass compass; private String beanName; private ClassLoader classLoader; private ApplicationContext applicationContext; private CompassConfiguration config; private LocalCompassBeanPostProcessor postProcessor; /** * Allows to register a post processor for the Compass configuration. */ public void setPostProcessor(LocalCompassBeanPostProcessor postProcessor) { this.postProcessor = postProcessor; } public void setBeanName(String beanName) { this.beanName = beanName; } public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } public void setBeanClassLoader(ClassLoader classLoader) { this.classLoader = classLoader; } /** * Sets an optional connection based on Spring <code>Resource</code> * abstraction. Will be used if none is set as part of other possible * configuration of Compass connection. * <p/> * Will use <code>Resource#getFile</code> in order to get the absolute * path. */ public void setConnection(Resource connection) { this.connection = connection; } /** * Set the location of the Compass XML config file, for example as classpath * resource "classpath:compass.cfg.xml". * <p/> * Note: Can be omitted when all necessary properties and mapping resources * are specified locally via this bean. */ public void setConfigLocation(Resource configLocation) { this.configLocation = configLocation; } /** * Set the location of the Compass XML config file, for example as classpath * resource "classpath:compass.cfg.xml". * <p/> * Note: Can be omitted when all necessary properties and mapping resources * are specified locally via this bean. */ public void setConfigLocations(Resource[] configLocations) { this.configLocations = configLocations; } /** * @see org.compass.core.config.CompassConfiguration#addScan(String) */ public void setMappingScan(String basePackage) { this.mappingScan = basePackage; } public void setCompassSettings(Properties compassSettings) { this.compassSettings = compassSettings; } public void setSettings(Map<String, Object> settings) { this.settings = settings; } /** * Set locations of Compass resource files (mapping and common metadata), * for example as classpath resource "classpath:example.cpm.xml". Supports * any resource location via Spring's resource abstraction, for example * relative paths like "WEB-INF/mappings/example.hbm.xml" when running in an * application context. * <p/> * Can be used to add to mappings from a Compass XML config file, or to * specify all mappings locally. */ public void setResourceLocations(Resource[] resourceLocations) { this.resourceLocations = resourceLocations; } /** * Set locations of jar files that contain Compass resources, like * "WEB-INF/lib/example.jar". * <p/> * Can be used to add to mappings from a Compass XML config file, or to * specify all mappings locally. */ public void setResourceJarLocations(Resource[] resourceJarLocations) { this.resourceJarLocations = resourceJarLocations; } /** * Set locations of directories that contain Compass mapping resources, like * "WEB-INF/mappings". * <p/> * Can be used to add to mappings from a Compass XML config file, or to * specify all mappings locally. */ public void setResourceDirectoryLocations(Resource[] resourceDirectoryLocations) { this.resourceDirectoryLocations = resourceDirectoryLocations; } /** * Sets the fully qualified class names for mappings. Useful when using annotations * for example. Will also try to load the matching "[Class].cpm.xml" file. */ public void setClassMappings(String[] classMappings) { this.classMappings = classMappings; } /** * Sets the mapping resolvers the resolved Compass mapping definitions. */ public void setMappingResolvers(InputStreamMappingResolver[] mappingResolvers) { this.mappingResolvers = mappingResolvers; } /** * Sets a <code>DataSource</code> to be used when the index is stored within a database. * The data source must be used with {@link org.compass.core.lucene.engine.store.jdbc.ExternalDataSourceProvider} * for externally configured data sources (such is the case some of the time with spring). If set, Compass data source provider * does not have to be set, since it will automatically default to <code>ExternalDataSourceProvider</code>. If the * compass data source provider is set as a compass setting, it will be used. * <p/> * Note, that it will be automatically wrapped with Spring's <literal>TransactionAwareDataSourceProxy</literal> if not * already wrapped by one. * {@link org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy}. * <p/> * Also note that setting the data source is not enough to configure Compass to store the index * within the database, the Compass connection string should also be set to <code>jdbc://</code>. */ public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; if (!(dataSource instanceof TransactionAwareDataSourceProxy)) { this.dataSource = new TransactionAwareDataSourceProxy(dataSource); } } /** * Sets Spring <code>PlatformTransactionManager</code> to be used with compass. If using * {@link org.compass.spring.transaction.SpringSyncTransactionFactory}, it must be set. */ public void setTransactionManager(PlatformTransactionManager transactionManager) { this.transactionManager = transactionManager; } /** * Sets a map of global converters to be registered with compass. The map key will be * the name that the converter will be registered against, and the value should be the * Converter itself (natuarally configured using spring DI). */ public void setConvertersByName(Map<String, Converter> convertersByName) { this.convertersByName = convertersByName; } public void setCompassConfiguration(CompassConfiguration config) { this.config = config; } public void afterPropertiesSet() throws Exception { CompassConfiguration config = this.config; if (config == null) { config = newConfiguration(); } if (classLoader != null) { config.setClassLoader(getClassLoader()); } if (this.configLocation != null) { config.configure(this.configLocation.getURL()); } if (this.configLocations != null) { for (Resource configLocation1 : configLocations) { config.configure(configLocation1.getURL()); } } if (this.mappingScan != null) { config.addScan(this.mappingScan); } if (this.compassSettings != null) { config.getSettings().addSettings(this.compassSettings); } if (this.settings != null) { config.getSettings().addSettings(this.settings); } if (resourceLocations != null) { for (Resource resourceLocation : resourceLocations) { config.addInputStream(resourceLocation.getInputStream(), resourceLocation.getFilename()); } } if (resourceJarLocations != null) { for (Resource resourceJarLocation : resourceJarLocations) { config.addJar(resourceJarLocation.getFile()); } } if (classMappings != null) { for (String classMapping : classMappings) { config.addClass(ClassUtils.forName(classMapping, getClassLoader())); } } if (resourceDirectoryLocations != null) { for (Resource resourceDirectoryLocation : resourceDirectoryLocations) { File file = resourceDirectoryLocation.getFile(); if (!file.isDirectory()) { throw new IllegalArgumentException("Resource directory location [" + resourceDirectoryLocation + "] does not denote a directory"); } config.addDirectory(file); } } if (mappingResolvers != null) { for (InputStreamMappingResolver mappingResolver : mappingResolvers) { config.addMappingResolver(mappingResolver); } } if (dataSource != null) { ExternalDataSourceProvider.setDataSource(dataSource); if (config.getSettings().getSetting(LuceneEnvironment.JdbcStore.DataSourceProvider.CLASS) == null) { config.getSettings().setSetting(LuceneEnvironment.JdbcStore.DataSourceProvider.CLASS, ExternalDataSourceProvider.class.getName()); } } String compassTransactionFactory = config.getSettings().getSetting(CompassEnvironment.Transaction.FACTORY); if (compassTransactionFactory == null && transactionManager != null) { // if the transaciton manager is set and a transcation factory is not set, default to the SpringSync one. config.getSettings().setSetting(CompassEnvironment.Transaction.FACTORY, SpringSyncTransactionFactory.class.getName()); } if (compassTransactionFactory != null && compassTransactionFactory.equals(SpringSyncTransactionFactory.class.getName())) { if (transactionManager == null) { throw new IllegalArgumentException("When using SpringSyncTransactionFactory the transactionManager property must be set"); } } SpringSyncTransactionFactory.setTransactionManager(transactionManager); if (convertersByName != null) { for (Map.Entry<String, Converter> entry : convertersByName.entrySet()) { config.registerConverter(entry.getKey(), entry.getValue()); } } if (config.getSettings().getSetting(CompassEnvironment.NAME) == null) { config.getSettings().setSetting(CompassEnvironment.NAME, beanName); } if (config.getSettings().getSetting(CompassEnvironment.CONNECTION) == null && connection != null) { config.getSettings().setSetting(CompassEnvironment.CONNECTION, connection.getFile().getAbsolutePath()); } if (applicationContext != null) { String[] names = applicationContext.getBeanNamesForType(PropertyPlaceholderConfigurer.class); for (String name : names) { try { PropertyPlaceholderConfigurer propConfigurer = (PropertyPlaceholderConfigurer) applicationContext.getBean(name); Method method = findMethod(propConfigurer.getClass(), "mergeProperties"); method.setAccessible(true); Properties props = (Properties) method.invoke(propConfigurer); method = findMethod(propConfigurer.getClass(), "convertProperties", Properties.class); method.setAccessible(true); method.invoke(propConfigurer, props); method = findMethod(propConfigurer.getClass(), "parseStringValue", String.class, Properties.class, Set.class); method.setAccessible(true); String nullValue = null; try { Field field = propConfigurer.getClass().getDeclaredField("nullValue"); field.setAccessible(true); nullValue = (String) field.get(propConfigurer); } catch (NoSuchFieldException e) { // no field (old spring version) } for (Map.Entry entry : config.getSettings().getProperties().entrySet()) { String key = (String) entry.getKey(); String value = (String) entry.getValue(); value = (String) method.invoke(propConfigurer, value, props, new HashSet()); config.getSettings().setSetting(key, value.equals(nullValue) ? null : value); } } catch (Exception e) { log.debug("Failed to apply property placeholder defined in bean [" + name + "]", e); } } } if (postProcessor != null) { postProcessor.process(config); } this.compass = newCompass(config); this.compass = (Compass) Proxy.newProxyInstance(SpringCompassInvocationHandler.class.getClassLoader(), new Class[]{InternalCompass.class}, new SpringCompassInvocationHandler(this.compass)); } protected CompassConfiguration newConfiguration() { return CompassConfigurationFactory.newConfiguration(); } protected Compass newCompass(CompassConfiguration config) throws CompassException { return config.buildCompass(); } public Object getObject() throws Exception { return this.compass; } public Class getObjectType() { return (compass != null) ? compass.getClass() : Compass.class; } public boolean isSingleton() { return true; } public void destroy() throws Exception { this.compass.close(); } protected ClassLoader getClassLoader() { if (classLoader != null) { return classLoader; } return Thread.currentThread().getContextClassLoader(); } private Method findMethod(Class clazz, String methodName, Class ... parameterTypes) { if (clazz.equals(Object.class)) { return null; } try { return clazz.getDeclaredMethod(methodName, parameterTypes); } catch (NoSuchMethodException e) { return findMethod(clazz.getSuperclass(), methodName, parameterTypes); } } /** * Invocation handler that handles close methods. */ private class SpringCompassInvocationHandler implements InvocationHandler { private static final String GET_TARGET_COMPASS_METHOD_NAME = "getTargetCompass"; private static final String CLONE_METHOD = "clone"; private Compass targetCompass; public SpringCompassInvocationHandler(Compass targetCompass) { this.targetCompass = targetCompass; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Invocation on ConnectionProxy interface coming in... if (method.getName().equals(GET_TARGET_COMPASS_METHOD_NAME)) { return compass; } if (method.getName().equals(CLONE_METHOD) && args.length == 1) { if (dataSource != null) { ExternalDataSourceProvider.setDataSource(dataSource); } SpringSyncTransactionFactory.setTransactionManager(transactionManager); } // Invoke method on target connection. try { return method.invoke(targetCompass, args); } catch (InvocationTargetException ex) { throw ex.getTargetException(); } } } }