Feature: String to collection converter #327

Closed
djmj opened this Issue Nov 14, 2016 · 0 comments

Projects

None yet

1 participant

@djmj
djmj commented Nov 14, 2016 edited

We need to mass input some comma separated data, which can be copied from other sources. For this reason i created a generic string to collection converter for JSF. If you think it solves a general problem, you can add it to omnifaces if you want.

The converter itself requires a converter to convert a single element unless its targeted type is string. Apache StringUtils is used.

It is related to: http://stackoverflow.com/questions/8478698/taking-multiple-values-from-inputtext-field-separated-by-commas-in-jsf

Example Bean:

private ToCollectionConverter batchConverter;
private Set<Foo> batchFoos;

@PostConstruct
public void init()
{
	// generic foo instance creator
	final IInstanceCreatorByValue<Foo, String> fooInstanceByValueCreator = name-> new Foo(name);
	// remote generic converter for Foo.name
	final EntityRemotePropertyConverter<Foo> fooRemotePropertyConverter = new ByPropertyConverter<>(
		Foo.class, this.jndiServiceLocatorBean, String.class, "name", true, fooInstanceByValueCreator);

	// create batch converter
	this.batchConverter = new ToCollectionConverter(fooRemotePropertyConverter, true, ",");
}

public void batchAdd()
{
	for (final Foo : this.batchFoos)
		System.out.println(foo.getName());
}

Example Markup:

<h:form>
	<p:inputText id="delimiter" value="#{fooBean.batchConverter.delimiter}" 
		required="true" size="1">
		<p:ajax update="foos"/>
	</p:inputText>
	<p:inputTextarea id="foos" label="Foos" value="#{fooBean.batchFoos}" converter="#{fooBean.batchConverter}"/>
	<p:commandButton value="Add" action="#{fooBean.batchAdd()}"
		update="@form"/>
</h:form>

ToCollectionConverter

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.stream.Collectors;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.validation.constraints.NotNull;

import org.apache.commons.lang3.StringUtils;

import package.CollectionUtils;

/**
 * Converts a delimiter separated string to a collection of elements.<br>
 * <br>
 * A single element is converted using a provided Converter. If none is provided
 * the string representation of the element is used.<br>
 * <br>
 * It has a public accessible field {@link #delimiter} to allow the user to set
 * the delimiter character using a GUI.<br>
 * <br>
 * By default it converts to an {@link ArrayList} but optional can convert to a
 * {@link HashSet} in case {@link #unique} is <code>true</code>.
 *
 * @author Marco Janc
 */
public class ToCollectionConverter implements Converter, Serializable
{
	private static final long serialVersionUID = 7995643171770295111L;

	/**
	 * Delimiter pattern
	 */
	@NotNull
	private String delimiter;

	/**
	 * <code>true</code> if a space shall be rendered after each element in its
	 * string representation.
	 */
	private final boolean space;

	/**
	 * Single element converter.
	 */
	private final Converter converter;

	/**
	 * <code>true</code> if elements in the collection must be unique
	 */
	private final boolean unique;

	/**
	 * @param delimiter delimiter pattern
	 * @param unique <code>true</code> if elements in the collection must be
	 *            unique
	 * @param converter single element converter
	 */
	public ToCollectionConverter(final Converter converter, final boolean unique, final String delimiter)
	{
		this(converter, unique, delimiter, true);
	}

	/**
	 * @param converter single element converter
	 * @param unique <code>true</code> if elements in the collection must be
	 *            unique
	 * @param delimiter delimiter pattern
	 * @param space <code>true</code> if a space shall be rendered after the
	 *            delimiter
	 */
	public ToCollectionConverter(	final Converter converter, final boolean unique, final String delimiter,
									final boolean space)
	{
		if (delimiter == null)
			throw new IllegalArgumentException("delimiter must not be null");
		this.delimiter = delimiter;
		this.space = space;
		this.converter = converter;
		this.unique = unique;
	}

	@SuppressWarnings(
	{
		"rawtypes", "unchecked"
	})
	@Override
	public Object getAsObject(final FacesContext context, final UIComponent component, final String value)
	{
		Collection entities = null;

		if (StringUtils.isNotBlank(value))
		{
			final String[] strEntities = StringUtils.normalizeSpace(value).split(this.delimiter);

			entities = this.unique ? new HashSet<>(strEntities.length) : new ArrayList<>(strEntities.length);

			for (String strEntity : strEntities)
			{
				strEntity = strEntity.trim();

				if (strEntity.equals(""))
					continue;

				final Object entity = this.converter == null ? strEntity : this.converter.getAsObject(context, component, strEntity);

				if (entity != null)
					entities.add(entity);
			}
		}

		return entities;
	}

	@Override
	public String getAsString(final FacesContext context, final UIComponent component, final Object value)
	{
		if (value instanceof Collection)
		{
			final Collection<?> col = (Collection<?>) value;

			String delimiter = this.delimiter;
			if (this.space)
				delimiter += " ";

			return col.stream()
				.map(
					v -> this.converter == null ? String.valueOf(v) : this.converter.getAsString(context, component, v))
				.collect(Collectors.joining(delimiter));
		}
		return null;
	}

	/*
	 * Getter and Setter
	 */

	public String getDelimiter()
	{
		return this.delimiter;
	}

	public void setDelimiter(final String delimiter)
	{
		this.delimiter = delimiter;
	}
}

Properties delimiter and space could also be passed via f:attribute in case it shall be a constant.

I dont know right now if its a good idea to return a HashSet. I already had a use case where i needed a LinkedHashSet and another use case, where i bound the same value to a p:autoComplete which just supports List.

So for now i just do:

entities = new ArrayList<>(strEntities.length);
@BalusC BalusC closed this in 9486717 Nov 25, 2016
@BalusC BalusC added a commit that referenced this issue Nov 26, 2016
@BalusC BalusC #327 Add itemConverter attribute 0c57bb5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment