/*
* 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.apache.cocoon.forms.formmodel;
import java.lang.reflect.Array;
import java.util.Locale;
import org.apache.cocoon.forms.FormsConstants;
import org.apache.cocoon.forms.FormContext;
import org.apache.cocoon.forms.datatype.Datatype;
import org.apache.cocoon.forms.datatype.SelectionList;
import org.apache.cocoon.forms.datatype.convertor.ConversionResult;
import org.apache.cocoon.forms.datatype.convertor.Convertor;
import org.apache.cocoon.forms.event.*;
import org.apache.cocoon.forms.util.I18nMessage;
import org.apache.cocoon.forms.validation.ValidationError;
import org.apache.cocoon.forms.validation.ValidationErrorAware;
import org.apache.cocoon.xml.XMLUtils;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
/**
* A MultiValueField is mostly the same as a normal {@link Field}, but can
* hold multiple values. A MultiValueField should have a Datatype which
* has a SelectionList, because the user will always select the values
* from a list. A MultiValueField has no concept of "required", you should
* instead use the ValueCountValidationRule to check how many items the user
* has selected.
*
* <p>A MultiValueField also has a {@link Datatype} associated with it. In
* case of MultiValueFields, this Datatype will always be an array
* type, thus {@link Datatype#isArrayType()} will always return true, and
* this in return has an influence on the kind of validation rules that
* can be used with the Datatype (see {@link Datatype} description for more
* information).</p>
*
* @version $Id$
*/
public class MultiValueField extends AbstractWidget
implements ValidationErrorAware, SelectableWidget, DataWidget,
ValueChangedListenerEnabled {
private static final String MULTIVALUEFIELD_EL = "multivaluefield";
private static final String VALUES_EL = "values";
private static final String VALUE_EL = "value";
private static final String VALIDATION_MSG_EL = "validation-message";
private final MultiValueFieldDefinition definition;
private SelectionList selectionList;
private String[] enteredValues;
private String invalidEnteredValue;
private Object[] values;
private ValidationError validationError;
private ValueChangedListener listener;
public MultiValueField(MultiValueFieldDefinition definition) {
super(definition);
this.definition = definition;
this.listener = definition.getValueChangedListener();
}
public void initialize() {
this.selectionList = this.definition.getSelectionList();
super.initialize();
}
public WidgetDefinition getDefinition() {
return definition;
}
public void readFromRequest(FormContext formContext) {
if (!getCombinedState().isAcceptingInputs()) {
return;
}
enteredValues = formContext.getRequest().getParameterValues(getRequestParameterName());
invalidEnteredValue = null;
validationError = null;
Object[] oldValues = values;
values = null;
boolean conversionFailed = false;
if (enteredValues != null) {
Object[] tempValues = (Object[])Array.newInstance(getDatatype().getTypeClass(), enteredValues.length);
for (int i = 0; i < enteredValues.length; i++) {
String param = enteredValues[i];
ConversionResult conversionResult = definition.getDatatype().convertFromString(param, formContext.getLocale());
if (conversionResult.isSuccessful()) {
tempValues[i] = conversionResult.getResult();
} else {
conversionFailed = true;
invalidEnteredValue = param;
break;
}
}
if (!conversionFailed)
values = tempValues;
else
values = null;
} else {
values = new Object[0];
}
engenderChangeEvent(oldValues);
}
private void engenderChangeEvent(Object[] oldValues) {
boolean hasListeners = hasValueChangedListeners() || this.getForm().hasFormHandler();
if (hasListeners) {
if (values != null) {
boolean changed = false;
if (oldValues == null) {
changed = true;
} else if (oldValues.length != values.length) {
changed = true;
} else {
for (int i = 0; i < values.length; i++) {
if (!values[i].equals(oldValues[i])) {
changed = true;
break;
}
}
}
if (changed)
getForm().addWidgetEvent(new ValueChangedEvent(this, oldValues, values));
}
}
}
public boolean validate() {
if (!getCombinedState().isValidatingValues()) {
this.wasValid = true;
return true;
}
if (values != null) {
validationError = definition.getDatatype().validate(values, new ExpressionContextImpl(this));
} else if (invalidEnteredValue != null) {
validationError = new ValidationError(new I18nMessage("multivaluefield.conversionfailed", new String[] {invalidEnteredValue}, FormsConstants.I18N_CATALOGUE));
}
this.wasValid = validationError == null ? super.validate() : false;
return this.wasValid;
}
/**
* @return "multivaluefield"
*/
public String getXMLElementName() {
return MULTIVALUEFIELD_EL;
}
public void generateItemSaxFragment(ContentHandler contentHandler, Locale locale) throws SAXException {
contentHandler.startElement(FormsConstants.INSTANCE_NS, VALUES_EL, FormsConstants.INSTANCE_PREFIX_COLON + VALUES_EL, XMLUtils.EMPTY_ATTRIBUTES);
Convertor convertor = definition.getDatatype().getConvertor();
if (values != null) {
for (int i = 0; i < values.length; i++) {
contentHandler.startElement(FormsConstants.INSTANCE_NS, VALUE_EL, FormsConstants.INSTANCE_PREFIX_COLON + VALUE_EL, XMLUtils.EMPTY_ATTRIBUTES);
String value = convertor.convertToString(values[i], locale, null);
contentHandler.characters(value.toCharArray(), 0, value.length());
contentHandler.endElement(FormsConstants.INSTANCE_NS, VALUE_EL, FormsConstants.INSTANCE_PREFIX_COLON + VALUE_EL);
}
} else if (enteredValues != null) {
for (int i = 0; i < enteredValues.length; i++) {
contentHandler.startElement(FormsConstants.INSTANCE_NS, VALUE_EL, FormsConstants.INSTANCE_PREFIX_COLON + VALUE_EL, XMLUtils.EMPTY_ATTRIBUTES);
String value = enteredValues[i];
contentHandler.characters(value.toCharArray(), 0, value.length());
contentHandler.endElement(FormsConstants.INSTANCE_NS, VALUE_EL, FormsConstants.INSTANCE_PREFIX_COLON + VALUE_EL);
}
}
contentHandler.endElement(FormsConstants.INSTANCE_NS, VALUES_EL, FormsConstants.INSTANCE_PREFIX_COLON + VALUES_EL);
// validation message element
if (validationError != null) {
contentHandler.startElement(FormsConstants.INSTANCE_NS, VALIDATION_MSG_EL, FormsConstants.INSTANCE_PREFIX_COLON + VALIDATION_MSG_EL, XMLUtils.EMPTY_ATTRIBUTES);
validationError.generateSaxFragment(contentHandler);
contentHandler.endElement(FormsConstants.INSTANCE_NS, VALIDATION_MSG_EL, FormsConstants.INSTANCE_PREFIX_COLON + VALIDATION_MSG_EL);
}
// the selection list
if (this.selectionList != null) {
this.selectionList.generateSaxFragment(contentHandler, locale);
}
// include some info about the datatype
definition.getDatatype().generateSaxFragment(contentHandler, locale);
}
public Object getValue() {
return values;
}
public void setValue(Object value) {
if (value == null) {
setValues(new Object[0]);
} else if (value.getClass().isArray()) {
setValues((Object[])value);
} else {
throw new RuntimeException("Cannot set value of field '" + getRequestParameterName() + "' with an object of type " +
value.getClass().getName());
}
getForm().addWidgetUpdate(this);
}
public void setValues(Object[] values) {
// check that all the objects in the array correspond to the datatype
for (int i = 0; i < values.length; i++) {
if (!definition.getDatatype().getTypeClass().isAssignableFrom(values[i].getClass())) {
throw new RuntimeException("Cannot set value of field '" + getRequestParameterName() + "' with an object of type " +
values[i].getClass().getName());
}
}
Object[] oldValues = this.values;
this.values = values;
engenderChangeEvent(oldValues);
getForm().addWidgetUpdate(this);
}
/**
* Set this field's selection list.
* @param selectionList The new selection list.
*/
public void setSelectionList(SelectionList selectionList) {
if (selectionList == null) {
throw new IllegalArgumentException("An MultiValueField's selection list cannot be null.");
}
if (selectionList.getDatatype() != null &&
selectionList.getDatatype() != definition.getDatatype()) {
throw new RuntimeException("Tried to assign a SelectionList that is not associated with this widget's datatype.");
}
this.selectionList = selectionList;
getForm().addWidgetUpdate(this);
}
/**
* Read this field's selection list from an external source.
* All Cocoon-supported protocols can be used.
* The format of the XML produced by the source should be the
* same as in case of inline specification of the selection list,
* thus the root element should be a <code>fd:selection-list</code>
* element.
* @param uri The URI of the source.
*/
public void setSelectionList(String uri) {
setSelectionList(this.definition.buildSelectionList(uri));
}
/**
* Set this field's selection list using values from an in-memory
* object. The <code>object</code> parameter should point to a collection
* (Java collection or array, or Javascript array) of objects. Each object
* belonging to the collection should have a <em>value</em> property and a
* <em>label</em> property, whose values are used to specify the <code>value</code>
* attribute and the contents of the <code>fd:label</code> child element
* of every <code>fd:item</code> in the list.
* <p>Access to the values of the above mentioned properties is done
* via <a href="http://jakarta.apache.org/commons/jxpath/users-guide.html">XPath</a> expressions.
* @param model The collection used as a model for the selection list.
* @param valuePath An XPath expression referring to the attribute used
* to populate the values of the list's items.
* @param labelPath An XPath expression referring to the attribute used
* to populate the labels of the list's items.
*/
public void setSelectionList(Object model, String valuePath, String labelPath) {
setSelectionList(this.definition.buildSelectionListFromModel(model, valuePath, labelPath));
}
public void broadcastEvent(WidgetEvent event) {
if (event instanceof ValueChangedEvent) {
if (this.listener != null) {
this.listener.valueChanged((ValueChangedEvent)event);
}
} else {
// Other kinds of events
super.broadcastEvent(event);
}
}
public ValidationError getValidationError() {
return this.validationError;
}
public void setValidationError(ValidationError error) {
this.validationError = error;
getForm().addWidgetUpdate(this);
}
public Datatype getDatatype() {
return definition.getDatatype();
}
public void addValueChangedListener(ValueChangedListener listener) {
this.listener = WidgetEventMulticaster.add(this.listener, listener);
}
public void removeValueChangedListener(ValueChangedListener listener) {
this.listener = WidgetEventMulticaster.remove(this.listener, listener);
}
public boolean hasValueChangedListeners() {
return this.listener != null;
}
}