/* * 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.woody.transformation; import org.apache.avalon.excalibur.pool.Recyclable; import org.apache.cocoon.i18n.I18nUtils; import org.apache.cocoon.woody.Constants; import org.apache.cocoon.woody.formmodel.AggregateField; import org.apache.cocoon.woody.formmodel.Repeater; import org.apache.cocoon.woody.formmodel.Struct; import org.apache.cocoon.woody.formmodel.Union; import org.apache.cocoon.woody.formmodel.Widget; import org.apache.cocoon.woody.validation.ValidationError; import org.apache.cocoon.woody.validation.ValidationErrorAware; import org.apache.cocoon.xml.AbstractXMLPipe; import org.apache.cocoon.xml.SaxBuffer; import org.apache.commons.jxpath.JXPathException; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import org.xml.sax.ext.LexicalHandler; import org.xml.sax.helpers.AttributesImpl; import java.util.HashMap; import java.util.LinkedList; import java.util.Locale; import java.util.Map; // TODO: Reduce the Element creation and deletion churn by using startElement // and endElement methods which do not create or use Elements on the stack. // The corresponding TODO in the EffectPipe needs to be completed first. /** * The basic operation of this Pipe is that it replaces wt:widget (in the * {@link Constants#WT_NS} namespace) tags (having an id attribute) * by the XML representation of the corresponding widget instance. * * <p>These XML fragments (normally all in the {@link Constants#WI_NS "Woody Instance"} namespace), can * then be translated to a HTML presentation by an XSL. This XSL will then only have to style * individual widget, and will not need to do the whole page layout. * * <p>For more information about the supported tags and their function, see the user documentation * for the woody template transformer.</p> * * @author Timothy Larson * @version CVS $Id$ */ public class EffectWidgetReplacingPipe extends EffectPipe { /** * Form location attribute on <code>wt:form-template</code> element, containing * JXPath expression which should result in Form object. * * @see WoodyPipelineConfig#findForm(String) */ private static final String LOCATION = "location"; private static final String CLASS = "class"; private static final String CONTINUATION_ID = "continuation-id"; private static final String FORM_TEMPLATE_EL = "form-template"; private static final String NEW = "new"; private static final String REPEATER_SIZE = "repeater-size"; private static final String REPEATER_WIDGET = "repeater-widget"; private static final String REPEATER_WIDGET_LABEL = "repeater-widget-label"; private static final String AGGREGATE_WIDGET = "aggregate-widget"; private static final String STRUCT = "struct"; private static final String STYLING_EL = "styling"; private static final String UNION = "union"; private static final String VALIDATION_ERROR = "validation-error"; private static final String WIDGET_LABEL = "widget-label"; private static final String WIDGET = "widget"; protected Widget contextWidget; protected LinkedList contextWidgets; protected String widgetId; protected Widget widget; protected Map classes; private final DocHandler docHandler = new DocHandler(); private final FormHandler formHandler = new FormHandler(); private final NestedHandler nestedHandler = new NestedHandler(); private final WidgetLabelHandler widgetLabelHandler = new WidgetLabelHandler(); private final WidgetHandler widgetHandler = new WidgetHandler(); private final RepeaterSizeHandler repeaterSizeHandler = new RepeaterSizeHandler(); private final RepeaterWidgetLabelHandler repeaterWidgetLabelHandler = new RepeaterWidgetLabelHandler(); private final RepeaterWidgetHandler repeaterWidgetHandler = new RepeaterWidgetHandler(); private final AggregateWidgetHandler aggregateWidgetHandler = new AggregateWidgetHandler(); private final StructHandler structHandler = new StructHandler(); private final UnionHandler unionHandler = new UnionHandler(); private final UnionPassThruHandler unionPassThruHandler = new UnionPassThruHandler(); private final NewHandler newHandler = new NewHandler(); private final ClassHandler classHandler = new ClassHandler(); private final ContinuationIdHandler continuationIdHandler = new ContinuationIdHandler(); private final StylingContentHandler stylingHandler = new StylingContentHandler(); private final ValidationErrorHandler validationErrorHandler = new ValidationErrorHandler(); /** * Map containing all handlers */ private final Map templates = new HashMap(12, 1); protected WoodyPipelineConfig pipeContext; /** * Have we encountered a <wi:style> element in a widget ? */ protected boolean gotStylingElement; /** * Namespace prefix used for the namespace <code>Constants.WT_NS</code>. */ protected String namespacePrefix; public EffectWidgetReplacingPipe() { // Setup map of templates. templates.put(WIDGET, widgetHandler); templates.put(WIDGET_LABEL, widgetLabelHandler); templates.put(REPEATER_WIDGET, repeaterWidgetHandler); templates.put(AGGREGATE_WIDGET, aggregateWidgetHandler); templates.put(REPEATER_SIZE, repeaterSizeHandler); templates.put(REPEATER_WIDGET_LABEL, repeaterWidgetLabelHandler); templates.put(STRUCT, structHandler); templates.put(UNION, unionHandler); templates.put(NEW, newHandler); templates.put(CLASS, classHandler); templates.put(CONTINUATION_ID, continuationIdHandler); templates.put(VALIDATION_ERROR, validationErrorHandler); } private void throwSAXException(String message) throws SAXException{ throw new SAXException("EffectWoodyTemplateTransformer: " + message); } public void init(Widget contextWidget, WoodyPipelineConfig pipeContext) { super.init(); this.pipeContext = pipeContext; // Attach document handler handler = docHandler; // Initialize widget related variables contextWidgets = new LinkedList(); classes = new HashMap(); } protected String getWidgetId(Attributes attributes) throws SAXException { String widgetId = attributes.getValue("id"); if (widgetId == null || widgetId.length() == 0) { throwSAXException("Missing required widget \"id\" attribute."); } return widgetId; } protected Widget getWidget(String widgetId) throws SAXException { Widget widget = contextWidget.getWidget(widgetId); if (widget == null) { if (contextWidget.getFullyQualifiedId() == null) { throwSAXException("Widget with id \"" + widgetId + "\" does not exist in the form container."); } else { throwSAXException("Widget with id \"" + widgetId + "\" does not exist in the container \"" + contextWidget.getFullyQualifiedId() + "\""); } } return widget; } protected void getRepeaterWidget(String handler) throws SAXException { widgetId = getWidgetId(input.attrs); widget = getWidget(widgetId); if (!(widget instanceof Repeater)) { throwWrongWidgetType("RepeaterWidgetLabelHandler", input.loc, "repeater"); } } public void throwWrongWidgetType(String pipeName, String element, String widget) throws SAXException { throwSAXException(pipeName + ": Element \"" + element + "\" can only be used for " + widget + " widgets."); } /** * Needed to get things working with JDK 1.3. Can be removed once we * don't support that platform any more. */ private ContentHandler getContentHandler() { return this.contentHandler; } /** * Needed to get things working with JDK 1.3. Can be removed once we * don't support that platform any more. */ private LexicalHandler getLexicalHandler() { return this.lexicalHandler; } public Handler nestedTemplate() throws SAXException { if (Constants.WT_NS.equals(input.uri)) { // Element in woody template namespace. Handler handler = (Handler)templates.get(input.loc); if (handler != null) { return handler; } else if (FORM_TEMPLATE_EL.equals(input.loc)) { throwSAXException("Element \"form-template\" must not be nested."); return null; // Keep the compiler happy. } else { throwSAXException("Unrecognized template: " + input.loc); return null; // Keep the compiler happy. } } else { // Element not in woody namespace. return nestedHandler; } } //============================================== // Handler classes to transform Woody templates //============================================== protected class DocHandler extends Handler { public Handler process() throws SAXException { switch (event) { case EVENT_SET_DOCUMENT_LOCATOR: return this; case EVENT_START_PREFIX_MAPPING: // We consume this namespace completely EffectWidgetReplacingPipe.this.namespacePrefix = input.prefix; return this; case EVENT_ELEMENT: if (Constants.WT_NS.equals(input.uri)) { if (FORM_TEMPLATE_EL.equals(input.loc)) { return formHandler; } else { throwSAXException("Woody template \"" + input.loc + "\" not permitted outside \"form-template\""); } } else { return this; } case EVENT_END_PREFIX_MAPPING: // We consume this namespace completely return this; default: out.copy(); return this; } } } protected class FormHandler extends Handler { public Handler process() throws SAXException { switch(event) { case EVENT_START_ELEMENT: if (contextWidget != null) { throwSAXException("Detected nested wt:form-template elements, this is not allowed."); } out.startPrefixMapping(Constants.WI_PREFIX, Constants.WI_NS); // ====> Retrieve the form // First look for the form using the location attribute, if any String formJXPath = input.attrs.getValue(LOCATION); if (formJXPath != null) { // remove the location attribute AttributesImpl attrsCopy = new AttributesImpl(input.attrs); attrsCopy.removeAttribute(input.attrs.getIndex(LOCATION)); input.attrs = attrsCopy; input.mine = true; } contextWidget = pipeContext.findForm(formJXPath); // ====> Determine the Locale //TODO pull this locale stuff also up in the Config object? String localeAttr = input.attrs.getValue("locale"); if (localeAttr != null) { // first use value of locale attribute if any localeAttr = pipeContext.translateText(localeAttr); pipeContext.setLocale(I18nUtils.parseLocale(localeAttr)); } else if (pipeContext.getLocaleParameter() != null) { // then use locale specified as transformer parameter, if any pipeContext.setLocale(pipeContext.getLocaleParameter()); } else { //TODO pull this locale stuff also up in the Config object? // use locale specified in bizdata supplied for form Object locale = null; try { locale = pipeContext.evaluateExpression("/locale"); } catch (JXPathException e) {} if (locale != null) { pipeContext.setLocale((Locale)locale); } else { // final solution: use locale defined in the server machine pipeContext.setLocale(Locale.getDefault()); } } String[] namesToTranslate = {"action"}; Attributes transAttrs = translateAttributes(input.attrs, namesToTranslate); out.element(Constants.WI_PREFIX, Constants.WI_NS, "form-template", transAttrs); out.startElement(); return this; case EVENT_ELEMENT: return nestedTemplate(); case EVENT_END_ELEMENT: out.copy(); out.endPrefixMapping(Constants.WI_PREFIX); contextWidget = null; return this; default: out.copy(); return this; } } } protected class NestedHandler extends Handler { public Handler process() throws SAXException { switch(event) { case EVENT_ELEMENT: return nestedTemplate(); default: out.copy(); return this; } } } protected class WidgetLabelHandler extends Handler { public Handler process() throws SAXException { switch (event) { case EVENT_START_ELEMENT: widgetId = getWidgetId(input.attrs); Widget widget = getWidget(widgetId); widget.generateLabel(getContentHandler()); widget = null; return this; case EVENT_ELEMENT: return nullHandler; case EVENT_END_ELEMENT: return this; default: out.copy(); return this; } } } protected class WidgetHandler extends Handler { public Handler process() throws SAXException { switch (event) { case EVENT_START_ELEMENT: widgetId = getWidgetId(input.attrs); widget = getWidget(widgetId); gotStylingElement = false; out.bufferInit(); return this; case EVENT_ELEMENT: if (Constants.WI_NS.equals(input.uri) && STYLING_EL.equals(input.loc)) { gotStylingElement = true; } return bufferHandler; case EVENT_END_ELEMENT: stylingHandler.recycle(); stylingHandler.setSaxFragment(out.getBuffer()); stylingHandler.setContentHandler(getContentHandler()); stylingHandler.setLexicalHandler(getLexicalHandler()); widget.generateSaxFragment(stylingHandler, pipeContext.getLocale()); widget = null; out.bufferFini(); return this; default: out.copy(); return this; } } } protected class RepeaterSizeHandler extends Handler { public Handler process() throws SAXException { switch(event) { case EVENT_START_ELEMENT: getRepeaterWidget("RepeaterSizeHandler"); ((Repeater)widget).generateSize(getContentHandler()); widget = null; return this; case EVENT_ELEMENT: return nullHandler; case EVENT_END_ELEMENT: return this; default: out.copy(); return this; } } } protected class RepeaterWidgetLabelHandler extends Handler { public Handler process() throws SAXException { switch(event) { case EVENT_START_ELEMENT: getRepeaterWidget("RepeaterWidgetLabelHandler"); String widgetId = input.attrs.getValue("widget-id"); if (widgetId == null || widgetId.length() == 0) throwSAXException("Element repeater-widget-label missing required widget-id attribute."); ((Repeater)widget).generateWidgetLabel(widgetId, getContentHandler()); widget = null; return this; case EVENT_ELEMENT: return nullHandler; case EVENT_END_ELEMENT: return this; default: out.copy(); return this; } } } protected class RepeaterWidgetHandler extends Handler { public Handler process() throws SAXException { switch(event) { case EVENT_START_ELEMENT: getRepeaterWidget("RepeaterWidgetHandler"); out.bufferInit(); return this; case EVENT_ELEMENT: return bufferHandler; case EVENT_END_ELEMENT: Repeater repeater = (Repeater)widget; int rowCount = repeater.getSize(); handlers.addFirst(handler); handler = nestedHandler; contextWidgets.addFirst(contextWidget); for (int i = 0; i < rowCount; i++) { Repeater.RepeaterRow row = repeater.getRow(i); contextWidget = row; out.getBuffer().toSAX(EffectWidgetReplacingPipe.this); } contextWidget = (Widget)contextWidgets.removeFirst(); handler = (Handler)handlers.removeFirst(); widget = null; out.bufferFini(); return this; default: out.buffer(); return this; } } } protected class AggregateWidgetHandler extends Handler { public Handler process() throws SAXException { switch(event) { case EVENT_START_ELEMENT: widgetId = getWidgetId(input.attrs); widget = getWidget(widgetId); if (!(widget instanceof AggregateField)) { throwWrongWidgetType("AggregateWidgetHandler", input.loc, "aggregate"); } contextWidgets.addFirst(contextWidget); contextWidget = widget; return this; case EVENT_ELEMENT: return nestedTemplate(); case EVENT_END_ELEMENT: contextWidget = (Widget)contextWidgets.removeFirst(); return this; default: out.copy(); return this; } } } protected class StructHandler extends Handler { public Handler process() throws SAXException { switch(event) { case EVENT_START_ELEMENT: widgetId = getWidgetId(input.attrs); widget = getWidget(widgetId); if (!(widget instanceof Struct)) { throwWrongWidgetType("StructHandler", input.loc, "struct"); } contextWidgets.addFirst(contextWidget); contextWidget = widget; out.element(Constants.WI_PREFIX, Constants.WI_NS, "struct"); out.attributes(); out.startElement(); return this; case EVENT_ELEMENT: return nestedTemplate(); case EVENT_END_ELEMENT: out.copy(); contextWidget = (Widget)contextWidgets.removeFirst(); return this; default: out.copy(); return this; } } } protected class UnionHandler extends Handler { public Handler process() throws SAXException { switch(event) { case EVENT_START_ELEMENT: widgetId = getWidgetId(input.attrs); widget = getWidget(widgetId); if (!(widget instanceof Union)) { throwWrongWidgetType("UnionHandler", input.loc, "union"); } contextWidgets.addFirst(contextWidget); contextWidget = widget; out.element(Constants.WI_PREFIX, Constants.WI_NS, "union"); out.startElement(); return this; case EVENT_ELEMENT: if (Constants.WT_NS.equals(input.uri)) { if ("case".equals(input.loc)) { String id = input.attrs.getValue("id"); if (id == null) throwSAXException("Element \"case\" missing required \"id\" attribute."); String value = (String)contextWidget.getValue(); if (id.equals(value != null ? value : "")) { return nestedHandler; } else { return nullHandler; } } else if (FORM_TEMPLATE_EL.equals(input.loc)) { throwSAXException("Element \"form-template\" must not be nested."); } else { throwSAXException("Unrecognized template: " + input.loc); } } else { return unionPassThruHandler; } case EVENT_END_ELEMENT: out.endElement(); contextWidget = (Widget)contextWidgets.removeFirst(); return this; default: out.copy(); return this; } } } protected class UnionPassThruHandler extends Handler { public Handler process() throws SAXException { switch(event) { case EVENT_ELEMENT: if (Constants.WT_NS.equals(input.uri)) { if ("case".equals(input.loc)) { if (contextWidget.getValue().equals(input.attrs.getValue("id"))) { return nestedHandler; } else { return nullHandler; } } else if (FORM_TEMPLATE_EL.equals(input.loc)) { throwSAXException("Element \"form-template\" must not be nested."); } else { throwSAXException("Unrecognized template: " + input.loc); } } else { return this; } default: out.copy(); return this; } } } protected class NewHandler extends Handler { public Handler process() throws SAXException { switch (event) { case EVENT_START_ELEMENT: widgetId = getWidgetId(input.attrs); SaxBuffer classBuffer = (SaxBuffer)classes.get(widgetId); if (classBuffer == null) { throwSAXException("New: Class \"" + widgetId + "\" does not exist."); } handlers.addFirst(handler); handler = nestedHandler; classBuffer.toSAX(EffectWidgetReplacingPipe.this); handler = (Handler)handlers.removeFirst(); return this; case EVENT_ELEMENT: return nullHandler; case EVENT_END_ELEMENT: return this; default: out.copy(); return this; } } } protected class ClassHandler extends Handler { public Handler process() throws SAXException { switch (event) { case EVENT_START_ELEMENT: widgetId = getWidgetId(input.attrs); out.bufferInit(); return this; case EVENT_ELEMENT: return bufferHandler; case EVENT_END_ELEMENT: classes.put(widgetId, out.getBuffer()); out.bufferFini(); return this; default: out.buffer(); return this; } } } protected class ContinuationIdHandler extends Handler { public Handler process() throws SAXException { switch(event) { case EVENT_START_ELEMENT: // Insert the continuation id // FIXME(SW) we could avoid costly JXPath evaluation if we had the objectmodel here. Object idObj = pipeContext.evaluateExpression("$continuation/id"); if (idObj == null) { throwSAXException("No continuation found"); } String id = idObj.toString(); out.element(Constants.WI_PREFIX, Constants.WI_NS, "continuation-id", input.attrs); out.startElement(); out.characters(id.toCharArray(), 0, id.length()); out.endElement(); return this; case EVENT_END_ELEMENT: return this; case EVENT_IGNORABLE_WHITESPACE: return this; default: throwSAXException("ContinuationIdHandler: No content allowed in \"continuation-id\" element"); return null; // Keep the compiler happy. } } } /** * This ContentHandler helps in inserting SAX events before the closing tag of the root * element. */ protected class StylingContentHandler extends AbstractXMLPipe implements Recyclable { private int elementNesting; private SaxBuffer saxBuffer; public void setSaxFragment(SaxBuffer saxFragment) { saxBuffer = saxFragment; } public void recycle() { super.recycle(); elementNesting = 0; saxBuffer = null; } public void startElement(String uri, String loc, String raw, Attributes a) throws SAXException { elementNesting++; super.startElement(uri, loc, raw, a); } public void endElement(String uri, String loc, String raw) throws SAXException { elementNesting--; if (elementNesting == 0 && saxBuffer != null) { if (gotStylingElement) { // Just deserialize saxBuffer.toSAX(getContentHandler()); } else { // Insert an enclosing <wi:styling> out.startElement(Constants.WI_NS, STYLING_EL, Constants.WI_PREFIX_COLON + STYLING_EL, Constants.EMPTY_ATTRS); saxBuffer.toSAX(getContentHandler()); out.endElement(Constants.WI_NS, STYLING_EL, Constants.WI_PREFIX_COLON + STYLING_EL); } } super.endElement(uri, loc, raw); } } /** * Inserts validation errors (if any) for the Field widgets */ protected class ValidationErrorHandler extends Handler { public Handler process() throws SAXException { switch (event) { case EVENT_START_ELEMENT: widgetId = getWidgetId(input.attrs); widget = getWidget(widgetId); out.bufferInit(); return this; case EVENT_ELEMENT: return bufferHandler; case EVENT_END_ELEMENT: if (widget instanceof ValidationErrorAware) { ValidationError error = ((ValidationErrorAware)widget).getValidationError(); if (error != null) { out.startElement(Constants.WI_NS, VALIDATION_ERROR, Constants.WI_PREFIX_COLON + VALIDATION_ERROR, Constants.EMPTY_ATTRS); error.generateSaxFragment(stylingHandler); out.endElement(Constants.WI_NS, VALIDATION_ERROR, Constants.WI_PREFIX_COLON + VALIDATION_ERROR); } } widget = null; out.bufferFini(); return this; default: out.copy(); return this; } } } private Attributes translateAttributes(Attributes attributes, String[] names) { AttributesImpl newAtts = new AttributesImpl(attributes); if (names!= null) { for (int i = 0; i < names.length; i++) { String name = names[i]; int position = newAtts.getIndex(name); String newValue = pipeContext.translateText(newAtts.getValue(position)); newAtts.setValue(position, newValue); } } return newAtts; } // /** // * Replaces JXPath expressions embedded inside #{ and } by their value. // */ // private String translateText(String original) { // StringBuffer expression; // StringBuffer translated = new StringBuffer(); // StringReader in = new StringReader(original); // int chr; // try { // while ((chr = in.read()) != -1) { // char c = (char) chr; // if (c == '#') { // chr = in.read(); // if (chr != -1) { // c = (char) chr; // if (c == '{') { // expression = new StringBuffer(); // boolean more = true; // while ( more ) { // more = false; // if ((chr = in.read()) != -1) { // c = (char)chr; // if (c != '}') { // expression.append(c); // more = true; // } else { // translated.append(evaluateExpression(expression.toString())); // } // } else { // translated.append('#').append('{').append(expression); // } // } // } // } else { // translated.append((char) chr); // } // } else { // translated.append(c); // } // } // } catch (IOException ignored) { // ignored.printStackTrace(); // } // return translated.toString(); // } // private String evaluateExpression(String expression) { // return pipeContext.evaluateExpression(expression).toString(); // } public void recycle() { super.recycle(); this.contextWidget = null; this.widget = null; this.widgetId = null; this.pipeContext = null; this.namespacePrefix = null; } }