blob: 11150ffb709a911fe9a3de62bb2d997a022591b8 [file] [log] [blame]
/*
* 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.transformation;
import org.apache.avalon.excalibur.pool.Recyclable;
import org.apache.avalon.framework.activity.Disposable;
import org.apache.avalon.framework.component.ComponentSelector;
import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.avalon.framework.service.ServiceException;
import org.apache.avalon.framework.service.ServiceManager;
import org.apache.avalon.framework.service.ServiceSelector;
import org.apache.avalon.framework.service.Serviceable;
import org.apache.cocoon.components.sax.XMLDeserializer;
import org.apache.cocoon.components.sax.XMLSerializer;
import org.apache.cocoon.environment.SourceResolver;
import org.apache.cocoon.taglib.IterationTag;
import org.apache.cocoon.taglib.Tag;
import org.apache.cocoon.taglib.BodyTag;
import org.apache.cocoon.taglib.BodyContent;
import org.apache.cocoon.xml.AbstractXMLProducer;
import org.apache.cocoon.xml.XMLConsumer;
import org.apache.cocoon.xml.XMLProducer;
import org.apache.cocoon.xml.SaxBuffer;
import org.apache.commons.collections.ArrayStack;
import org.apache.commons.collections.map.StaticBucketMap;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* Transformer which implements the taglib functionalty.
*
* <p>Transformer processes incoming SAX events and for each element it tries to
* find {@link Tag} component with matching namespace and tag name.
*
* @author <a href="mailto:volker.schmitt@basf-it-services.com">Volker Schmitt</a>
* @version CVS $Id$
*/
public class TagTransformer
extends AbstractXMLProducer
implements Transformer, Serviceable, Configurable, Disposable, Recyclable {
private int recordingLevel;
private int skipLevel;
private String transformerHint;
private ServiceSelector transformerSelector;
private final ArrayStack tagStack = new ArrayStack();
private final ArrayStack tagSelectorStack = new ArrayStack();
private final ArrayStack tagTransformerStack = new ArrayStack();
private ServiceSelector tagNamespaceSelector;
private Tag currentTag;
/** current SAX Event Consumer */
private XMLConsumer currentConsumer;
/** backup of currentConsumer while recording */
private XMLConsumer currentConsumerBackup;
private XMLSerializer xmlSerializer;
/** The SourceResolver for this request */
private SourceResolver resolver;
/** The current objectModel of the environment */
private Map objectModel;
/** The parameters specified in the sitemap */
private Parameters parameters;
/** The Avalon ServiceManager */
private ServiceManager manager;
/** Array for dynamic calling of Tag set property methods */
private final String[] paramArray = new String[1];
/** Map for caching Tag Introspection */
private static Map TAG_PROPERTIES_MAP = new StaticBucketMap();
//
// Component Lifecycle Methods
//
/**
* Avalon Serviceable Interface
* @param manager The Avalon Service Manager
*/
public void service(ServiceManager manager) throws ServiceException {
this.manager = manager;
this.tagNamespaceSelector = (ServiceSelector) manager.lookup(Tag.ROLE + "Selector");
}
/**
* Avalon Configurable Interface
*/
public void configure(Configuration conf) throws ConfigurationException {
this.transformerHint = conf.getChild("transformer-hint").getValue(null);
if (this.transformerHint != null) {
try {
this.transformerSelector = (ServiceSelector) manager.lookup(Transformer.ROLE + "Selector");
} catch (ServiceException e) {
String message = "Can't lookup transformer selector";
if (getLogger().isDebugEnabled()) {
getLogger().debug(message, e);
}
throw new ConfigurationException(message, e);
}
}
}
/**
* Set the <code>EntityResolver</code>, objectModel <code>Map</code>,
* the source and sitemap <code>Parameters</code> used to process the request.
*/
public void setup(SourceResolver resolver, Map objectModel, String source, Parameters parameters)
throws IOException, SAXException {
this.resolver = resolver;
this.objectModel = objectModel;
this.parameters = parameters;
}
/**
* Recycle this component.
*/
public void recycle() {
this.recordingLevel = 0;
this.skipLevel = 0;
this.resolver = null;
this.objectModel = null;
this.parameters = null;
this.currentTag = null;
this.currentConsumer = null;
this.currentConsumerBackup = null;
// can happen if there was a error in the pipeline
if (xmlSerializer != null) {
manager.release(xmlSerializer);
xmlSerializer = null;
}
while (!tagStack.isEmpty()) {
Tag tag = (Tag) tagStack.pop();
if (tag == null)
continue;
ComponentSelector tagSelector = (ComponentSelector)tagSelectorStack.pop();
tagSelector.release(tag);
tagNamespaceSelector.release(tagSelector);
}
while (!tagTransformerStack.isEmpty()) {
Transformer transformer = (Transformer) tagTransformerStack.pop();
transformerSelector.release(transformer);
}
if (!tagSelectorStack.isEmpty()) {
getLogger().fatalError("recycle: internal Error, tagSelectorStack not empty");
tagSelectorStack.clear();
}
super.recycle();
}
/**
* Dispose this component.
*/
public void dispose() {
this.manager.release(tagNamespaceSelector);
tagNamespaceSelector = null;
if (transformerSelector != null) {
this.manager.release(transformerSelector);
transformerSelector = null;
}
}
/*
* @see XMLProducer#setConsumer(XMLConsumer)
*/
public void setConsumer(XMLConsumer consumer) {
this.currentConsumer = consumer;
super.setConsumer(consumer);
}
//
// SAX Events Methods
//
public void setDocumentLocator(org.xml.sax.Locator locator) {
// If we are skipping the body of a tag, ignore this...
if (this.skipLevel > 0) {
return;
}
this.currentConsumer.setDocumentLocator(locator);
}
public void startDocument() throws SAXException {
this.currentConsumer.startDocument();
}
public void endDocument() throws SAXException {
this.currentConsumer.endDocument();
}
public void processingInstruction(String target, String data) throws SAXException {
// If we are skipping the body of a tag, ignore this...
if (this.skipLevel > 0) {
return;
}
this.currentConsumer.processingInstruction(target, data);
}
public void startDTD(String name, String publicId, String systemId) throws SAXException {
// If we are skipping the body of a tag, ignore this...
if (this.skipLevel > 0) {
return;
}
this.currentConsumer.startDTD(name, publicId, systemId);
}
public void endDTD() throws SAXException {
// If we are skipping the body of a tag, ignore this...
if (this.skipLevel > 0) {
return;
}
this.currentConsumer.endDTD();
}
public void startPrefixMapping(String prefix, String uri) throws SAXException {
// If we are skipping the body of a tag, ignore this...
if (this.skipLevel > 0) {
return;
}
this.currentConsumer.startPrefixMapping(prefix, uri);
}
public void endPrefixMapping(String prefix) throws SAXException {
// If we are skipping the body of a tag, ignore this...
if (this.skipLevel > 0) {
return;
}
this.currentConsumer.endPrefixMapping(prefix);
}
public void startCDATA() throws SAXException {
// If we are skipping the body of a tag, ignore this...
if (this.skipLevel > 0) {
return;
}
this.currentConsumer.startCDATA();
}
public void endCDATA() throws SAXException {
// If we are skipping the body of a tag, ignore this...
if (this.skipLevel > 0) {
return;
}
this.currentConsumer.endCDATA();
}
public void startElement(String namespaceURI, String localName, String qName, Attributes atts)
throws SAXException {
// Are we recording for iteration ?
if (this.recordingLevel > 0) {
this.recordingLevel ++;
this.currentConsumer.startElement(namespaceURI, localName, qName, atts);
return;
}
// If we are skipping the body of a Tag
if (this.skipLevel > 0) {
// Remember to skip one more end element
this.skipLevel ++;
// and ignore this start element
return;
}
Tag tag = null;
if (namespaceURI != null && namespaceURI.length() > 0) {
// Try to find Tag corresponding to this element
ComponentSelector tagSelector = null;
try {
tagSelector = (ComponentSelector) tagNamespaceSelector.select(namespaceURI);
tagSelectorStack.push(tagSelector);
// namespace matches tag library, lookup tag now.
tag = (Tag) tagSelector.select(localName);
if (getLogger().isDebugEnabled()) {
getLogger().debug("startElement: Got tag " + qName);
}
setupTag(tag, qName, atts);
} catch (SAXException e) {
throw e;
} catch (Exception ignore) {
// No namespace or tag found, process it as normal element (tag == null)
}
}
tagStack.push(tag);
if (tag == null) {
currentConsumer.startElement(namespaceURI, localName, qName, atts);
return;
}
// Execute Tag
int eval = tag.doStartTag(namespaceURI, localName, qName, atts);
switch (eval) {
case Tag.EVAL_BODY :
skipLevel = 0;
if (tag instanceof IterationTag) {
// start recording for IterationTag
startRecording();
}
break;
case Tag.SKIP_BODY :
skipLevel = 1;
break;
default :
String tagName = tag.getClass().getName();
getLogger().warn("Bad return value from doStartTag(" + tagName + "): " + eval);
break;
}
}
public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
Object saxFragment = null;
// Are we recording?
if (recordingLevel > 0) {
if (--recordingLevel > 0) {
currentConsumer.endElement(namespaceURI, localName, qName);
return;
}
// Recording finished
saxFragment = endRecording();
}
if (skipLevel > 0) {
if (--skipLevel > 0) {
return;
}
}
Tag tag = (Tag) tagStack.pop();
if (tag != null) {
ComponentSelector tagSelector = (ComponentSelector)tagSelectorStack.pop();
try {
if (saxFragment != null) {
// Start Iteration
IterationTag iterTag = (IterationTag) tag;
XMLDeserializer xmlDeserializer = null;
try {
xmlDeserializer = (XMLDeserializer) manager.lookup(XMLDeserializer.ROLE);
xmlDeserializer.setConsumer(this);
// BodyTag Support
XMLConsumer backup = this.currentConsumer;
if (tag instanceof BodyTag) {
SaxBuffer content = new SaxBuffer();
this.currentConsumer = content;
((BodyTag)tag).setBodyContent(new BodyContent(content, backup));
((BodyTag)tag).doInitBody();
}
do {
xmlDeserializer.deserialize(saxFragment);
} while (iterTag.doAfterBody() != Tag.SKIP_BODY);
// BodyTag Support
if (tag instanceof BodyTag) {
this.currentConsumer = backup;
}
} catch (ServiceException e) {
throw new SAXException("Can't obtain XMLDeserializer", e);
} finally {
if (xmlDeserializer != null) {
manager.release(xmlDeserializer);
}
}
}
tag.doEndTag(namespaceURI, localName, qName);
currentTag = tag.getParent();
if (tag == this.currentConsumer) {
popConsumer();
}
} finally {
if (getLogger().isDebugEnabled()) {
getLogger().debug("endElement: Release tag " + qName);
}
tagSelector.release(tag);
tagNamespaceSelector.release(tagSelector);
if (transformerSelector != null && tag instanceof XMLProducer) {
getLogger().debug("endElement: Release transformer");
Transformer transformer = (Transformer) tagTransformerStack.pop();
transformerSelector.release(transformer);
}
}
} else {
this.currentConsumer.endElement(namespaceURI, localName, qName);
}
}
public void startEntity(String name) throws SAXException {
// If we are skipping the body of a tag, ignore this...
if (this.skipLevel > 0) {
return;
}
this.currentConsumer.startEntity(name);
}
public void endEntity(String name) throws SAXException {
// If we are skipping the body of a tag, ignore this...
if (this.skipLevel > 0) {
return;
}
this.currentConsumer.endEntity(name);
}
public void skippedEntity(String name) throws SAXException {
// If we are skipping the body of a tag, ignore this...
if (this.skipLevel > 0) {
return;
}
this.currentConsumer.skippedEntity(name);
}
public void characters(char[] ch, int start, int length) throws SAXException {
// If we are skipping the body of a tag, ignore this...
if (this.skipLevel > 0) {
return;
}
this.currentConsumer.characters(ch, start, length);
}
public void comment(char[] ch, int start, int length) throws SAXException {
// If we are skipping the body of a tag, ignore this...
if (this.skipLevel > 0) {
return;
}
this.currentConsumer.comment(ch, start, length);
}
public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
// If we are skipping the body of a tag, ignore this...
if (this.skipLevel > 0) {
return;
}
this.currentConsumer.ignorableWhitespace(ch, start, length);
}
//
// Internal Implementation Methods
//
private void setupTag(Tag tag, String name, Attributes atts) throws SAXException {
// Set Tag Parent
tag.setParent(this.currentTag);
// Set Tag XML Consumer
if (tag instanceof XMLProducer) {
XMLConsumer tagConsumer;
if (transformerSelector != null) {
Transformer tagTransformer = null;
try {
// Add additional (Tag)Transformer to the output of the Tag
tagTransformer = (Transformer) transformerSelector.select(transformerHint);
tagTransformerStack.push(tagTransformer);
tagTransformer.setConsumer(currentConsumer);
tagTransformer.setup(this.resolver, this.objectModel, null, this.parameters);
} catch (SAXException e) {
throw e;
} catch (Exception e) {
throw new SAXException("Failed to setup tag transformer " + transformerHint, e);
}
tagConsumer = tagTransformer;
} else {
tagConsumer = this.currentConsumer;
}
((XMLProducer) tag).setConsumer(tagConsumer);
}
// Setup Tag
try {
tag.setup(this.resolver, this.objectModel, this.parameters);
} catch (IOException e) {
throw new SAXException("Could not set up tag " + name, e);
}
if (tag instanceof XMLConsumer) {
this.currentConsumer = (XMLConsumer) tag;
}
this.currentTag = tag;
// Set Tag-Attributes, Attributes are mapped to the coresponding Tag method
for (int i = 0; i < atts.getLength(); i++) {
String attributeName = atts.getLocalName(i);
String attributeValue = atts.getValue(i);
this.paramArray[0] = attributeValue;
try {
Method method = getWriteMethod(tag.getClass(), attributeName);
method.invoke(tag, this.paramArray);
} catch (Throwable e) {
if (getLogger().isInfoEnabled()) {
getLogger().info("Tag " + name + " attribute " + attributeName + " not set", e);
}
}
}
}
/**
* Start recording for the iterator tag.
*/
private void startRecording() throws SAXException {
try {
this.xmlSerializer = (XMLSerializer) manager.lookup(XMLSerializer.ROLE);
} catch (ServiceException e) {
throw new SAXException("Can't lookup XMLSerializer", e);
}
this.currentConsumerBackup = this.currentConsumer;
this.currentConsumer = this.xmlSerializer;
this.recordingLevel = 1;
}
/**
* End recording for the iterator tag and returns recorded XML fragment.
*/
private Object endRecording() {
// Restore XML Consumer
this.currentConsumer = this.currentConsumerBackup;
this.currentConsumerBackup = null;
// Get XML Fragment
Object saxFragment = this.xmlSerializer.getSAXFragment();
// Release Serializer
this.manager.release(this.xmlSerializer);
this.xmlSerializer = null;
return saxFragment;
}
/**
* Find previous XML consumer when processing of current consumer
* is complete.
*/
private void popConsumer() {
Tag loop = this.currentTag;
for (; loop != null; loop = loop.getParent()) {
if (loop instanceof XMLConsumer) {
this.currentConsumer = (XMLConsumer) loop;
return;
}
}
this.currentConsumer = this.xmlConsumer;
}
private static Method getWriteMethod(Class type, String propertyName) throws IntrospectionException {
Map map = getWriteMethodMap(type);
Method method = (Method) map.get(propertyName);
if (method == null) {
throw new IntrospectionException("No such property: " + propertyName);
}
return method;
}
private static Map getWriteMethodMap(Class beanClass) throws IntrospectionException {
Map map = (Map) TAG_PROPERTIES_MAP.get(beanClass);
if (map != null) {
return map;
}
BeanInfo info = Introspector.getBeanInfo(beanClass);
if (info != null) {
PropertyDescriptor pds[] = info.getPropertyDescriptors();
map = new HashMap(pds.length * 4 / 3, 1);
for (int i = 0; i < pds.length; i++) {
PropertyDescriptor pd = pds[i];
String name = pd.getName();
Method method = pd.getWriteMethod();
Class type = pd.getPropertyType();
if (type != String.class) // only String properties
continue;
map.put(name, method);
}
}
TAG_PROPERTIES_MAP.put(beanClass, map);
return map;
}
}