/*
* Copyright 2013 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.springframework.xd.module.options;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader;
import org.springframework.beans.factory.support.AbstractBeanDefinitionReader;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.xd.module.CompositeModuleDefinition;
import org.springframework.xd.module.ModuleDefinition;
import org.springframework.xd.module.SimpleModuleDefinition;
import org.springframework.xd.module.core.ResourceConfiguredModule;
import org.springframework.xd.module.options.spi.Mixin;
import org.springframework.xd.module.support.ModuleUtils;
import org.springframework.xd.module.support.ParentLastURLClassLoader;
/**
* The default implementation of {@link ModuleOptionsMetadataResolver} that deals with simple modules and reads the
* companion {@code .properties} file sitting next to the module definition.
*
* <p>
* The following strategies will be applied in turn:
* <ul>
* <li>look for a file named {@code <modulename>.properties} next to the module xml definition file</li>
* <li>if that file exists
* <ul>
* <li>look for an {@value #OPTIONS_CLASS} property. If found, use a
* {@link PojoModuleOptionsMetadata} backed by that POJO class name</li>
* <li>use a {@link SimpleModuleOptionsMetadata} backed by keys of the form {@code options.<name>.description}.
* Additionally, one can provide {@code options.<name>.default} and {@code options.<name>.type} properties.</li>
* </ul>
* <li>return an instance of {@link PassthruModuleOptionsMetadata}.
* <ul>
*
* @author Eric Bottard
*/
public class DefaultModuleOptionsMetadataResolver implements ModuleOptionsMetadataResolver, ResourceLoaderAware {
private static final Pattern DESCRIPTION_KEY_PATTERN = Pattern.compile("^options\\.([a-zA-Z\\-_0-9]+)\\.description$");
/**
* Name of the property containing a POJO fully qualified classname, which will be used to create a
* {@link PojoModuleOptionsMetadata}.
*/
public static final String OPTIONS_CLASS = "options_class";
private static final Map<String, Class<?>> SHORT_CLASSNAMES = new HashMap<String, Class<?>>();
static {
SHORT_CLASSNAMES.put("String", String.class);
SHORT_CLASSNAMES.put("boolean", boolean.class);
SHORT_CLASSNAMES.put("Boolean", Boolean.class);
SHORT_CLASSNAMES.put("int", int.class);
SHORT_CLASSNAMES.put("Integer", Integer.class);
SHORT_CLASSNAMES.put("long", long.class);
SHORT_CLASSNAMES.put("Long", Long.class);
SHORT_CLASSNAMES.put("float", float.class);
SHORT_CLASSNAMES.put("Float", Float.class);
SHORT_CLASSNAMES.put("double", double.class);
SHORT_CLASSNAMES.put("Double", Double.class);
}
private ConversionService conversionService;
/**
* The resolver to delegate to when building metadata for a composite module.
*/
private ModuleOptionsMetadataResolver compositeResolver = this;
private final DefaultModuleOptionsMetadataCollector defaultModuleOptionsMetadataCollector = new DefaultModuleOptionsMetadataCollector();
private ResourcePatternResolver resourceLoader = new PathMatchingResourcePatternResolver();
/**
* Construct a new {@link DefaultModuleOptionsMetadataResolver} that will use the provided conversion service when
* converting from String to rich object (supported for {@link PojoModuleOptionsMetadata} only).
*/
public DefaultModuleOptionsMetadataResolver(ConversionService conversionService) {
this.conversionService = conversionService;
}
/**
* Construct a new {@link DefaultModuleOptionsMetadataResolver}, using no particular conversion service for
* {@link PojoModuleOptionsMetadata}.
*/
public DefaultModuleOptionsMetadataResolver() {
this(null);
}
public void setCompositeResolver(ModuleOptionsMetadataResolver compositeResolver) {
this.compositeResolver = compositeResolver;
}
private ModuleOptionsMetadata makeSimpleModuleOptions(Properties props) {
SimpleModuleOptionsMetadata result = new SimpleModuleOptionsMetadata();
for (Object key : props.keySet()) {
if (key instanceof String) {
String propName = (String) key;
Matcher matcher = DESCRIPTION_KEY_PATTERN.matcher(propName);
if (!matcher.matches()) {
continue;
}
String optionName = matcher.group(1);
String description = props.getProperty(propName);
Object defaultValue = props.getProperty(String.format("options.%s.default", optionName));
String type = props.getProperty(String.format("options.%s.type", optionName));
Class<?> clazz = null;
if (type != null) {
if (SHORT_CLASSNAMES.containsKey(type)) {
clazz = SHORT_CLASSNAMES.get(type);
}
else {
try {
clazz = Class.forName(type);
}
catch (ClassNotFoundException e) {
throw new IllegalStateException("Can't find class used for type of option '"
+ optionName
+ "': " + type);
}
}
}
ModuleOption moduleOption = new ModuleOption(optionName, description).withDefaultValue(
defaultValue).withType(clazz);
result.add(moduleOption);
}
}
return result;
}
@Override
public ModuleOptionsMetadata resolve(ModuleDefinition definition) {
if (!definition.isComposed()) {
return resolveNormalMetadata((SimpleModuleDefinition) definition);
}
else {
return resolveComposedModuleMetadata((CompositeModuleDefinition)definition);
}
}
private ModuleOptionsMetadata resolveComposedModuleMetadata(CompositeModuleDefinition definition) {
Map<String, ModuleOptionsMetadata> hierarchy = new HashMap<String, ModuleOptionsMetadata>();
for (ModuleDefinition subModuleDefinition : definition.getChildren()) {
ModuleOptionsMetadata subMetadata = compositeResolver.resolve(subModuleDefinition);
// TODO: should be .getAlias() instead of name
hierarchy.put(subModuleDefinition.getName(), subMetadata);
}
return new HierarchicalCompositeModuleOptionsMetadata(hierarchy);
}
private ModuleOptionsMetadata resolveNormalMetadata(SimpleModuleDefinition definition) {
try {
Resource moduleLocation = resourceLoader.getResource(definition.getLocation());
ClassLoader classLoaderToUse = ModuleUtils.createModuleClassLoader(moduleLocation, ModuleOptionsMetadataResolver.class.getClassLoader());
Resource propertiesResource = ModuleUtils.modulePropertiesFile(definition, classLoaderToUse);
if (propertiesResource == null) {
return inferModuleOptionsMetadata(definition, classLoaderToUse);
}
else {
Properties props = new Properties();
props.load(propertiesResource.getInputStream());
String pojoClass = props.getProperty(OPTIONS_CLASS);
if (pojoClass != null) {
List<ModuleOptionsMetadata> mixins = new ArrayList<ModuleOptionsMetadata>();
createPojoOptionsMetadata(classLoaderToUse, pojoClass, mixins);
return mixins.size() == 1 ? mixins.get(0) : new FlattenedCompositeModuleOptionsMetadata(mixins);
}
else {
return makeSimpleModuleOptions(props);
}
}
}
catch (IOException e) {
return new PassthruModuleOptionsMetadata();
}
}
/**
* Create {@link PojoModuleOptionsMetadata} out of the {@code pojoClass} and add them to the list.
* Also, recursively invoke itself with all classes mixed in {@code pojoClass} (that is,
* if {@code pojoClass} bears the {@link Mixin} annotation.
*/
private void createPojoOptionsMetadata(ClassLoader classLoaderToUse, String pojoClass,
List<ModuleOptionsMetadata> mixins) {
try {
Class<?> clazz = Class.forName(pojoClass, true, classLoaderToUse);
Mixin mixin = clazz.getAnnotation(Mixin.class);
if (mixin != null) {
for (Class<?> classToMixin : mixin.value()) {
createPojoOptionsMetadata(classLoaderToUse, classToMixin.getName(), mixins);
}
}
mixins.add(new PojoModuleOptionsMetadata(clazz, conversionService));
}
catch (ClassNotFoundException e) {
throw new IllegalStateException("Unable to load class used by ModuleOptionsMetadata: "
+ pojoClass, e);
}
}
/**
* Will parse the module xml definition file, looking for "${foo}" placeholders and advertise a {@link ModuleOption}
* for each of those.
*
* Note that this may end up in false positives and does not convey much information.
*/
private ModuleOptionsMetadata inferModuleOptionsMetadata(SimpleModuleDefinition definition, ClassLoader classLoaderToUse) {
final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
Resource source = ResourceConfiguredModule.resourceBasedConfigurationFile(definition, classLoaderToUse);
if (source == null) {
return new PassthruModuleOptionsMetadata();
}
AbstractBeanDefinitionReader reader = source.getFilename().endsWith("xml") ?
new XmlBeanDefinitionReader(beanFactory):
new GroovyBeanDefinitionReader(beanFactory);
reader.setResourceLoader(new PathMatchingResourcePatternResolver(classLoaderToUse));
reader.loadBeanDefinitions(source);
return defaultModuleOptionsMetadataCollector.collect(beanFactory);
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = (ResourcePatternResolver) resourceLoader;
}
}