blob: 0b7f03513b2f468da49ce83ee95d6a133e7413e8 [file] [log] [blame]
/*
* Copyright 1999-2004 The Apache Software Foundation.
*
* 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.apache.cocoon.components.xmlform;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.apache.cocoon.Constants;
import org.apache.cocoon.components.validation.Validator;
import org.apache.cocoon.components.validation.Violation;
import org.apache.cocoon.environment.ObjectModelHelper;
import org.apache.cocoon.environment.Request;
import org.apache.cocoon.environment.Session;
import org.apache.cocoon.transformation.XMLFormTransformer;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.JXPathException;
import org.apache.commons.jxpath.Pointer;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.ScriptableObject;
/**
* <p>
* Encapsulates a form bean and the validation result
* in a single class. It is created automatically by the
* FormValidatingAction
* </p>
*
* <b>NOTE: This class is NOT thread safe</b>
*
* @author Ivelin Ivanov, ivelin@apache.org
* @author michael_hampel@sonynetservices.com
* @version CVS $Id: Form.java,v 1.8 2004/03/05 13:02:38 bdelacretaz Exp $
*/
public class Form {
public static String SCOPE_REQUEST = "request";
public static String SCOPE_SESSION = "session";
public static String FORM_VIEW_PARAM = "cocoon-xmlform-view";
public static String VIOLATION_MESSAGE_DATA_FORMAT_ERROR = "Invalid data format or invalid reference path.";
/**
* An XMLForm is only usable when it has an id and an underlying model.
*
* @param id
* @param model
*/
public Form(String id, Object model) {
if ((id==null) || (model==null)) {
throw new java.lang.IllegalStateException("Form cannot be created with null id or null model ");
}
setId(id);
setModel(model);
}
public String getId() {
return id_;
}
public void setId(String newId) {
id_ = newId;
}
public Object getModel() {
return model_;
}
public void setModel(Object newModel) {
model_ = newModel;
jxcontext_ = JXPathContext.newContext(model_);
jxcontext_.setLenient(false);
}
public Validator getValidator() {
return validator_;
}
public void setValidator(Validator newValidator) {
validator_ = newValidator;
}
public List getViolations() {
return violations_;
}
/**
* Expose the JXPathContext for the sake of subclasses
*/
protected JXPathContext getJXContext() {
return jxcontext_;
}
/**
* This method allows custom validations to be added
* after population and after a call to validate
* (either automatic or explicit).
* Usually used from within the perform method of
* a concrete XMLFormAction.
*
* @param newViolations
*/
public void addViolations(List newViolations) {
if (violations_!=null) {
violations_.addAll(newViolations);
} else {
violations_ = newViolations;
}
updateViolationsAsSortedSet();
}
public SortedSet getViolationsAsSortedSet() {
return violationsAsSortedSet_;
}
public void clearViolations() {
violations_ = null;
violationsAsSortedSet_ = null;
}
/**
* Encapsulates access to the model.
*
* @param xpath to the model attribute.
* @param value to be set.
*/
public void setValue(String xpath, Object value) {
if (model_==null) {
throw new IllegalStateException("Form model not set");
}
jxcontext_.setValue(xpath, value);
}
public void setValue(String xpath, Object[] values) {
// // Dmitri Plotnikov's patch
//
// // if there are multiple values to set
// // (like in the selectMany case),
// // iterate over the array and set individual values
// if ( values.length > 1 )
// {
// Iterator iter = jxcontext_.iteratePointers(xpath);
// for (int i = 0; i < values.length; i++ )
// {
// Pointer ptr = (Pointer)iter.next();
// ptr.setValue(values[i]);
// }
// }
// else
// {
// // This is supposed to do the right thing
// jxcontext_.setValue(xpath, values);
// }
//
Pointer pointer = jxcontext_.getPointer(xpath);
Object property = pointer.getNode();
// if there are multiple values to set
// (like in the selectMany case),
// iterate over the array and set individual values
// when the instance property is array
if ((property!=null) && property.getClass().isArray()) {
Class componentType = property.getClass().getComponentType();
property = java.lang.reflect.Array.newInstance(componentType,
values.length);
java.lang.System.arraycopy(values, 0, property, 0, values.length);
pointer.setValue(property);
} else if (property instanceof Collection) {
Collection cl = (Collection) property;
cl.clear();
cl.addAll(java.util.Arrays.asList(values));
} else if (property instanceof NativeArray) {
Context.enter();
try {
NativeArray arr = (NativeArray) property;
ScriptableObject.putProperty(arr, "length", new Integer(0));
ScriptableObject.putProperty(arr, "length",
new Integer(values.length));
for (int i = 0; i<values.length; i++) {
Object val = values[i];
if ( !((val==null) || (val instanceof String) ||
(val instanceof Number) ||
(val instanceof Boolean))) {
val = Context.toObject(val, arr);
}
ScriptableObject.putProperty(arr, i, val);
}
} catch (Exception willNotBeThrown) {
// shouldn't happen
willNotBeThrown.printStackTrace();
} finally {
Context.exit();
}
} else {
jxcontext_.setValue(xpath, values[0]);
}
}
/**
* Encapsulates access to the model.
*
* @param xpath of the model attribute
*/
public Object getValue(String xpath) {
if (model_==null) {
throw new IllegalStateException("Form model not set");
}
Object result = jxcontext_.getValue(xpath);
if (result instanceof NativeArray) {
// Convert JavaScript array to Collection
NativeArray arr = (NativeArray) result;
int len = (int) arr.jsGet_length();
List list = new ArrayList(len);
for (int i = 0; i<len; i++) {
Object obj = arr.get(i, arr);
if (obj==Context.getUndefinedValue()) {
obj = null;
}
list.add(obj);
}
result = list;
}
return result;
}
/**
* Resolves a nodeset selector
* into a list of concrete node locations.
* @param xpathSelector the nodeset selector
*
* @return a Set of XPath strings pointing to
* each nodeset satisfying the nodeset selector
*
* <p>
* TODO: the Collection return type should be replaced with a Set.
* LinkedHashSet implementation should be used. All resolved
* nodes are unique in the resulting set, therefore Set is more appropriate.
* Since LinkedHashSet is only available in JDK 1.4 or later, it is not
* appropriate to make the change immediately.
*/
public Collection locate(String xpathSelector) {
if (model_==null) {
throw new IllegalStateException("Form model not set");
}
List nodeset = new LinkedList();
Iterator iter = jxcontext_.iteratePointers(xpathSelector);
while (iter.hasNext()) {
Pointer nextPointer = (Pointer) iter.next();
String path = nextPointer.asPath();
nodeset.add(path);
}
return nodeset;
}
/**
* Performs complete validation
* of the form model.
*
*/
public boolean validate() {
return validate(null);
}
/**
*
* @param phase the validation phase
*
* @return If validation finishes without any violations,
* return true otherwise return false and save
* all violations.
*/
public boolean validate(String phase) {
if (validator_==null) {
return true;
}
validator_.setProperty(Validator.PROPERTY_PHASE, phase);
List vs = validator_.validate(model_);
if (vs!=null) {
if (violations_!=null) {
violations_.addAll(vs);
} else {
if ( !vs.isEmpty()) {
violations_ = vs;
}
}
}
if (violations_==null) {
return true;
} else {
updateViolationsAsSortedSet();
return false;
}
}
/**
* Populates an HTML Form POST into the XMLForm model (JavaBean or DOM node).
*
* <p>
* Expects that all request parameter names are XPath expressions
* to attributes of the model.
* For each request parameter, finds and assigns its value to the
* JavaBean property corresponding to the parameter's name
* </p>
*
* TODO: provide a more sophisticated examples with checkboxes, multi choice,
* radio button, text area, file upload, etc.
*
* @param sitemapObjectModel
*/
public void populate(Map sitemapObjectModel) {
// clean violations_ set
clearViolations();
// let listeners know that
// population is about to start
reset();
// data format violations
// gathered during population
// For example when
// a request parameter value is "saymyname"
// while the request parameter name points to an int attribute
List pviolations = new ArrayList();
Map filteredParameters = getFilteredRequestParameters(sitemapObjectModel);
Iterator iter = filteredParameters.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
String path = (String) entry.getKey();
// filter custom request parameter
// not refering to the model
if (filterRequestParameter(path)) {
continue;
}
Object[] values = (Object[]) entry.getValue();
try {
setValue(path, values);
} catch (JXPathException ex) {
Violation v = new Violation();
v.setPath(path);
v.setMessage(ex.getMessage());
pviolations.add(v);
}
} // while
// validate form model
autoValidate(sitemapObjectModel);
// merge violation sets
if (violations_!=null) {
violations_.addAll(pviolations);
} else {
if ( !pviolations.isEmpty()) {
violations_ = pviolations;
}
}
if (violations_!=null) {
updateViolationsAsSortedSet();
}
}
/**
* Filters request parameters which are not references to model properties.
* Sets default values for parameters which were expected in the request,
* but did not arrive (e.g. check boxes).
*
* @param sitemapObjectModel
* @return filtered request parameters
*/
protected Map getFilteredRequestParameters(Map sitemapObjectModel) {
Request request = getRequest(sitemapObjectModel);
Map filteredParameters = new HashMap();
// first filter out request parameters which do not refer to model properties
Enumeration enum = request.getParameterNames();
while (enum.hasMoreElements()) {
String path = (String) enum.nextElement();
// filter custom request parameter
// not refering to the model
if (filterRequestParameter(path)) {
continue;
}
Object[] values = request.getParameterValues(path);
filteredParameters.put(path, values);
}
// now, find expected parameters which did not arrive
// and set default values for them
String viewName = getFormView(sitemapObjectModel);
Map expectedReferences = getFormViewState(viewName).getModelReferenceMap();
Iterator iter = expectedReferences.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
String propertyReference = (String) entry.getKey();
// check if the expected parameter actually arrived in the request
if (filteredParameters.get(propertyReference)==null) {
// Since it is not there, try to provide a default value
String inputType = (String) entry.getValue();
Object defaultValue = null;
if (inputType.equals(XMLFormTransformer.TAG_SELECTBOOLEAN)) {
// false for boolean type (usually, single check-box)
defaultValue = new Object[]{ Boolean.FALSE };
} else if (inputType.equals(XMLFormTransformer.TAG_SELECTMANY)) {
// empty array for select many (usually, multi check-box)
defaultValue = new Object[0];
} else {
// for all the rest, use a blank value and hope for the best
defaultValue = new Object[]{ "" };
}
filteredParameters.put(propertyReference, defaultValue);
}
} // iterate over expectedReferences.entrySet()
return filteredParameters;
} // getFilteredRequestParameters
/**
* Create a SortedSet view of the violations collection
* for convenience of processors down the pipeline
* protected void updateViolationsAsSortedSet()
*/
protected void updateViolationsAsSortedSet() {
violationsAsSortedSet_ = new TreeSet(violations_);
}
/**
* Convenience method invoked after populate()
* By default it performs Form model validation.
*
* <br>
* - If default validation is not necessary
* setAutoValidate( false ) should be used
*
* <br>
* If the validation
* criteria needs to be different, subclasses can override
* this method to change the behaviour.
*
* @param sitemapObjectModel
*/
protected void autoValidate(Map sitemapObjectModel) {
if ( !autoValidateEnabled_) {
return;
}
// perform validation for the phase
// which matches the name of the current form view
// if one is available
String formView = getFormView(sitemapObjectModel);
if (formView!=null) {
validate(formView);
}
}
/**
* Filters custom request parameter not refering to the model.
*
* TODO: implement default filtering
* for standard Cocoon parameters
* like cocoon-action[-suffix]
*
* @param name
*
*/
protected boolean filterRequestParameter(String name) {
// filter standard cocoon-* parameters
if (filterDefaultRequestParameter(name)) {
return true;
}
// then consult with FormListeners
Set ls = new HashSet();
ls.addAll(Collections.synchronizedSet(formListeners_));
Iterator iter = ls.iterator();
while (iter.hasNext()) {
FormListener fl = (FormListener) iter.next();
// if any of the listeners wants this parameter filtered
// then filter it (return true)
if (fl.filterRequestParameter(this, name)) {
return true;
}
}
// if none of the listeners wants this parameter filtered
// then don't filter it
return false;
}
/**
* Filters the standard cocoon request parameters.
* If default filtering needs to be different,
* subclasses can override this method.
* It is invoked before all listeners are asked to filter the parameter
*
* @param paramName
*
*/
protected boolean filterDefaultRequestParameter(String paramName) {
// Forbid parameters containing parenthesis to avoid method-call injection
if (paramName.indexOf('(') != -1) {
return true;
}
if (paramName.startsWith(Constants.ACTION_PARAM_PREFIX) ||
paramName.startsWith(Constants.VIEW_PARAM)) {
return true;
}
if (paramName.equals(FORM_VIEW_PARAM)) {
return true;
} else {
return false;
}
}
/**
* Try to extract from the request
* and return the current form view
*
* @param sitemapObjectModel
*
*/
public String getFormView(Map sitemapObjectModel) {
return getRequest(sitemapObjectModel).getParameter(Form.FORM_VIEW_PARAM);
}
/**
* This method is called before
* the form is populated with request parameters.
*
* Semantically similar to that of the
* ActionForm.reset() in Struts
*
* Can be used for clearing checkbox fields,
* because the browser will not send them when
* not checked.
*
* Calls reset on all FormListeners
*/
protected void reset() {
// notify FormListeners
Set ls = new HashSet();
ls.addAll(Collections.synchronizedSet(formListeners_));
Iterator iter = ls.iterator();
while (iter.hasNext()) {
FormListener fl = (FormListener) iter.next();
fl.reset(this);
}
return;
}
/**
* Loads a form from the request or session
*
* @param sitemapObjectModel
* @param id the form id
*
*/
public static Form lookup(Map sitemapObjectModel, String id) {
Request request = getRequest(sitemapObjectModel);
Form form = (Form) request.getAttribute(id);
if (form!=null) {
return form;
} else {
Session session = request.getSession(false);
if (session!=null) {
form = (Form) session.getAttribute(id);
}
return form;
}
}
/**
* Removes a form from the request and session.
* This method will remove the attribute bindings
* correspoding to the form id from both request
* and session to ensure that a subsequent
* Form.lookup will not succeed.
*
* @param sitemapObjectModel
* @param id the form id
*/
public static void remove(Map sitemapObjectModel, String id) {
Request request = getRequest(sitemapObjectModel);
request.removeAttribute(id);
Session session = request.getSession(false);
if (session!=null) {
session.removeAttribute(id);
}
}
/**
* Saves the form in the request or session.
*
* @param sitemapObjectModel
* @param scope if true the form will be bound in the session, otherwise request
*/
public void save(Map sitemapObjectModel, String scope) {
Request request = getRequest(sitemapObjectModel);
if (lookup(sitemapObjectModel, id_)!=null) {
throw new java.lang.IllegalStateException("Form [id="+id_+
"] already bound in request or session ");
}
if (SCOPE_REQUEST.equals(scope)) {
request.setAttribute(id_, this);
} else // session scope
{
Session session = request.getSession(true);
session.setAttribute(id_, this);
}
}
/**
* Add another FormListener.
*/
public synchronized void addFormListener(FormListener formListener) {
formListeners_.add(formListener);
}
/**
* Add another FormListener
*/
public synchronized void removeFormListener(FormListener formListener) {
formListeners_.remove(formListener);
}
protected final static Request getRequest(Map sitemapObjectModel) {
return (Request) sitemapObjectModel.get(ObjectModelHelper.REQUEST_OBJECT);
}
public void setAutoValidate(boolean newAVFlag) {
autoValidateEnabled_ = newAVFlag;
}
/**
* <pre>
* When the transformer renders a form view,
* it lets the form wrapper know about each referenced model property.
* This allows a precise tracking and can be used for multiple reasons:
* 1) Verify that the client does not temper with the input fields as specified by the
* form view author
* 2) Allow default values to be used for properties which were expected to be send by the client,
* but for some reason were not. A typical example is a check box. When unchecked, the browser
* does not send any request parameter, leaving it to the server to handle the situation.
* This proves to be a very error prone problem when solved on a case by case basis.
* By having a list of expected property references, the model populator can detect
* a checkbox which was not send and set the property value to false.
*
* NOTE: This added functionality is ONLY useful for SESSION scope forms.
* Request scope forms are constructed anew for every request and therefore
* cannot benefit from this extra feature.
* With the high performance CPUs and cheap memory used in today's servers,
* session scope forms are a safe choice.
* </pre>
*
* @param currentFormView
* @param ref
* @param inputType
*/
public void saveExpectedModelReferenceForView(String currentFormView,
String ref, String inputType) {
// if the form view is null, we are not interested in saving any references
if (currentFormView==null) {
return;
}
FormViewState formViewState = getFormViewState(currentFormView);
formViewState.addModelReferenceAndInputType(ref, inputType);
}
/**
* When the transformer starts rendering a new form element.
* It needs to reset previously saved references for another
* transformation of the same view.
*
* @param currentFormView
*/
public void clearSavedModelReferences(String currentFormView) {
FormViewState formViewState = getFormViewState(currentFormView);
formViewState.clear();
}
/**
* We keep a map of ViewState objects which store
* all references to model properties in a particular form view
* which were rendered by the
* XMLFormTansformer in the most recent transformation.
*
* @param viewName
*
*/
protected FormViewState getFormViewState(String viewName) {
FormViewState formViewState = (FormViewState) viewStateMap_.get(viewName);
if (formViewState==null) {
formViewState = new FormViewState();
viewStateMap_.put(viewName, formViewState);
}
return formViewState;
}
/**
* Internal class used for keeping state information
* during the life cycle of a form.
*
* <p>Used only for session scoped forms
*/
class FormViewState {
private Map modelReferences_ = new HashMap();
FormViewState() {
}
/**
*
* @return Map of (String modelPropertyReference, String inputType) pairs
*/
Map getModelReferenceMap() {
return modelReferences_;
}
void addModelReferenceAndInputType(String modelPropertyReference,
String inputType) {
modelReferences_.put(modelPropertyReference, inputType);
}
void clear() {
modelReferences_.clear();
}
}
/** the set of violations the model commited during validation */
private List violations_ = null;
/** another view of the violations_ collection */
private SortedSet violationsAsSortedSet_ = null;
/** flag allowing control over automatic validation on populate() */
private boolean autoValidateEnabled_ = true;
/** The data model this form encapsulates */
private Object model_ = null;
/** The list of FormListeners */
private Set formListeners_ = new HashSet();
/**
* The unique identifier for this form. Used when form is stored in request
* or session for reference by other components
*
* <p>
* TODO: a centralized form registry would be helpful to prevent from id collision
*/
private String id_ = null;
/**
* The JXPath context associated with the model.
* Used to traverse the model with XPath expressions
*/
private JXPathContext jxcontext_ = null;
/**
* Used to validate the content of the model
* at various phases.
*/
private Validator validator_ = null;
/**
* Keeps a state information for
* each form view that has been processed.
*/
private Map viewStateMap_ = new HashMap();
}