/* * 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.beans.PropertyDescriptor; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; import org.springframework.beans.InvalidPropertyException; import org.springframework.boot.bind.PropertySourcesPropertyValues; import org.springframework.core.convert.ConversionService; import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MutablePropertySources; import org.springframework.util.Assert; import org.springframework.validation.BindException; import org.springframework.validation.DataBinder; import org.springframework.validation.FieldError; import org.springframework.validation.beanvalidation.CustomValidatorBean; import org.springframework.xd.module.options.spi.ProfileNamesProvider; import org.springframework.xd.module.options.spi.ValidationGroupsProvider; /** * An implementation of {@link ModuleOptionsMetadata} that derives its information from a plain old java object: * <ul> * <li>public setters are reported as valid options</li> * <li>the type of the option is derived from the accepted type by the setter</li> * </ul> * * {@link ModuleOptions} for such a POJO will work as follows: * <ul> * <li>an instance of the class will be created reflectively and injected with user provided values,</li> * <li>reported properties are computed from all the getters,</li> * <li>the POJO may bear JSR303 validation annotations, which will be used to validate the interpolated options,</li> * <li>if the POJO implements {@link ProfileNamesProvider}, profile names will be gathered from a reflective call to * {@link ProfileNamesProvider#profilesToActivate()}</li> * </ul> * * @author Eric Bottard */ public class PojoModuleOptionsMetadata implements ModuleOptionsMetadata { private BeanWrapperImpl beanWrapper; private List<ModuleOption> options; /** * Used to perform conversion from String representation of options to actual arguments of setters. */ private final ConversionService conversionService; public PojoModuleOptionsMetadata(Class<?> clazz) { this(clazz, null); } public PojoModuleOptionsMetadata(Class<?> clazz, ConversionService conversionService) { this.conversionService = conversionService; beanWrapper = new BeanWrapperImpl(clazz); options = new ArrayList<ModuleOption>(); for (PropertyDescriptor pd : beanWrapper.getPropertyDescriptors()) { String name = pd.getName(); if (!beanWrapper.isWritableProperty(name)) { continue; } org.springframework.xd.module.options.spi.ModuleOption annotation = pd.getWriteMethod().getAnnotation( org.springframework.xd.module.options.spi.ModuleOption.class); Assert.notNull( annotation, String.format( "Setter method for option '%s' needs to bear the @%s annotation and provide a description", name, org.springframework.xd.module.options.spi.ModuleOption.class.getSimpleName())); String description = descriptionFromAnnotation(name, annotation); // Don't use pd.getPropertyType(), as it considers the getter first // which may be different from the setter type, which is what we semantically want here Class<?> type = BeanUtils.getWriteMethodParameter(pd).getParameterType(); ModuleOption option = new ModuleOption(name, description).withType(type).hidden(annotation.hidden()); if (beanWrapper.isReadableProperty(name)) { option.withDefaultValue(beanWrapper.getPropertyValue(name)); } else { option.withDefaultValue(defaultFromAnnotation(annotation)); } options.add(option); } } private Object defaultFromAnnotation(org.springframework.xd.module.options.spi.ModuleOption annotation) { String value = annotation.defaultValue(); return org.springframework.xd.module.options.spi.ModuleOption.NO_DEFAULT.equals(value) ? null : value; } /** * Read the description from the value() attribute of the annotation on the getter. */ private String descriptionFromAnnotation(String optionName, org.springframework.xd.module.options.spi.ModuleOption annotation) { Assert.hasLength( annotation.value(), String.format( "Setter method for option '%s' needs to bear the @%s annotation and provide a non-empty description", optionName, org.springframework.xd.module.options.spi.ModuleOption.class.getSimpleName())); return annotation.value(); } @Override public Iterator<ModuleOption> iterator() { return options.iterator(); } @Override public ModuleOptions interpolate(Map<String, String> raw) throws BindException { bindAndValidate(raw); return new ModuleOptions() { @Override public EnumerablePropertySource<?> asPropertySource() { return new EnumerablePropertySource<BeanWrapper>(this.toString(), beanWrapper) { @Override public String[] getPropertyNames() { List<String> result = new ArrayList<String>(); for (PropertyDescriptor pd : beanWrapper.getPropertyDescriptors()) { String name = pd.getName(); if (beanWrapper.isReadableProperty(name) && !"class".equals(name)) { result.add(name); } } return result.toArray(new String[result.size()]); } @Override public Object getProperty(String name) { if (Arrays.asList(getPropertyNames()).contains(name)) { return beanWrapper.getPropertyValue(name); } else { return null; } } }; } @Override public String[] profilesToActivate() { if (beanWrapper.getWrappedInstance() instanceof ProfileNamesProvider) { return ((ProfileNamesProvider) beanWrapper.getWrappedInstance()).profilesToActivate(); } else { return super.profilesToActivate(); } } }; } @SuppressWarnings("unchecked") private void bindAndValidate(Map<String, String> raw) throws BindException { DataBinder dataBinder = new DataBinder(beanWrapper.getWrappedInstance()); dataBinder.setIgnoreUnknownFields(false); dataBinder.setConversionService(conversionService); MutablePropertySources mps = new MutablePropertySources(); mps.addFirst(new MapPropertySource("options", (Map) raw)); try { dataBinder.bind(new PropertySourcesPropertyValues(mps)); } catch (InvalidPropertyException e) { dataBinder.getBindingResult().addError(new FieldError("options", e.getPropertyName(), e.getMessage())); } CustomValidatorBean validator = new CustomValidatorBean(); validator.afterPropertiesSet(); dataBinder.setValidator(validator); Class<?>[] groups = determineGroupsToUse(beanWrapper.getWrappedInstance()); dataBinder.validate((Object[]) groups); if (dataBinder.getBindingResult().hasErrors()) { throw new BindException(dataBinder.getBindingResult()); } } private Class<?>[] determineGroupsToUse(Object pojo) { if (pojo instanceof ValidationGroupsProvider) { ValidationGroupsProvider groupsProvider = (ValidationGroupsProvider) pojo; return groupsProvider.groupsToValidate(); } else { return ValidationGroupsProvider.DEFAULT_GROUP; } } @Override public String toString() { return String.format("%s backed by %s, defining options [%s]", getClass().getSimpleName(), beanWrapper.getWrappedClass(), options); } }