/*
* Copyright 2017 OmniFaces
*
* 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.omnifaces.converter;
import static java.util.regex.Pattern.quote;
import static org.omnifaces.util.Faces.createConverter;
import static org.omnifaces.util.Reflection.instance;
import static org.omnifaces.util.Utils.coalesce;
import static org.omnifaces.util.Utils.isEmpty;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Set;
import javax.el.ValueExpression;
import javax.faces.application.Application;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.FacesConverter;
import org.omnifaces.el.ExpressionInspector;
import org.omnifaces.util.Faces;
/**
* <p>
* The <code>omnifaces.ToCollectionConverter</code> is intented to convert submitted {@link String} values to a Java
* collection based on a delimiter. Additionally, it trims any whitespace around each delimited submitted value. This is
* useful for among others comma separated value inputs.
*
* <h3>Usage</h3>
* <p>
* This converter is available by converter ID <code>omnifaces.ToCollectionConverter</code>. Just specify it in the
* <code>converter</code> attribute of the component referring the <code>Collection</code> property. For example:
* <pre>
* <h:inputText value="#{bean.commaSeparatedValues}" converter="omnifaces.ToCollectionConverter" />
* </pre>
* <p>
* The default delimiter is comma followed by space <code>, </code> and the default collection type is
* <code>java.util.LinkedHashSet</code> for a <code>Set</code> property and <code>java.util.ArrayList</code> for anything
* else, and the default converter for each item will in <code>getAsString()</code> be determined based on item type and
* in <code>getAsObject()</code> be determined based on generic return type of the getter method.
* <p>
* You can use <code><o:converter></code> to specify those attributes. The <code>delimiter</code> must be a
* <code>String</code>, the <code>collectionType</code> must be a FQN and the <code>itemConverter</code> can be
* anything which is acceptable by {@link Faces#createConverter(Object)}.
* <pre>
* <h:inputText value="#{bean.uniqueOrderedSemiColonSeparatedNumbers}">
* <o:converter converterId="omnifaces.ToCollectionConverter"
* delimiter=";"
* collectionType="java.util.TreeSet"
* itemConverter="javax.faces.Integer" >
* </h:inputText>
* </pre>
*
* @author Bauke Scholtz
* @see TrimConverter
* @since 2.6
*/
@FacesConverter("omnifaces.ToCollectionConverter")
public class ToCollectionConverter extends TrimConverter {
private static final String DEFAULT_DELIMITER = ",";
private static final String DEFAULT_SET_TYPE = "java.util.LinkedHashSet";
private static final String DEFAULT_COLLECTION_TYPE = "java.util.ArrayList";
private String delimiter;
private String collectionType;
private Object itemConverter;
@Override
public Object getAsObject(FacesContext context, UIComponent component, String submittedValue) {
if (isEmpty(submittedValue)) {
return null;
}
String type = collectionType;
Converter converter = createConverter(itemConverter);
ValueExpression valueExpression = component.getValueExpression("value");
if (valueExpression != null) {
Method getter = ExpressionInspector.getMethodReference(context.getELContext(), valueExpression).getMethod();
Class<?> returnType = getter.getReturnType();
if (!Collection.class.isAssignableFrom(returnType)) {
throw new IllegalArgumentException(valueExpression.getExpressionString() + " does not resolve to Collection.");
}
if (collectionType == null && Set.class.isAssignableFrom(returnType)) {
type = DEFAULT_SET_TYPE;
}
if (converter == null) {
Type[] actualTypeArguments = ((ParameterizedType) getter.getGenericReturnType()).getActualTypeArguments();
if (actualTypeArguments.length > 0) {
Class<?> forClass = (Class<?>) actualTypeArguments[0];
converter = context.getApplication().createConverter(forClass);
}
}
}
Collection<Object> collection = instance(coalesce(type, DEFAULT_COLLECTION_TYPE));
for (String item : submittedValue.split(quote(coalesce(delimiter, DEFAULT_DELIMITER).trim()))) {
Object trimmed = super.getAsObject(context, component, item);
collection.add(converter == null ? trimmed : converter.getAsString(context, component, trimmed));
}
return collection;
}
@Override
public String getAsString(FacesContext context, UIComponent component, Object modelValue) {
if (isEmpty(modelValue)) {
return "";
}
Application application = context.getApplication();
StringBuilder builder = new StringBuilder();
String specifiedDelimiter = coalesce(delimiter, DEFAULT_DELIMITER);
Converter specifiedConverter = createConverter(itemConverter);
Class<?> forClass = null;
Converter converter = specifiedConverter;
int i = 0;
for (Object item : (Collection<?>) modelValue) {
if (i++ > 0) {
builder.append(specifiedDelimiter);
}
if (specifiedConverter == null && item != null && forClass != item.getClass()) {
forClass = item.getClass();
converter = application.createConverter(forClass);
}
builder.append(converter == null ? super.getAsString(context, component, item) : converter.getAsString(context, component, item));
}
return builder.toString();
}
public void setDelimiter(String delimiter) {
this.delimiter = delimiter;
}
public void setCollectionType(String collectionType) {
this.collectionType = collectionType;
}
public void setItemConverter(Object itemConverter) {
this.itemConverter = itemConverter;
}
}