/* * 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.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import org.apache.cocoon.forms.FormsConstants; import org.apache.cocoon.forms.event.CreateEvent; import org.apache.cocoon.forms.event.ValueChangedListenerEnabled; import org.apache.cocoon.forms.event.WidgetEvent; import org.apache.cocoon.forms.validation.ValidationErrorAware; import org.apache.cocoon.forms.validation.WidgetValidator; import org.apache.cocoon.util.location.Location; import org.apache.cocoon.xml.AttributesImpl; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; /** * Abstract base class for Widget implementations. Provides functionality * common to many widgets. * * @version $Id$ */ public abstract class AbstractWidget implements Widget { /** * Containing parent-widget to this widget. * NOTE: avoid directly accessing this member since subclasses can mask this * property through own implemented getParent() */ private Widget parent; /** * The widget's own state */ private WidgetState state = WidgetState.ACTIVE; /** * Lazy loaded reference to the top-level form. */ private Form form; /** * Validation-rules local to the widget instance */ private List validators; /** * Storage for the widget allocated attributes */ private Map attributes; /** * The result of the last call to {@link #validate()}. */ protected boolean wasValid = true; protected AbstractWidget(AbstractWidgetDefinition definition) { this.state = definition.getState(); } /** * Called after widget's environment has been setup, * to allow for any contextual initalization, such as * looking up case widgets for union widgets. */ public void initialize() { ((AbstractWidgetDefinition)getDefinition()).widgetCreated(this); } /** * Gets the id of this widget. */ public String getId() { return getDefinition().getId(); } public String getName() { return getId(); } /** * Concrete subclasses should allow access to their underlaying Definition * through this method. * * If subclasses decide to return <code>null</code> they should also organize * own implementations of {@link #getId()}, {@link #getLocation()}, * {@link #validate()}, {@link #generateLabel(ContentHandler)} and * {@link #generateDisplayData(ContentHandler)} to avoid NPE's. * * @return the widgetDefinition from which this widget was instantiated. * (See {@link org.apache.cocoon.forms.formmodel.WidgetDefinition#createInstance()}) */ public abstract WidgetDefinition getDefinition(); /** * @return the location-information (file, line and column) where this widget was * configured. */ public Location getLocation() { return getDefinition().getLocation(); } /** * @return The parent-widget of this widget. */ // This method is final in order for other methods in this class to use this.parent public final Widget getParent() { return this.parent; } /** * Sets the parent-widget of this widget. * This is a write-once property. * * @param widget the parent-widget of this one. * @throws IllegalStateException when the parent had already been set. */ public void setParent(Widget widget) { if (this.parent != null) { throw new IllegalStateException("The parent of widget " + getRequestParameterName() + " should only be set once."); } this.parent = widget; } /** * @return the form where this widget belongs to. */ public Form getForm() { if (this.form == null) { Widget myParent = getParent(); if (myParent == null) { this.form = (Form)this; } else { this.form = myParent.getForm(); } } return this.form; } public WidgetState getState() { return this.state; } public void setState(WidgetState state) { if (state == null) { throw new IllegalArgumentException("A widget state cannot be set to null"); } this.state = state; // Update the browser getForm().addWidgetUpdate(this); } public WidgetState getCombinedState() { if (this.parent == null) { return this.state; } return WidgetState.strictest(this.state, this.parent.getCombinedState()); } // Cached param names, used to speed up execution of the method below while // still allowing ids to change (e.g. repeater rows when they are reordered). private String cachedParentParamName; private String cachedParamName; /** * Should be called when a widget's own name has changed, in order to clear * internal caches used to compute request parameters. */ protected void widgetNameChanged() { this.cachedParentParamName = null; this.cachedParamName = null; } public String getFullName() { return getRequestParameterName(); } public String getRequestParameterName() { if (this.parent == null) { return getId(); } String parentParamName = parent.getRequestParameterName(); if (parentParamName.equals(this.cachedParentParamName)) { // Parent name hasn't changed, so ours hasn't changed too return this.cachedParamName; } // Compute our name and cache it this.cachedParentParamName = parentParamName; if (this.cachedParentParamName.length() == 0) { // the top level form returns an id == "" this.cachedParamName = getId(); } else { this.cachedParamName = this.cachedParentParamName + "." + getId(); } return this.cachedParamName; } public Widget lookupWidget(String path) { if (path == null || path.length() == 0) { return this; } Widget relativeWidget; String relativePath; int sepPosition = path.indexOf("" + Widget.PATH_SEPARATOR); if (sepPosition < 0) { //last step if (path.startsWith("..")) return getParent(); return getChild(path); } else if (sepPosition == 0) { //absolute path relativeWidget = getForm(); relativePath = path.substring(1); } else { if (path.startsWith(".." + Widget.PATH_SEPARATOR)) { relativeWidget = getParent(); relativePath = path.substring(3); } else { String childId = path.substring(0, sepPosition ); relativeWidget = getChild(childId); relativePath = path.substring(sepPosition+1); } } if (relativeWidget == null) { return null; } return relativeWidget.lookupWidget(relativePath); } /** * Concrete widgets that contain actual child widgets should override to * return the actual child-widget. * * @param id of the child-widget * @return <code>null</code> if not overriden. */ protected Widget getChild(String id) { return null; } public Widget getWidget(String id) { throw new UnsupportedOperationException("getWidget(id) got deprecated from the API. \n" + "Consider using getChild(id) or even lookupWidget(path) instead."); } public Object getValue() { throw new UnsupportedOperationException("Widget " + this + " has no value, at " + getLocation()); } public void setValue(Object object) { throw new UnsupportedOperationException("Widget " + this + " has no value, at " + getLocation()); } public boolean isRequired() { return false; } /** * {@inheritDoc} * * Abstract implementation throws a {@link UnsupportedOperationException}. * Concrete subclass widgets need to override when supporting event broadcasting. */ public void broadcastEvent(WidgetEvent event) { if (event instanceof CreateEvent) { ((AbstractWidgetDefinition) getDefinition()).fireCreateEvent((CreateEvent) event); } else { throw new UnsupportedOperationException("Widget " + getRequestParameterName() + " doesn't handle events."); } } /** * Add a validator to this widget instance. * * @param validator */ public void addValidator(WidgetValidator validator) { if (this.validators == null) { this.validators = new ArrayList(); } this.validators.add(validator); } /** * Remove a validator from this widget instance * * @param validator * @return <code>true</code> if the validator was found. */ public boolean removeValidator(WidgetValidator validator) { if (this.validators != null) { return this.validators.remove(validator); } return false; } /** * @see org.apache.cocoon.forms.formmodel.Widget#validate() */ public boolean validate() { // Consider widget valid if it is not validating values. if (!getCombinedState().isValidatingValues()) { this.wasValid = true; return true; } // Test validators from the widget definition if (!getDefinition().validate(this)) { // Failed this.wasValid = false; return false; } // Definition successful, test local validators if (this.validators != null) { Iterator iter = this.validators.iterator(); while(iter.hasNext()) { WidgetValidator validator = (WidgetValidator)iter.next(); if (!validator.validate(this)) { this.wasValid = false; return false; } } } // Successful validation if (this instanceof ValidationErrorAware) { // Clear validation error if any ((ValidationErrorAware)this).setValidationError(null); } this.wasValid = true; return true; } /** * @see org.apache.cocoon.forms.formmodel.Widget#isValid() */ public boolean isValid() { return this.wasValid; } /** * {@inheritDoc} * * Delegates to the {@link #getDefinition()} to generate the 'label' part of * the display-data of this widget. * * Subclasses should override if the getDefinition can return <code>null</code> * to avoid NPE's * * @param contentHandler * @throws SAXException */ public void generateLabel(ContentHandler contentHandler) throws SAXException { if (getCombinedState().isDisplayingValues()) { getDefinition().generateDisplayData("label", contentHandler); } } /** * Generates nested additional content nested inside the main element for this * widget which is generated by {@link #generateSaxFragment(ContentHandler, Locale)} * * The implementation on the AbstractWidget level inserts no additional XML. * Subclasses need to override to insert widget specific content. * * @param contentHandler to send the SAX events to * @param locale in which context potential content needs to be put. * @throws SAXException */ protected void generateItemSaxFragment(ContentHandler contentHandler, Locale locale) throws SAXException { // Do nothing } /** * The XML element name used in {@link #generateSaxFragment(ContentHandler, Locale)} * to produce the wrapping element for all the XML-instance-content of this Widget. * * @return the main elementname for this widget's sax-fragment. */ protected abstract String getXMLElementName(); /** * The XML attributes used in {@link #generateSaxFragment(ContentHandler, Locale)} * to be placed on the wrapping element for all the XML-instance-content of this Widget. * * This automatically adds @id={@link #getRequestParameterName()} to that element. * Concrete subclasses should call super.getXMLElementAttributes and possibly * add additional attributes. * * Note: the @id is not added for those widgets who's getId() returns <code>null</code> * (e.g. top-level container widgets like 'form'). The contract of returning a non-null * {@link AttributesImpl} is however maintained. * * @return the attributes for the main element for this widget's sax-fragment. */ protected AttributesImpl getXMLElementAttributes() { AttributesImpl attrs = new AttributesImpl(); // top-level widget-containers like forms might have their id set to "" // for those the @id should not be included. if (getId().length() != 0) { attrs.addCDATAAttribute("id", getRequestParameterName()); } // Add the "state" attribute attrs.addCDATAAttribute("state", getCombinedState().getName()); // Add the "listening" attribute is the value has change listeners if (this instanceof ValueChangedListenerEnabled && ((ValueChangedListenerEnabled)this).hasValueChangedListeners()) { attrs.addCDATAAttribute("listening", "true"); } return attrs; } /** * Delegates to the {@link #getDefinition()} of this widget to generate a common * set of 'display' data. (i.e. help, label, hint,...) * * Subclasses should override if the getDefinition can return <code>null</code> * to avoid NPE's. * * @param contentHandler where to send the SAX events to. * @throws SAXException * * @see WidgetDefinition#generateDisplayData(ContentHandler) */ protected void generateDisplayData(ContentHandler contentHandler) throws SAXException { getDefinition().generateDisplayData(contentHandler); } /** * {@inheritDoc} * * This will generate some standard XML consisting of a simple wrapper * element (name provided by {@link #getXMLElementName()}) with attributes * (provided by {@link #getXMLElementAttributes()} around anything injected * in by both {@link #generateDisplayData(ContentHandler)} and * {@link #generateItemSaxFragment(ContentHandler, Locale)}. * * <pre> * <fi:{@link #getXMLElementName()} {@link #getXMLElementAttributes()} > * {@link #generateDisplayData(ContentHandler)} (i.e. help, label, ...) * * {@link #generateItemSaxFragment(ContentHandler, Locale)} * </fi:{@link #getXMLElementName()} > * </pre> * * @param contentHandler to send the SAX events to * @param locale in which context potential content needs to be put. * @throws SAXException */ public void generateSaxFragment(ContentHandler contentHandler, Locale locale) throws SAXException { if (getCombinedState().isDisplayingValues()) { // FIXME: we may want to strip out completely widgets that aren't updated when in AJAX mode String element = this.getXMLElementName(); AttributesImpl attrs = getXMLElementAttributes(); contentHandler.startElement(FormsConstants.INSTANCE_NS, element, FormsConstants.INSTANCE_PREFIX_COLON + element, attrs); generateDisplayData(contentHandler); if (locale == null) { locale = getForm().getLocale(); } generateItemSaxFragment(contentHandler, locale); contentHandler.endElement(FormsConstants.INSTANCE_NS, element, FormsConstants.INSTANCE_PREFIX_COLON + element); } else { // Generate a placeholder that can be used later by AJAX updates AttributesImpl attrs = new AttributesImpl(); attrs.addCDATAAttribute("id", getRequestParameterName()); contentHandler.startElement(FormsConstants.INSTANCE_NS, "placeholder", FormsConstants.INSTANCE_PREFIX_COLON + "placeholder", attrs); contentHandler.endElement(FormsConstants.INSTANCE_NS, "placeholder", FormsConstants.INSTANCE_PREFIX_COLON + "placeholder"); } } public Object getAttribute(String name) { Object result = null; // First check locally if (this.attributes != null) { result = this.attributes.get(name); } // Fall back to the definition's attributes if (result == null) { result = getDefinition().getAttribute(name); } return result; } public void setAttribute(String name, Object value) { if (this.attributes == null) { this.attributes = new HashMap(); } this.attributes.put(name, value); } public void removeAttribute(String name) { if (this.attributes != null) { this.attributes.remove(name); } } public String toString() { String className = this.getClass().getName(); int last = className.lastIndexOf('.'); if (last != -1) { className = className.substring(last+1); } String name = getRequestParameterName(); return name.length() == 0 ? className : className + " '" + getRequestParameterName() + "'"; } }