/* Copyright 2005-2006 Tim Fennell
*
* 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 net.sourceforge.stripes.validation;
import net.sourceforge.stripes.controller.StripesFilter;
import net.sourceforge.stripes.exception.StripesRuntimeException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Locale;
/**
* <p>A specialized type converter for converting a <b>single</b> input field/parameter value
* into one or more Java objects contained in a <b>List</b>. Designed to handle the case where a user
* is allowed to enter more than one value into a single field, separated by certain characters,
* that should result in a number of Java objects being created.</p>
*
* <p>For example imagine a search field where a user is allowed to enter one or more numbers.
* They might enter "2 4 8 16". In this case the {@code OneToManyTypeConverter} will convert
* this to a Collection of numbers with one entry for each of the four numbers shown above. The type
* of number created ({@link java.lang.Integer}, {@link java.lang.Long} etc.) is inferred from the
* declaration of the property on the {@code ActionBean}. For example:</p>
*
*<p>{@code @Validate(converter=OneToManyTypeConverter.class) private List<Long> numbers;}</p>
*
* <p>would result in the numbers being converted to Longs as opposed to any other numeric type.</p>
*
* <p>The splitting of the input String is done using the {@link java.lang.String#split(String)}
* method. The regular expression passed to {@code split()} is obtained by calling
* {@link #getSplitRegex()}. By default the regular expression used will match an optional comma
* followed by one or spaces (e.g. " " or ", " or " " etc.). This behaviour can easily be
* modified by subclassing and overriding {@link #getSplitRegex()} to return a different
* regular expression string.</p>
*
* <p>The individual components of the String are then converted using an appropriate
* {@link TypeConverter} which is looked up using the {@link TypeConverterFactory}. As a result
* the {@code OneToManyTypeConverter} can be used to convert to a list of any type for which
* a {@code TypeConverter} has been registered. If a usable {@code TypeConverter} cannot be
* discovered then an Exception will be thrown! However, if you wish to use the
* {@code OneToManyTypeConverter} with a {@code TypeConverter} which is not registered as the
* default converter for its type you can override this behaviour by subclassing
* this class and overriding {@link #getSingleItemTypeConverter(Class)}.</p>
*
* <p>Strictly speaking the {@code OneToManyTypeConverter} returns a {@link java.util.Collection}
* of converted items. It does not have any way of inferring the collection type that should be
* used, and so by default it will always return an instance of {@link java.util.List}. This
* behaviour can easily be overridden by extending this class and overriding
* {@link #getCollectionInstance()}.</p>
*
* <p>Note that the converter itself does not create any {@link ValidationError}s, but that
* by using other {@link TypeConverter}s internally it is possible to produce one or more
* errors per item split out of the input String!</p>
*
* @author Tim Fennell
* @since Stripes 1.2.2
*/
public class OneToManyTypeConverter implements TypeConverter<Object> {
private Locale locale;
public void setLocale(Locale locale) { this.locale = locale; }
/**
* Converts the supplied String into one or more objects is the manner described in
* the class level JavaDoc. If any validation errors occur then {@code null} is returned
* regardless of whether any items were successfully converted or not.
*
* @param input an input String containing one or more items to be converted in a single
* String
* @param targetType the type that each individual item will be converted to
* @param errors a collection of ValidationErrors that can be added to
* @return a Collection containing one or more items of targetType, or null if any
* ValidationErrors occur.
*/
@SuppressWarnings("unchecked")
public Collection<? extends Object> convert(String input,
Class<? extends Object> targetType,
Collection<ValidationError> errors) {
TypeConverter converter = getSingleItemTypeConverter(targetType);
String[] splits = input.split( getSplitRegex() );
Collection<Object> items = getCollectionInstance();
for (String split : splits) {
Object item = converter.convert(split, targetType, errors);
if (item != null) {
items.add(item);
}
}
return items.size() > 0 ? items : null;
}
/**
* Instantiates and returns a Collection of a type that can be set on ActionBeans using
* this converter. By default returns an instance of {@link java.util.List}.
*
* @return an instance of {@link java.util.List}
*/
@SuppressWarnings("unchecked")
public Collection getCollectionInstance() {
return new LinkedList<Object>();
}
/**
* Returns the String form of a regular expression that identifies the separator Strings
* in the input String. The default expression matches an optional comma followed by one
* or more spaces.
*
* @return a regular expression matching an optional comma followed by one or more spaces.
*/
protected String getSplitRegex() {
return "[, ]+";
}
/**
* Fetches an instance of {@link TypeConverter} that can be used to convert the individual
* items split out of the input String. By default uses the {@link TypeConverterFactory} to
* find an appropriate {@link TypeConverter}.
*
* @param targetType the type that each item should be converted to.
* @return a TypeConverter for use in converting each individual item.
*/
@SuppressWarnings("unchecked")
protected TypeConverter getSingleItemTypeConverter(Class targetType) {
try {
TypeConverterFactory factory = StripesFilter.getConfiguration().getTypeConverterFactory();
return factory.getTypeConverter(targetType, this.locale);
}
catch (Exception e) {
throw new StripesRuntimeException(
"You are using the OneToManyTypeConverter to convert a String to a List of " +
"items for which there is no registered converter! Please check that the " +
"TypeConverterFactory knows how to make a converter for: " +
targetType, e
);
}
}
}