/*
* 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.core.config;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.compass.annotations.config.binding.AnnotationsMappingBinding;
import org.compass.annotations.config.binding.OverrideAnnotationsWithJsonCpmMappingBinding;
import org.compass.annotations.config.binding.OverrideAnnotationsWithXmlCpmMappingBinding;
import org.compass.core.Compass;
import org.compass.core.CompassException;
import org.compass.core.config.binding.JsonMetaDataBinding;
import org.compass.core.config.binding.JsonPlainMappingBinding;
import org.compass.core.config.binding.XmlMetaDataBinding;
import org.compass.core.config.binding.XmlPlainMappingBinding;
import org.compass.core.config.binding.scanner.Filter;
import org.compass.core.config.binding.scanner.ScanItem;
import org.compass.core.config.binding.scanner.Scanner;
import org.compass.core.config.binding.scanner.ScannerFactoy;
import org.compass.core.config.builder.ConfigurationBuilder;
import org.compass.core.config.builder.SmartConfigurationBuilder;
import org.compass.core.config.process.MappingProcessor;
import org.compass.core.converter.Converter;
import org.compass.core.converter.ConverterLookup;
import org.compass.core.converter.DefaultConverterLookup;
import org.compass.core.engine.naming.DefaultPropertyNamingStrategyFactory;
import org.compass.core.engine.naming.PropertyNamingStrategy;
import org.compass.core.engine.naming.PropertyNamingStrategyFactory;
import org.compass.core.executor.DefaultExecutorManager;
import org.compass.core.impl.DefaultCompass;
import org.compass.core.impl.RefreshableCompass;
import org.compass.core.mapping.CompassMapping;
import org.compass.core.mapping.ContractMapping;
import org.compass.core.mapping.ContractMappingProvider;
import org.compass.core.mapping.MappingException;
import org.compass.core.mapping.ResourceMapping;
import org.compass.core.mapping.ResourceMappingProvider;
import org.compass.core.mapping.internal.DefaultCompassMapping;
import org.compass.core.mapping.internal.InternalCompassMapping;
import org.compass.core.metadata.CompassMetaData;
import org.compass.core.metadata.impl.DefaultCompassMetaData;
import org.compass.core.util.ClassUtils;
import org.compass.core.util.matcher.AntPathMatcher;
import org.compass.core.util.matcher.PathMatcher;
/**
* Used to configure <code>Compass</code> instances.
* <p/>
* An instance of it allows the application to specify settings and mapping
* files to be used when creating <code>Compass</code>.
* </p>
* <p/>
* There are several options to configure a <code>Compass</code> instance,
* programmatically using the <code>CompassConfiguration</code> class, using
* the xml configuration file (compass.cfg.xml), or a json configuration file, or a combination of all.
* </p>
* <p/>
* Usually the application will create a single
* <code>CompassConfiguration</code>, use it to configure and than build a
* <code>Compass</code> instance, and than instantiate
* <code>CompassSession</code>s in threads servicing client requests.
* </p>
* <p/>
* The <code>CompassConfiguration</code> is meant only as an
* initialization-time object. <code>Compass</code> is immutable and do not
* affect the <code>CompassConfiguration</code> that created it.
* </p>
*
* @author kimchy
* @see org.compass.core.Compass
*/
public class CompassConfiguration {
protected static final Log log = LogFactory.getLog(CompassConfiguration.class);
private CompassMetaData metaData;
private InternalCompassMapping mapping;
private CompassSettings settings;
private ClassLoader classLoader;
protected CompassMappingBinding mappingBinding;
protected ConfigurationBuilder configurationBuilder = new SmartConfigurationBuilder();
private HashMap<String, ConverterHolder> temporaryConvertersByName = new HashMap<String, ConverterHolder>();
public CompassConfiguration() {
mapping = new DefaultCompassMapping();
metaData = new DefaultCompassMetaData();
settings = new CompassSettings();
}
private CompassMappingBinding getMappingBinding() {
if (mappingBinding == null) {
mappingBinding = new CompassMappingBinding();
addMappingBindings(mappingBinding);
mappingBinding.setUpBinding(mapping, metaData, settings);
}
return mappingBinding;
}
protected void addMappingBindings(CompassMappingBinding mappingBinding) {
mappingBinding.addMappingBinding(new XmlMetaDataBinding());
mappingBinding.addMappingBinding(new JsonMetaDataBinding());
mappingBinding.addMappingBinding(new XmlPlainMappingBinding());
mappingBinding.addMappingBinding(new JsonPlainMappingBinding());
mappingBinding.addMappingBinding(new AnnotationsMappingBinding());
mappingBinding.addMappingBinding(new OverrideAnnotationsWithXmlCpmMappingBinding());
mappingBinding.addMappingBinding(new OverrideAnnotationsWithJsonCpmMappingBinding());
}
/**
* Sets the class loader that will be used to load classes and resources.
*/
public CompassConfiguration setClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
this.settings.setClassLoader(classLoader);
return this;
}
/**
* Returns the class loader that will be used to load classes and resources. If directly
* set, will return it. If not, will return the thread local context class loader.
*/
public ClassLoader getClassLoader() {
if (this.classLoader == null) {
return Thread.currentThread().getContextClassLoader();
}
return this.classLoader;
}
/**
* Returns the current set of settings associated with the configuration.
*
* @return The settings used by the configuration
*/
public CompassSettings getSettings() {
return settings;
}
/**
* Sets a specific setting in the compass configuration settings.
*
* @param setting The setting name
* @param value The setting value
* @return <code>CompassConfiguration</code> for method chaining
*/
public CompassConfiguration setSetting(String setting, Object value) {
settings.setObjectSetting(setting, value);
return this;
}
/**
* Sets the connection for the compass instance.
*
* @param connection The connection for compass to use
* @return <code>CompassConfiguration</code> for method chaining
*/
public CompassConfiguration setConnection(String connection) {
settings.setSetting(CompassEnvironment.CONNECTION, connection);
return this;
}
/**
* Registers a {@link Converter} under the given name. The name can then be used in the mapping
* definitions as a logical name to the converter.
*
* @param converterName the converter name the converter will be registered under
* @param converter The converter to use
* @return The configuration
*/
public CompassConfiguration registerConverter(String converterName, Converter converter) {
this.temporaryConvertersByName.put(converterName, new ConverterHolder(converter));
return this;
}
/**
* Regsiters a {@link Converter} under the given name. This converter will apply to all the given
* types that match the given type.
*
* @param converterName The name of the converter
* @param type The type to register the converter for
* @param converter The converter
* @return The configuration
*/
public CompassConfiguration registerConverter(String converterName, Class type, Converter converter) {
this.temporaryConvertersByName.put(converterName, new ConverterHolder(type, converter));
return this;
}
/**
* Build compass with the configurations set. Creates a copy of all the
* current settings and mappings, configures a {@link Compass} instance and
* starts it.
* <p/>
* Note that the <code>CompassConfiguration</code> class can be used to
* create more Compass objects after the method has been called.
* </p>
*
* @return the Compass
*/
public Compass buildCompass() throws CompassException {
CompassSettings copySettings = settings.copy();
copySettings.setClassLoader(getClassLoader());
// add any mappings set in the properties
for (Iterator it = settings.keySet().iterator(); it.hasNext();) {
String setting = (String) it.next();
if (setting.startsWith(CompassEnvironment.Mapping.MAPPING_PREFIX)) {
String mapping = settings.getSetting(setting);
if (mapping.endsWith("cpm.xml") || mapping.endsWith("cmd.xml")) {
addResource(mapping);
} else {
try {
addClass(ClassUtils.forName(mapping, copySettings.getClassLoader()));
} catch (ClassNotFoundException e) {
throw new CompassException("Failed to find class [" + mapping + "]");
}
}
settings.removeSetting(setting);
}
}
// add any scans
Map<String, CompassSettings> scanGroups = settings.getSettingGroups(CompassEnvironment.Mapping.SCAN_MAPPING_PREFIX);
for (Map.Entry<String, CompassSettings> entry : scanGroups.entrySet()) {
String packageSetting = entry.getValue().getSetting(CompassEnvironment.Mapping.SCAN_MAPPING_PACKAGE);
if (packageSetting == null) {
throw new ConfigurationException("[" + CompassEnvironment.Mapping.SCAN_MAPPING_PACKAGE + "] must be set when scanning for [" + entry.getKey() + "] scan");
}
addScan(packageSetting, entry.getValue().getSetting(CompassEnvironment.Mapping.SCAN_MAPPING_PATTERN));
}
ConverterLookup converterLookup = new DefaultConverterLookup();
registerExtraConverters(converterLookup);
converterLookup.configure(copySettings);
for (String converterName : temporaryConvertersByName.keySet()) {
ConverterHolder converterHolder = temporaryConvertersByName.get(converterName);
if (converterHolder.type == null) {
converterLookup.registerConverter(converterName, converterHolder.converter);
} else {
converterLookup.registerConverter(converterName, converterHolder.converter, converterHolder.type);
}
}
CompassMapping copyCompassMapping = mapping.copy(converterLookup);
PropertyNamingStrategyFactory propertyNamingStrategyFactory = new DefaultPropertyNamingStrategyFactory();
PropertyNamingStrategy propertyNamingStrategy = propertyNamingStrategyFactory.createNamingStrategy(copySettings);
MappingProcessor mappingProcessor = new CompassMappingProcessor();
mappingProcessor.process(copyCompassMapping, propertyNamingStrategy, converterLookup, copySettings);
CompassMetaData copyMetaData = metaData.copy();
DefaultExecutorManager executorManager = new DefaultExecutorManager();
executorManager.configure(settings);
return new RefreshableCompass(this,
new DefaultCompass(copyCompassMapping, converterLookup, copyMetaData, propertyNamingStrategy, executorManager, copySettings));
}
protected void registerExtraConverters(ConverterLookup converterLookup) {
}
/**
* Use the mappings and properties specified in an application resource with
* the path <code>/compass.cfg.xml</code>.
*
* @return <code>CompassConfiguration</code> for method chaining
*/
public CompassConfiguration configure() throws ConfigurationException {
configure("/compass.cfg.xml");
return this;
}
/**
* Use the mappings and properties specified in the given application
* resource.
*
* @param resource The compass configuration resource path
* @return <code>CompassConfiguration</code> for method chaining
*/
public CompassConfiguration configure(String resource) throws ConfigurationException {
log.info("Configuring from resource [" + resource + "]");
configurationBuilder.configure(resource, this);
return this;
}
/**
* Use the mappings and properties specified in the given document.
*
* @param url URL from which you wish to load the configuration
* @return A configuration configured via the file
* @throws ConfigurationException
*/
public CompassConfiguration configure(URL url) throws ConfigurationException {
log.info("Configuring from url [" + url.toExternalForm() + "]");
configurationBuilder.configure(url, this);
return this;
}
/**
* Use the mappings and properties specified in the given application file.
*
* @param configFile <code>File</code> from which you wish to load the
* configuration
* @return A configuration configured via the file
* @throws ConfigurationException
*/
public CompassConfiguration configure(File configFile) throws ConfigurationException {
log.info("Configuring from file [" + configFile.getAbsolutePath() + "]");
configurationBuilder.configure(configFile, this);
return this;
}
/**
* Advance: Add mappings based on {@link org.compass.core.mapping.ContractMapping}
* implementation which allows for adding pre built mapping constructs.
*/
public CompassConfiguration addMapping(ContractMapping contractMapping) {
boolean hasAddedResource = getMappingBinding().addContractMaping(contractMapping);
if (!hasAddedResource) {
throw new ConfigurationException("No mapping match contract mapping [" + contractMapping.getAlias() + "]");
}
if (log.isInfoEnabled()) {
log.info("Adding Contract Mapping [" + contractMapping.getAlias() + "]");
}
return this;
}
/**
* Allows to provide contract mapping through a level of indiraction.
*/
public CompassConfiguration addMapping(ContractMappingProvider contractMappingProvider) {
return addMapping(contractMappingProvider.getMapping());
}
/**
* Advance: Add mappings based on {@link org.compass.core.mapping.ResourceMapping}
* implementation which allows for adding pre built mapping constructs.
*/
public CompassConfiguration addMapping(ResourceMapping resourceMapping) {
boolean hasAddedResource = getMappingBinding().addResourceMapping(resourceMapping);
if (!hasAddedResource) {
throw new ConfigurationException("No mapping match resource mapping [" + resourceMapping.getAlias() + "]");
}
if (log.isInfoEnabled()) {
log.info("Adding Resource Mapping [" + resourceMapping.getAlias() + "]");
}
return this;
}
/**
* Allows to provide resource mapping through a level of indiraction.
*/
public CompassConfiguration addMapping(ResourceMappingProvider resourceMappingProvider) {
return addMapping(resourceMappingProvider.getMapping());
}
/**
* Removes the mapping registered under the given alias.
*/
public CompassConfiguration removeMappingByAlias(String alias) throws MappingException {
boolean removed = mapping.removeMappingByAlias(alias);
if (removed) {
if (log.isInfoEnabled()) {
log.info("Removing Resource Mapping with alias [" + alias + "]");
}
}
return this;
}
/**
* Removes all the mappings registered under the given class name.
*/
public CompassConfiguration removeMappingByClass(Class clazz) throws MappingException {
return removeMappingByClass(clazz.getName());
}
/**
* Removes all the mappings registered under the given class name.
*/
public CompassConfiguration removeMappingByClass(String className) throws MappingException {
boolean removed = mapping.removeMappingByClass(className);
if (removed) {
if (log.isInfoEnabled()) {
log.info("Removing Resource Mappings with class [" + className + "]");
}
}
return this;
}
/**
* Uses a class that implements the {@link InputStreamMappingResolver} for auto
* generation of mapping definitions.
*
* @param mappingResolver The mapping resolver
*/
public CompassConfiguration addMappingResolver(InputStreamMappingResolver mappingResolver) throws ConfigurationException {
boolean hasAddedResource = getMappingBinding().addMappingResolver(mappingResolver);
if (!hasAddedResource) {
throw new ConfigurationException("No mapping match mapping resolver [" + mappingResolver + "]");
}
if (log.isInfoEnabled()) {
log.info("Mapping resolver [" + mappingResolver + "]");
}
return this;
}
/**
* Read mappings from an application resource
*
* @param path a resource
* @param classLoader a <code>ClassLoader</code> to use
*/
public CompassConfiguration addResource(String path, ClassLoader classLoader) throws ConfigurationException {
boolean hasAddedResource = getMappingBinding().addResource(path, classLoader);
if (!hasAddedResource) {
throw new ConfigurationException("No mapping match resource [" + path + "] and class loader [" + classLoader + "]");
}
if (log.isInfoEnabled()) {
log.info("Mapping resource [" + path + "] from class loader [" + classLoader + "]");
}
return this;
}
/**
* Read mappings from an application resource trying different classloaders.
* This method will try to load the resource first from the thread context
* classloader and then from the classloader that loaded Compass.
*
* @param path The path of the resource
*/
public CompassConfiguration addResource(String path) throws ConfigurationException {
boolean hasAddedResource = getMappingBinding().addResource(path, getClassLoader());
if (!hasAddedResource) {
throw new ConfigurationException("No mapping match resource [" + path + "]");
}
if (log.isInfoEnabled()) {
log.info("Mapping resource [" + path + "] in class loader");
}
return this;
}
/**
* Read mappings from a particular file.
*
* @param filePath a path to a file
*/
public CompassConfiguration addFile(String filePath) throws ConfigurationException {
boolean hasAddedResource = getMappingBinding().addFile(filePath);
if (!hasAddedResource) {
throw new ConfigurationException("No mapping match file [" + filePath + "]");
}
if (log.isInfoEnabled()) {
log.info("Mapping file [" + filePath + "]");
}
return this;
}
/**
* Read mappings from a particular file.
*
* @param file a path to a file
*/
public CompassConfiguration addFile(File file) throws ConfigurationException {
boolean hasAddedResource = getMappingBinding().addFile(file);
if (!hasAddedResource) {
throw new ConfigurationException("No mapping match file [" + file.getAbsolutePath() + "]");
}
if (log.isInfoEnabled()) {
log.info("Mapping file [" + file.getAbsolutePath() + "]");
}
return this;
}
/**
* Read annotated package definitions.
*
* @param packageName The package name to load
*/
public CompassConfiguration addPackage(String packageName) throws ConfigurationException {
boolean hasAddedResource = getMappingBinding().addPackage(packageName);
if (!hasAddedResource) {
throw new ConfigurationException("No mapping match package [" + packageName + "]");
}
if (log.isInfoEnabled()) {
log.info("Mapping package [" + packageName + "]");
}
return this;
}
/**
* Read a mapping from an application resource, using a convention. The
* class <code>foo.bar.Foo</code> is mapped by the file
* <code>foo/bar/Foo.cpm.xml</code> (in the case of Xml binding).
*
* @param searchableClass the mapped class
*/
public CompassConfiguration addClass(Class searchableClass) throws ConfigurationException {
boolean hasAddedResource = getMappingBinding().addClass(searchableClass);
if (!hasAddedResource) {
throw new ConfigurationException("No mapping match class [" + searchableClass.getName() + "]");
}
if (log.isInfoEnabled()) {
log.info("Mapping class [" + searchableClass + "]");
}
return this;
}
/**
* Scans the given base package recursivly for any applicable mappings definitions. This
* incldues xml mapping defintiions as well as annotations.
*/
public CompassConfiguration addScan(String basePackage) throws ConfigurationException {
return addScan(basePackage, null);
}
/**
* Scans the given base package recursivly for any applicable mappings definitions. This
* incldues xml mapping defintiions as well as annotations.
*
* <p>An optional ant style pattern can be provided to narrow down the search. For example,
* the base package can be <code>com.mycompany</code>, and the pattern can be <code>**/model/**</code>
* which will match all the everythign that has a package named model within it under the given base package.
*/
public CompassConfiguration addScan(String basePackage, final String pattern) throws ConfigurationException {
basePackage = basePackage.replace('.', '/');
Enumeration<URL> urls;
try {
urls = settings.getClassLoader().getResources(basePackage);
} catch (IOException e) {
throw new ConfigurationException("Failed to list resource for base package [" + basePackage + "]", e);
}
final PathMatcher matcher = new AntPathMatcher();
final boolean performMatch = pattern != null && matcher.isPattern(pattern);
Filter filter = new Filter() {
public boolean accepts(String name) {
for (String suffix : getMappingBinding().getSuffixes()) {
if (performMatch && !matcher.match(pattern, name)) {
return false;
}
if (name.endsWith(suffix)) {
return true;
}
}
return false;
}
};
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
Scanner scanner;
try {
scanner = ScannerFactoy.create(basePackage, url, filter);
} catch (IOException e) {
throw new ConfigurationException("Failed to create scan factory for basePackage [" + basePackage + "] and url [" + url + "]", e);
}
try {
ScanItem si;
while ((si = scanner.next()) != null) {
try {
getMappingBinding().addInputStream(si.getInputStream(), si.getName());
} finally {
si.close();
}
}
} finally {
scanner.close();
}
}
return this;
}
/**
* Tries to add a class and returns a boolean indicator if it was added or not.
*
* @param searchableClass The searchable class to add
* @return <code>true</code> if the class was added, <code>false</code> otherwise
* @throws ConfigurationException
*/
public boolean tryAddClass(Class searchableClass) throws ConfigurationException {
boolean hasAddedResource = getMappingBinding().addClass(searchableClass);
if (log.isInfoEnabled() && hasAddedResource) {
log.info("Mapping class [" + searchableClass + "]");
}
return hasAddedResource;
}
/**
* Read all mapping and meta-data documents from a directory tree. Assume
* that any file named <code>*.cpm.xml</code> or <code>*.cmd.xml</code>
* is a mapping document.
*
* @param dir a directory
*/
public CompassConfiguration addDirectory(File dir) throws ConfigurationException {
boolean hasAddedResource = getMappingBinding().addDirectory(dir);
if (!hasAddedResource) {
throw new ConfigurationException("No mapping match directory [" + dir.getAbsolutePath() + "]");
}
if (log.isInfoEnabled()) {
log.info("Mapping directory [" + dir.getAbsolutePath() + "]");
}
return this;
}
/**
* Read all mappings and meta-data from a jar file. Assume that any file
* named <code>*.cpm.xml</code> or <code>*.cmd.xml</code> is a mapping
* document.
*
* @param jar a jar file
*/
public CompassConfiguration addJar(File jar) throws ConfigurationException {
boolean hasAddedResource = getMappingBinding().addJar(jar);
if (!hasAddedResource) {
throw new ConfigurationException("No mapping match jar [" + jar.getAbsolutePath() + "]");
}
if (log.isInfoEnabled()) {
log.info("Mapping jar [" + jar.getName() + "]");
}
return this;
}
/**
* Read mappings from a <code>URL</code>.
*
* @param url the URL
*/
public CompassConfiguration addURL(URL url) throws ConfigurationException {
boolean hasAddedResource = getMappingBinding().addURL(url);
if (!hasAddedResource) {
throw new ConfigurationException("No mapping match URL [" + url.toExternalForm() + "]");
}
if (log.isInfoEnabled()) {
log.info("Mapping URL [" + url.toExternalForm() + "]");
}
return this;
}
/**
* Read mappings from an <code>InputStream</code>.
*
* @param inputStream an <code>InputStream</code> containing
*/
public CompassConfiguration addInputStream(InputStream inputStream, String resourceName)
throws ConfigurationException {
boolean hasAddedResource = getMappingBinding().addInputStream(inputStream, resourceName);
if (!hasAddedResource) {
throw new ConfigurationException("No mapping match input stream [" + resourceName + "]");
}
if (log.isInfoEnabled()) {
log.info("Mapping InputStream [" + resourceName + "]");
}
return this;
}
private class ConverterHolder {
Class type;
Converter converter;
public ConverterHolder(Converter converter) {
this.converter = converter;
}
private ConverterHolder(Class type, Converter converter) {
this.type = type;
this.converter = converter;
}
}
}