| /* |
| * 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.util; |
| |
| import java.util.Iterator; |
| import java.util.Locale; |
| |
| import org.apache.cocoon.xml.AbstractXMLConsumer; |
| import org.apache.cocoon.forms.datatype.Datatype; |
| import org.apache.cocoon.forms.datatype.convertor.ConversionResult; |
| import org.apache.cocoon.forms.formmodel.Action; |
| import org.apache.cocoon.forms.formmodel.AggregateField; |
| import org.apache.cocoon.forms.formmodel.BooleanField; |
| import org.apache.cocoon.forms.formmodel.ContainerWidget; |
| import org.apache.cocoon.forms.formmodel.DataWidget; |
| import org.apache.cocoon.forms.formmodel.Form; |
| import org.apache.cocoon.forms.formmodel.MultiValueField; |
| import org.apache.cocoon.forms.formmodel.Repeater; |
| import org.apache.cocoon.forms.formmodel.Widget; |
| import org.apache.excalibur.xml.sax.XMLizable; |
| import org.xml.sax.Attributes; |
| import org.xml.sax.ContentHandler; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.helpers.AttributesImpl; |
| |
| /** |
| * Adapter class that wraps a <code>Form</code> object and makes it |
| * possible to populate a widget hierarchy from XML in form of SAX |
| * events and serialize the content of the widget hierarchy as XML. |
| * |
| * <p>The XML format is such that there is one XML element for each |
| * widget and the element get the widgets id as name. Exceptions from |
| * this is that the elements in a repeater gets the name |
| * <code>item</code> and a attribute <code>position</code> with the |
| * position of the repeater child, instead of just a number (which is |
| * not allowed as element name). Childs of a |
| * <code>MultiValueField</code> are also embeded within a |
| * <code>item</code> element. If the <code>Form</code> widget does |
| * not have an id it get the name <code>uknown</code>.</p> |
| * |
| * <p>An <code>AggregateField</code> can both be interpreted as one value |
| * and as several widgets. This ambiguity is resolved by chosing to emit |
| * the single value rather than the fields as XML. For population of the |
| * form both forms are however allowed.</p> |
| * |
| * @version $Id$ |
| */ |
| public class XMLAdapter extends AbstractXMLConsumer implements XMLizable { |
| |
| /** Name of element in list. */ |
| private final static String ITEM = "item"; |
| /** Name of unkown element. */ |
| private final static String UNKNOWN = "unknown"; |
| /** Name of position attribute in list. */ |
| private final static String POSITION = "position"; |
| /** The namespace prefix of this component. */ |
| private final static String PREFIX = ""; |
| /** The namespace URI of this component. */ |
| private final static String URI = ""; |
| |
| /** The <code>ContentHandler</code> receiving SAX events. */ |
| private ContentHandler contentHandler; |
| /** The <code>Widget</code> to read and write XML to. */ |
| private Widget widget; |
| /** The <code>Widget</code> that we are currently writing to. */ |
| private Widget currentWidget; |
| /** The <code>Locale</code> that decides how to convert widget values to strings */ |
| private Locale locale; |
| /** Is a <code>MultiValueField</code> handled? */ |
| private boolean isMultiValueItem; |
| /** The buffer used to receive character events */ |
| private StringBuffer textBuffer; |
| |
| |
| /** |
| * Wrap a <code>Form</code> with an <code>XMLAdapter</code> |
| */ |
| public XMLAdapter(Widget widget) { |
| this.widget = widget; |
| this.locale = Locale.US; |
| } |
| |
| /** |
| * Set the locale used for conversion between XML data and Java objects |
| */ |
| public void setLocale(Locale locale) { |
| this.locale = locale; |
| } |
| |
| /** |
| * Get the locale used for conversion between XML data and Java objects |
| */ |
| public Locale getLocale() { |
| return this.locale; |
| } |
| |
| /* ================ SAX -> Widget ================ */ |
| |
| /* |
| * The current state during handling of input events is described |
| * by <code>currentWidget</code> that points to the widget that is |
| * beeing populated. The state that the population has not began |
| * yet or that it is finished is encoded by setting |
| * <code>currentWidget</code> to <code>null</code> and the state of |
| * being within a <code>item</code> within a |
| * <code>MultiValueField</code> is encoded by setting the variable |
| * <code>isMultiValueItem</code> to true. |
| */ |
| |
| /** |
| * Receive notification of the beginning of an element. |
| * |
| * @param uri The Namespace URI, or the empty string if the element has no |
| * Namespace URI or if Namespace |
| * processing is not being performed. |
| * @param loc The local name (without prefix), or the empty string if |
| * Namespace processing is not being performed. |
| * @param raw The raw XML 1.0 name (with prefix), or the empty string if |
| * raw names are not available. |
| * @param a The attributes attached to the element. If there are no |
| * attributes, it shall be an empty Attributes object. |
| */ |
| public void startElement(String uri, String loc, String raw, Attributes a) |
| throws SAXException { |
| handleText(); |
| |
| if (this.currentWidget == null) { |
| // The name of the root element is ignored |
| this.currentWidget = this.widget; |
| |
| } else if (this.currentWidget instanceof ContainerWidget) { |
| Widget child = ((ContainerWidget)this.currentWidget).getChild(loc); |
| if (child == null) { |
| throw new SAXException("There is no widget with id: " + loc + |
| " as child to: " + this.currentWidget.getId()); |
| } |
| this.currentWidget = child; |
| |
| } else if (this.currentWidget instanceof Repeater) { |
| // In a repeater the XML elements are added in the order |
| // they are recieved, the position attribute is not used |
| if (!ITEM.equals(loc)) { |
| throw new SAXException("The element: " + loc + |
| " is not allowed as a direct child of a Repeater"); |
| } |
| Repeater repeater = (Repeater) currentWidget; |
| this.currentWidget = repeater.addRow(); |
| |
| } else if (this.currentWidget instanceof MultiValueField) { |
| this.isMultiValueItem = true; |
| if (!ITEM.equals(loc)) { |
| throw new SAXException("The element: " + loc + |
| " is not allowed as a direct child of a MultiValueField"); |
| } |
| } |
| } |
| |
| /** |
| * Receive notification of the end of an element. |
| * |
| * @param uri The Namespace URI, or the empty string if the element has no |
| * Namespace URI or if Namespace |
| * processing is not being performed. |
| * @param loc The local name (without prefix), or the empty string if |
| * Namespace processing is not being performed. |
| * @param raw The raw XML 1.0 name (with prefix), or the empty string if |
| * raw names are not available. |
| */ |
| public void endElement(String uri, String loc, String raw) |
| throws SAXException { |
| handleText(); |
| if (this.currentWidget == null) |
| throw new SAXException("Wrong state"); |
| |
| String id = this.currentWidget.getId(); |
| |
| if (this.currentWidget instanceof Form) { |
| this.currentWidget = null; |
| return; |
| } else if (this.currentWidget instanceof AggregateField) { |
| ((AggregateField)this.currentWidget).combineFields(); |
| } else if (this.currentWidget instanceof Repeater.RepeaterRow) { |
| id = ITEM; |
| } else if (this.currentWidget instanceof MultiValueField && loc.equals(ITEM)) { |
| this.isMultiValueItem = false; |
| return; |
| } |
| |
| if (loc.equals(id)) |
| this.currentWidget = this.currentWidget.getParent(); |
| else |
| throw new SAXException("Unexpected element, was: " + loc + |
| " expected: " + id); |
| } |
| |
| /** |
| * Receive notification of character data. |
| * |
| * @param ch The characters from the XML document. |
| * @param start The start position in the array. |
| * @param len The number of characters to read from the array. |
| */ |
| public void characters(char ch[], int start, int len) |
| throws SAXException { |
| // Buffer text, as a single text node can be sent in several chunks. |
| if (this.textBuffer == null) { |
| this.textBuffer = new StringBuffer(); |
| } |
| this.textBuffer.append(ch, start, len); |
| } |
| |
| /** |
| * Handle text nodes, if any. Called on every potential text node boundary, |
| * i.e. start and end element events. |
| * |
| * @throws SAXException |
| */ |
| private void handleText() throws SAXException { |
| if (this.textBuffer == null) |
| return; |
| |
| String input = this.textBuffer.toString().trim(); |
| this.textBuffer = null; // clear buffer |
| if (input.length() == 0) |
| return; |
| |
| if (this.currentWidget instanceof MultiValueField && isMultiValueItem) { |
| MultiValueField field = (MultiValueField) this.currentWidget; |
| Datatype type = field.getDatatype(); |
| ConversionResult conv = type.convertFromString(input, this.locale); |
| if (!conv.isSuccessful()) { |
| throw new SAXException("Could not convert: " + input + |
| " to " + type.getTypeClass()); |
| } |
| Object[] values = (Object[]) field.getValue(); |
| int valLen = values == null ? 0 : values.length; |
| Object[] newValues = new Object[valLen + 1]; |
| for (int i = 0; i < valLen; i++) { |
| newValues[i] = values[i]; |
| } |
| newValues[valLen] = conv.getResult(); |
| field.setValues(newValues); |
| } else if (this.currentWidget instanceof DataWidget) { |
| DataWidget data = (DataWidget) this.currentWidget; |
| Datatype type = data.getDatatype(); |
| ConversionResult conv = type.convertFromString(input, this.locale); |
| if (!conv.isSuccessful()) { |
| throw new SAXException("Could not convert: " + input + |
| " to " + type.getTypeClass()); |
| } |
| data.setValue(conv.getResult()); |
| } else if (this.currentWidget instanceof BooleanField) { |
| // FIXME: BooleanField should implement DataWidget, which |
| // would make this case unnecessary |
| if ("true".equals(input)) |
| this.currentWidget.setValue(Boolean.TRUE); |
| else if ("false".equals(input)) |
| this.currentWidget.setValue(Boolean.FALSE); |
| else |
| throw new SAXException("Unkown boolean: " + input); |
| } else { |
| throw new SAXException("Unknown widget type: " + this.currentWidget); |
| } |
| } |
| |
| /* ================ Widget -> SAX ================ */ |
| |
| /* |
| * Just recurses in deep first order over the widget hierarchy and |
| * emits XML |
| */ |
| |
| /** |
| * Generates SAX events representing the object's state. |
| */ |
| public void toSAX( ContentHandler handler ) throws SAXException { |
| this.contentHandler = handler; |
| |
| this.contentHandler.startDocument(); |
| this.contentHandler.startPrefixMapping(PREFIX, URI); |
| |
| generateSAX(this.widget); |
| |
| this.contentHandler.endPrefixMapping(PREFIX); |
| this.contentHandler.endDocument(); |
| } |
| |
| /** |
| * Generate XML data. |
| */ |
| private void generateSAX(Widget widget) |
| throws SAXException { |
| generateSAX(widget, null); |
| } |
| |
| private void generateSAX(Widget widget, String id) |
| throws SAXException { |
| |
| // no XML output for actions |
| if (widget instanceof Action) |
| return; |
| |
| if (id == null) |
| id = widget.getId().length() == 0 ? UNKNOWN : widget.getId(); |
| |
| final AttributesImpl attr = new AttributesImpl(); |
| if (widget instanceof Repeater.RepeaterRow) |
| attribute(attr, POSITION, widget.getId()); |
| |
| start(id, attr); |
| // Placing the handling DataWidget before ContainerWidget |
| // means that an AggregateField is handled like a DataWidget |
| if (widget instanceof MultiValueField) { |
| Datatype datatype = ((MultiValueField)widget).getDatatype(); |
| Object[] values = (Object[])widget.getValue(); |
| if (values != null) |
| for (int i = 0; i < values.length; i++) { |
| start(ITEM, attr); |
| data(datatype.convertToString(values[i], this.locale)); |
| end(ITEM); |
| } |
| } else if (widget instanceof DataWidget) { |
| Datatype datatype = ((DataWidget)widget).getDatatype(); |
| if (widget.getValue() != null) |
| data(datatype.convertToString(widget.getValue(), this.locale)); |
| } else if (widget instanceof BooleanField) { |
| // FIXME: BooleanField should implement DataWidget, which |
| // would make this case unnecessary |
| if (widget.getValue() != null) { |
| data(widget.getValue().toString()); |
| } |
| } else if (widget instanceof ContainerWidget) { |
| Iterator children = ((ContainerWidget)widget).getChildren(); |
| while (children.hasNext()) |
| generateSAX((Widget)children.next()); |
| } else if (widget instanceof Repeater) { |
| Repeater repeater = (Repeater)widget; |
| for (int i = 0; i < repeater.getSize(); i++) |
| generateSAX(repeater.getRow(i), ITEM); |
| } |
| end(id); |
| } |
| |
| private void attribute(AttributesImpl attr, String name, String value) { |
| attr.addAttribute("", name, name, "CDATA", value); |
| } |
| |
| private void start(String name, AttributesImpl attr) |
| throws SAXException { |
| String qName = PREFIX == "" ? name : PREFIX + ":" + name; |
| this.contentHandler.startElement(URI, name, qName, attr); |
| attr.clear(); |
| } |
| |
| private void end(String name) |
| throws SAXException { |
| String qName = PREFIX == "" ? name : PREFIX + ":" + name; |
| this.contentHandler.endElement(URI, name, qName); |
| } |
| |
| private void data(String data) |
| throws SAXException { |
| this.contentHandler.characters(data.toCharArray(), 0, data.length()); |
| } |
| } |