/* * #%L * Wisdom-Framework * %% * Copyright (C) 2013 - 2014 Wisdom Framework * %% * 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. * #L% */ package org.wisdom.content.converters; import com.google.common.collect.ImmutableSet; import com.google.common.primitives.Primitives; import org.apache.felix.ipojo.annotations.Component; import org.apache.felix.ipojo.annotations.Instantiate; import org.apache.felix.ipojo.annotations.Provides; import org.apache.felix.ipojo.annotations.Requires; import org.wisdom.api.content.ParameterConverter; import org.wisdom.api.content.ParameterFactories; import org.wisdom.api.content.ParameterFactory; import org.wisdom.api.http.Context; import java.lang.reflect.Array; import java.lang.reflect.Type; import java.util.*; /** * Implementation of the {@link org.wisdom.api.content.ParameterFactories} service to convert objects. */ @Component @Provides @Instantiate(name = "ParameterConverterEngine") public class ParamConverterEngine implements ParameterFactories { @Requires(specification = ParameterConverter.class, optional = true) List<ParameterConverter> converters; @Requires(specification = ParameterFactory.class, optional = true) List<ParameterFactory> factories; /** * Creates the singleton instance of {@link org.wisdom.content.converters.ParamConverterEngine} used at runtime. */ public ParamConverterEngine() { // The constructor used by iPOJO. } /** * Constructor used for testing purpose only. * * @param conv the list of converter. * @param fact the list of parameter factories */ public ParamConverterEngine(List<ParameterConverter> conv, List<ParameterFactory> fact) { converters = conv; factories = fact; } @Override public <T> T convertValue(String input, Class<T> rawType, Type type, String defaultValue) throws IllegalArgumentException { if (rawType.isArray()) { List<String> args = getMultipleValues(input, defaultValue); return createArray(args, rawType.getComponentType()); } else if (Collection.class.isAssignableFrom(rawType)) { List<String> args = getMultipleValues(input, defaultValue); return createCollection(args, rawType, type); } else { return convertSingleValue(input, rawType, defaultValue); } } private List<String> getMultipleValues(String input, String defaultValue) { if (input == null && defaultValue == null) { return null; } if (input == null) { input = defaultValue; } String[] segments = input.split(","); List<String> values = new ArrayList<>(); for (String s : segments) { String v = s.trim(); if (!v.isEmpty()) { values.add(v); } } return values; } /** * Creates an instance of T from the given input. Unlike {@link #convertValue(String, Class, * java.lang.reflect.Type, String)}, this method support multi-value parameters. * * @param input the input Strings, may be {@literal null} or empty * @param rawType the target class * @param type the type representation of the raw type, may contains metadata about generics * @param defaultValue the default value if any * @return the created object * @throws IllegalArgumentException if there are no converter available from the type T at the moment */ @Override public <T> T convertValues(Collection<String> input, Class<T> rawType, Type type, String defaultValue) throws IllegalArgumentException { if (rawType.isArray()) { if (input == null) { input = getMultipleValues(defaultValue, null); } return createArray(input, rawType.getComponentType()); } else if (Collection.class.isAssignableFrom(rawType)) { if (input == null) { input = getMultipleValues(defaultValue, null); } return createCollection(input, rawType, type); } else { return convertSingleValue(input, rawType, defaultValue); } } /** * Creates an instance of T from the given HTTP content. Unlike converters, it does not handler generics or * collections. * * @param context the HTTP content * @param type the class to instantiate * @return the created object * @throws IllegalArgumentException if there are no {@link org.wisdom.api.content.ParameterFactory} available for * the type T, or if the instantiation failed. */ @Override public <T> T newInstance(Context context, Class<T> type) throws IllegalArgumentException { // Retrieve the factory for (ParameterFactory factory : factories) { if (factory.getType().equals(type)) { // Factory found - instantiate //noinspection unchecked return (T) factory.newInstance(context); } } throw new IllegalArgumentException("Unable to find a ParameterFactory able to create instance of " + type.getName()); } /** * Gets the current set of classes that can be instantiated using an available * {@link org.wisdom.api.content.ParameterFactory}. This set if dynamic, the returned collection is an immutable * copy of a snapshot. * * @return the set of classes that can be instantiated using an available {@link org.wisdom.api.content * .ParameterFactory} service. */ @Override public Set<Class> getTypesHandledByFactories() { final ImmutableSet.Builder<Class> builder = ImmutableSet.builder(); for (ParameterFactory factory : factories) { builder.add(factory.getType()); } return builder.build(); } private <T> T createCollection(Collection<String> input, Class<T> rawType, Type type) { // Get the generic type of the list // If none default to String final List<ClassTypePair> ctps = ReflectionHelper.getTypeArgumentAndClass(type); ClassTypePair ctp = (ctps.size() == 1) ? ctps.get(0) : null; if (ctp == null || ctp.rawClass() == String.class) { return createCollectionWithConverter(input, rawType, StringConverter.INSTANCE); } else { ParameterConverter converter = getConverter(ctp.rawClass()); // On Java 8 we cannot use 'cast' here, I don't really understand why. //noinspection unchecked return (T) createCollectionWithConverter(input, rawType, converter); } } private <T, A> T createCollectionWithConverter(Collection<String> input, Class<T> type, ParameterConverter<A> converter) { Collection<A> collection; if (type == Collection.class || List.class.isAssignableFrom(type)) { if (input == null) { return type.cast(Collections.emptyList()); } collection = new ArrayList<>(); } else if (Set.class.isAssignableFrom(type)) { if (input == null) { return type.cast(Collections.emptySet()); } collection = new LinkedHashSet<>(); } else { throw new IllegalArgumentException("Not supported collection type " + type.getName()); } for (String v : input) { collection.add(converter.fromString(v)); } return type.cast(collection); } private <T> T createArray(Collection<String> input, Class<?> componentType) { if (input == null) { //noinspection unchecked return (T) Array.newInstance(componentType, 0); } Class<?> theType = componentType; if (componentType.isPrimitive()) { theType = Primitives.wrap(componentType); } ParameterConverter converter = getConverter(theType); List<Object> list = new ArrayList<>(); for (String v : input) { list.add(converter.fromString(v)); } // We cannot use the toArray method as the the type does not match (toArray would produce an object[]). Object array = Array.newInstance(componentType, list.size()); int i = 0; for (Object o : list) { Array.set(array, i, o); i++; } //noinspection unchecked return (T) array; } private <T> T convertSingleValue(String input, Class<T> type, String defaultValue) { if (type.isPrimitive()) { type = Primitives.wrap(type); if (input == null && defaultValue == null) { defaultValue = ReflectionHelper.getPrimitiveDefault(type); } } ParameterConverter<T> converter = getConverter(type); if (input != null) { return converter.fromString(input); } else { return converter.fromString(defaultValue); } } private <T> T convertSingleValue(Collection<String> input, Class<T> type, String defaultValue) { if (input == null || input.isEmpty()) { return convertSingleValue((String) null, type, defaultValue); } else { String v = input.iterator().next(); return convertSingleValue(v, type, defaultValue); } } /** * Searches a suitable converter to convert String to the given type. * * @param type the target type * @param <T> the class * @return the parameter converter able to creates instances of the target type from String representations. * @throws java.util.NoSuchElementException if no converter can be found */ @SuppressWarnings("unchecked") private <T> ParameterConverter<T> getConverter(Class<T> type) { // check for String first if (type == String.class) { return (ParameterConverter<T>) StringConverter.INSTANCE; } // Search for exposed converters. for (ParameterConverter pc : converters) { //noinspection EqualsBetweenInconvertibleTypes if (pc.getType().equals(type)) { //NOSONAR return pc; } } // Boolean has a special case as they support other form of "truth" such as "yes", "on", "1"... if (type == Boolean.class) { return (ParameterConverter<T>) BooleanConverter.INSTANCE; } // None of them are there, try default converters in the following order: // 1. constructor // 2. valueOf // 3. from // 4. fromString ParameterConverter<T> converter = ConstructorBasedConverter.getIfEligible(type); if (converter != null) { return converter; } converter = ValueOfBasedConverter.getIfEligible(type); if (converter != null) { return converter; } converter = FromBasedConverter.getIfEligible(type); if (converter != null) { return converter; } converter = FromStringBasedConverter.getIfEligible(type); if (converter != null) { return converter; } // Unlike other primitive type, characters cannot be created using the 'valueOf' method, // so we need a specific converter. As creating characters is quite rare, this must be the last check. if (type == Character.class) { return (ParameterConverter<T>) CharacterConverter.INSTANCE; } // running out of converters... throw new NoSuchElementException("Cannot find a converter able to create instance of " + type.getName()); } }