blob: 08d74764b113361d607f5597b2a261d5710daa1e [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.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: EffectWidgetReplacingPipe.java,v 1.10 2004/03/05 13:02:34 bdelacretaz Exp $
*/
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
*/
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.equals("")) {
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.equals(""))
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;
widgetId = null;
this.namespacePrefix = null;
}
}