org.wicketstuff.multitextinput.MultiTextInput.java Source code

Java tutorial

Introduction

Here is the source code for org.wicketstuff.multitextinput.MultiTextInput.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.wicketstuff.multitextinput;

import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.apache.wicket.ConverterLocator;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.html.IHeaderResponse;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.markup.html.internal.HtmlHeaderContainer;
import org.apache.wicket.model.AbstractReadOnlyModel;
import org.apache.wicket.model.IDetachable;
import org.apache.wicket.model.IModel;
import org.apache.wicket.protocol.http.ClientProperties;
import org.apache.wicket.protocol.http.request.WebClientInfo;
import org.apache.wicket.request.resource.PackageResourceReference;
import org.apache.wicket.util.convert.ConversionException;
import org.apache.wicket.util.convert.IConverter;
import org.apache.wicket.util.lang.Classes;
import org.apache.wicket.util.string.StringValue;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.validation.ValidationError;
import org.wicketstuff.prototype.PrototypeResourceReference;

/**
 * <p>
 * A component which allows users to type multiple pieces of text into what looks like a text input.
 * </p>
 * <p>
 * When the user hits the enter key, the text they typed is added to the widget and the user may
 * continue to add more.
 * </p>
 * <p>
 * When the form is submitted two sets of inputs are put into the model, the first is the list of
 * items the user has added (or were already there when the widget was first rendered). These inputs
 * are converted from String to whatever type the {@link #getType()} returns and are placed in the
 * model.
 * </p>
 * <p>
 * The developer retrieves the inputs via the standard {@link IModel#getObject()} way, but the model
 * is also loaded with another Collection, the collection of items which were removed from the
 * widget. These are accessible by casting the model to a MultiTextInputModel and using it's
 * {@link MultiTextInputModel#getRemovedItems()} method
 * </p>
 * <p>
 * The component itself was adapted from an existing javascript widget and thus the Wicket component
 * has no "markup" rendering to speak of. Rendering is all done on the client's browser using
 * javascript
 * </p>
 * <p>
 * The only dependency the component has on the client side is the prototype javascript library,
 * which is included via the WicketStuff prototype include.
 * </p>
 * 
 * @author Craig Tataryn (craiger AT tataryn DOT net)
 * 
 * @param <T>
 *            underlying type of the objects which comprise the model's collection of items for the
 *            widget
 */
public class MultiTextInput<T> extends FormComponent<Collection<T>> {

    private int inputLength = 15;

    /**
     * A Model which holds a collection of items for a MultiTextInput. Another list of items is kept
     * which holds the "removed" items (see: {@link MultiTextInputModel#getRemovedItems()}) from the
     * input.
     * 
     * @author Craig Tataryn (craiger AT tataryn DOT net)
     * 
     * @param <T>
     *            a collection type which holds values for a MultiTextInput
     */
    public static class MultiTextInputModel<T extends Collection<?>> implements IModel<T> {

        private static final long serialVersionUID = 1L;
        private T items;
        private T removedItems;

        public MultiTextInputModel(T items) {
            this.items = items;
        }

        /**
         * Returns a list of items removed from the {@link MultiTextInput}
         * 
         * @return List of items removed {@link MultiTextInput}.
         */
        public T getRemovedItems() {
            return removedItems;
        }

        /**
         * Sets a list of items removed from the {@link MultiTextInput}. Users may find this a
         * convenience when having to update a database table after form submission.
         * 
         * @param removedItems
         */
        public void setRemovedItems(T removedItems) {
            this.removedItems = removedItems;
        }

        /**
         * Gets the items for this model
         * 
         * @see IModel#getObject()
         */
        public T getObject() {
            return this.items;
        }

        /**
         * Sets the items for this model
         * 
         * @see IModel#setObject(Object)
         */
        public void setObject(T object) {
            this.items = object;
        }

        /**
         * Does nothing at the moment
         * 
         * @see IDetachable#detach()
         */
        public void detach() {
            // TODO not sure if there is anything to do.
        }

    }

    private static final long serialVersionUID = 1L;
    final ClientProperties properties = ((WebClientInfo) getSession().getClientInfo()).getProperties();

    public MultiTextInput(String id, IModel<Collection<T>> model) {
        super(id, model);
    }

    /**
     * Allows the user to create a MultiTextInput using a simple Collection
     * 
     * @param id
     *            of the component
     * @param items
     *            to display when the component is rendered
     */
    public MultiTextInput(String id, Collection<T> items) {
        super(id, new MultiTextInputModel<Collection<T>>(items));
    }

    /**
     * Allows the user to construct a MultTextInput using a collection of items and a maximum number
     * of characters allowable
     * 
     * @param id
     *            of the component
     * @param items
     *            to display when the component is rendered
     * @param inputLength
     *            maximum number of characters the user is allowed to type (note, if one of the
     *            items in the items parameter contains more characters, it is still displayed)
     */
    public MultiTextInput(String id, Collection<T> items, int inputLength) {
        super(id, new MultiTextInputModel<Collection<T>>(items));
        this.inputLength = inputLength;
    }

    public MultiTextInput(String id) {
        super(id);
    }

    /**
     * Constructs a MultiTextInput using a colletion of items and the type of the underlying items
     * in the model. This type is used to convert String values from the user's input to the correct
     * type used in the model
     * 
     * @param id
     *            of the component
     * @param items
     *            to display when the component is rendered
     * @param type
     *            to use when converting user input (see: {@link ConverterLocator})
     */
    public MultiTextInput(String id, Collection<T> items, Class<T> type) {
        super(id, new MultiTextInputModel<Collection<T>>(items));
        setType(type);
    }

    /**
     * Gets the maximum number of characters a user can type into the input
     * 
     * @return
     */
    public int getInputLength() {
        return inputLength;
    }

    /**
     * Sets the maximum number of characters a user can type into the input
     * 
     * @param inputLength
     */
    public void setInputLength(int inputLength) {
        this.inputLength = inputLength;
    }

    @Override
    public void renderHead(HtmlHeaderContainer container) {
        IHeaderResponse response = container.getHeaderResponse();

        // shouldn't be using MarkupAttributes as it's an internal method, but
        // have to, no other way to
        // find out if the user put an id attribute on the tag,
        // getMarkupId(false) should tell us, but it doens't
        // at this stage in rendering it seems
        String tmpId = getMarkupAttributes().getString("id");
        // if they haven't set the id on the component tag, we'll set one for
        // them
        if (Strings.isEmpty(tmpId)) {
            setOutputMarkupId(true);
            tmpId = this.getMarkupId(true);
        }
        final String id = tmpId;

        // add prototype
        response.renderJavaScriptReference(PrototypeResourceReference.INSTANCE);
        // add this components javascript
        response.renderJavaScriptReference(new PackageResourceReference(this.getClass(), "res/scripts/tag.js"));
        // add component css
        if (properties.isBrowserInternetExplorer()) {
            response.renderCSSReference(
                    new PackageResourceReference(this.getClass(), "res/stylesheets/tag-ie.css"));
        } else if (properties.isBrowserSafari()) {
            response.renderCSSReference(
                    new PackageResourceReference(this.getClass(), "res/stylesheets/tag-webkit.css"));
        } else {
            response.renderCSSReference(
                    new PackageResourceReference(this.getClass(), "res/stylesheets/tag-moz.css"));
        }

        // render the javascript to setup the component
        IModel<Map<String, CharSequence>> variablesModel = new AbstractReadOnlyModel<Map<String, CharSequence>>() {
            private static final long serialVersionUID = 1L;

            @Override
            public Map<String, CharSequence> getObject() {
                Map<String, CharSequence> variables = new HashMap<String, CharSequence>(2);
                variables.put("id", id);
                StringBuffer arr = new StringBuffer();
                // join our collection into a comma delimeted string
                Collection<T> model = (Collection<T>) MultiTextInput.this.getInnermostModel().getObject();
                if (model != null) {
                    Iterator<?> iter = model.iterator();
                    while (iter.hasNext()) {
                        arr.append('\'');
                        // looks like a weird substitution, but regexp in java
                        // ftl.
                        arr.append(iter.next().toString().replaceAll("'", "\\\\'").replaceAll("\"", "\\\\\""));
                        arr.append('\'');
                        if (iter.hasNext()) {
                            arr.append(',');
                        }
                    }
                }
                variables.put("model", arr.toString());
                variables.put("length", String.valueOf(MultiTextInput.this.inputLength));
                return variables;
            }
        };
        // merge the javascript from the properties file with the properties we
        // set above
        String js = new StringBuffer().append(getString("javascript.tagEntry", variablesModel)).toString();
        response.renderOnDomReadyJavaScript(js.toString());

        super.renderHead(container);
    }

    @Override
    protected void onComponentTag(final ComponentTag tag) {
        super.onComponentTag(tag);
        // our component tag is not actually the input that is submitted
        if (tag.getAttributes().containsKey("name")) {
            tag.remove("name");
        }
    }

    @Override
    public void updateModel() {
        super.updateModel();
        // do one more step by setting our model with the removed items
        // in case the user of the component needs this convenience
        List<StringValue> removed = getRequest().getRequestParameters()
                .getParameterValues("removed_" + getInputName());
        MultiTextInputModel<Collection<T>> model = (MultiTextInputModel<Collection<T>>) getModel();

        // mocleiri: adapted for 1.5 but is untested.
        String[] input = new String[removed.size()];

        for (int i = 0; i < removed.size(); i++) {

            input[i] = removed.get(i).toString();
        }
        model.setRemovedItems(convertInput(input));

    }

    /**
     * Robbed from {@link FormComponent}
     * 
     * @param e
     * @param error
     */
    private void reportValidationError(ConversionException e, ValidationError error) {
        final Locale locale = e.getLocale();
        if (locale != null) {
            error.setVariable("locale", locale);
        }
        error.setVariable("exception", e);
        Format format = e.getFormat();
        if (format instanceof SimpleDateFormat) {
            error.setVariable("format", ((SimpleDateFormat) format).toLocalizedPattern());
        }

        Map<String, Object> variables = e.getVariables();
        if (variables != null) {
            error.getVariables().putAll(variables);
        }

        error(error);
    }

    @Override
    protected Collection<T> convertValue(String[] values) throws ConversionException {
        Collection<T> convertedValues = new ArrayList<T>();
        if (values != null && values.length > 0 && values[0] != null) {
            for (String val : values) {
                // 2011.04.14. akiraly This cast does not seem okay
                // casting String to T?
                convertedValues.add((T) val);
            }
        }
        return convertedValues;
    }

    @SuppressWarnings("unchecked")
    private Collection<T> convertInput(String[] inputs) {
        if (getType() == null) {
            try {
                return convertValue(inputs);
            } catch (ConversionException e) {
                ValidationError error = new ValidationError();
                if (e.getResourceKey() != null) {
                    error.addMessageKey(e.getResourceKey());
                }
                if (e.getTargetType() != null) {
                    error.addMessageKey("ConversionError." + Classes.simpleName(e.getTargetType()));
                }
                error.addMessageKey("ConversionError");
                reportValidationError(e, error);
            }
        } else {
            final IConverter<Collection<T>> converter = getConverter(getType());
            String curInput = "";
            try {
                Collection<T> convertedInput = new ArrayList<T>();
                if (inputs != null) {
                    for (String input : inputs) {
                        curInput = input;
                        // 2011.04.14. akiraly This cast does not seem okay
                        // converter returns Collection<T> which is cast to T?
                        convertedInput.add((T) converter.convertToObject(curInput, getLocale()));
                    }
                }
                return convertedInput;
            } catch (ConversionException e) {
                ValidationError error = new ValidationError();
                if (e.getResourceKey() != null) {
                    error.addMessageKey(e.getResourceKey());
                }
                String simpleName = Classes.simpleName(getType());
                error.addMessageKey("IConverter." + simpleName);
                error.addMessageKey("IConverter");
                error.setVariable("type", simpleName);
                error.setVariable("input", curInput);
                reportValidationError(e, error);
            }
        }
        return null;
    }

    /**
     *
     */
    @Override
    protected void convertInput() {
        String[] inputs = getInputAsArray();

        setConvertedInput(convertInput(inputs));

    }

}