New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: String to collection converter #327

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

Comments

Projects
None yet
1 participant
@djmj

djmj commented Nov 14, 2016

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 added a commit that referenced this issue Nov 26, 2016

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment