/* Copyright (c) 2008 Google Inc.
*
* 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 com.google.gdata.wireformats;
import com.google.gdata.util.common.xml.XmlNamespace;
import com.google.gdata.util.common.xml.XmlWriter;
import com.google.gdata.util.common.xml.XmlWriter.WriterFlags;
import com.google.gdata.model.Attribute;
import com.google.gdata.model.AttributeKey;
import com.google.gdata.model.AttributeMetadata;
import com.google.gdata.model.Element;
import com.google.gdata.model.ElementKey;
import com.google.gdata.model.ElementMetadata;
import com.google.gdata.model.ElementVisitor;
import com.google.gdata.model.QName;
import com.google.gdata.wireformats.output.OutputProperties;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
/**
* XML generator that outputs a tree of {@link Element} objects
* using the XML wire format.
*/
public class XmlGenerator implements WireFormatGenerator, ElementVisitor {
/**
* A namespace marker that means we should use the root element namespace
* as the default namespace. This can be overridden by calling
* {@link #XmlGenerator(OutputProperties, Writer, Charset, boolean, XmlNamespace)}
* and giving it a different namespace.
*/
private static final XmlNamespace USE_ROOT_ELEMENT_NAMESPACE =
new XmlNamespace("__USE_ROOT_ELEMENT_NAMESPACE__");
/**
* Metadata for the root element
*/
protected final ElementMetadata<?, ?> rootMetadata;
/**
* XML writer used by this generator.
*/
protected final XmlWriter xw;
/**
* The default namespace to use for this generator.
*/
private final XmlNamespace defaultNamespace;
/**
* Creates a new xml generator for generating xml output. This constructor
* will use the namespace of the root element as the default namespace of the
* output. Callers that would like to change the default namespace should
* call {@link #XmlGenerator(OutputProperties, Writer, Charset, boolean, XmlNamespace)}
* with the namespace that should be used as the default.
*/
public XmlGenerator(StreamProperties props, Writer w, Charset cs,
boolean prettyPrint) {
this(props, w, cs, prettyPrint, USE_ROOT_ELEMENT_NAMESPACE);
}
/**
* Creates a new xml generator for generating xml output, using the
* given namespace as the default namespace.
*/
public XmlGenerator(StreamProperties props, Writer w, Charset cs,
boolean prettyPrint, XmlNamespace defaultNamespace) {
EnumSet<WriterFlags> flags = EnumSet.of(WriterFlags.WRITE_HEADER);
if (prettyPrint) {
flags.add(WriterFlags.PRETTY_PRINT);
}
try {
this.xw = new XmlWriter(w, flags, cs.name());
} catch (IOException ioe) {
throw new RuntimeException("Unable to create XML generator", ioe);
}
this.rootMetadata = props.getRootMetadata();
this.defaultNamespace = defaultNamespace;
}
/**
* The ElementGenerator interface is implemented by helper classes that
* will generate the start element, text content, and end element syntax
* for an {@link Element} to an {@link XmlWriter}.
* <p>
* The generic implementation of this interface is provided by the
* {@link XmlElementGenerator} class, but it may be overridden by element
* types that want to take more direct control over XML output for a given
* element.
*/
public interface ElementGenerator {
/**
* Start an element. If an ElementGenerator instances writes a full
* element tag, it should return {@code false} to indicate that textContent
* and child elements should not be added.
*
* @param xw the xml writer to write to.
* @param parent the parent element.
* @param e the element to start.
* @param metadata the metadata for the element
* @return true if child elements should be written, false if the element
* was fully written.
* @throws IOException if an error occurs while writing to the writer.
*/
public boolean startElement(XmlWriter xw, Element parent, Element e,
ElementMetadata<?, ?> metadata) throws IOException;
/**
* Write the text content for an element.
*/
public void textContent(XmlWriter xw, Element e,
ElementMetadata<?, ?> metadata) throws IOException;
/**
* End an element, writing a close tag if needed.
*/
public void endElement(XmlWriter xw, Element e,
ElementMetadata<?, ?> metadata) throws IOException;
}
/**
* The XmlElementGenerator class provides the default implementation of the
* {@link ElementGenerator interface}. It will generate start and end
* elements based directly on the element metadata, attributes, and value.
*/
public static class XmlElementGenerator implements ElementGenerator {
public boolean startElement(XmlWriter xw, Element parent, Element e,
ElementMetadata<?, ?> metadata) throws IOException {
Collection<XmlNamespace> namespaces = getNamespaces(parent, e, metadata);
List<XmlWriter.Attribute> attrs = getAttributes(e, metadata);
QName name = getName(e, metadata);
xw.startElement(name.getNs(), name.getLocalName(), attrs, namespaces);
return true;
}
/**
* Returns the QName of an element, possibly using the given metadata for
* the name if it is not {@code null}.
*/
protected QName getName(Element e, ElementMetadata<?, ?> metadata) {
return (metadata == null) ? e.getElementId() : metadata.getName();
}
/**
* Get a collection of namespaces for the current element and parent. This
* will only return namespaces for the root element, because we bubble all
* namespaces up to the root for wire efficiency.
*/
protected Collection<XmlNamespace> getNamespaces(
Element parent, Element e, ElementMetadata<?, ?> metadata) {
if (parent == null) {
return GeneratorUtils.calculateNamespaces(e, metadata).values();
}
return null;
}
/**
* Get a list of attributes for the given element.
*/
protected List<XmlWriter.Attribute> getAttributes(Element e,
ElementMetadata<?, ?> metadata) {
List<XmlWriter.Attribute> attrs = null;
Iterator<Attribute> attributeIterator = e.getAttributeIterator(metadata);
if (attributeIterator.hasNext()) {
ElementKey<?, ?> key = e.getElementKey();
attrs = new ArrayList<XmlWriter.Attribute>();
while (attributeIterator.hasNext()) {
Attribute attribute = attributeIterator.next();
AttributeKey<?> attKey = attribute.getAttributeKey();
AttributeMetadata<?> attMeta = (metadata == null) ? null
: metadata.bindAttribute(attKey);
QName qName = attMeta != null ? attMeta.getName() : attKey.getId();
String alias = (qName.getNs() != null) ?
qName.getNs().getAlias() : null;
attrs.add(new XmlWriter.Attribute(alias, qName.getLocalName(),
attribute.getValue().toString()));
}
}
return attrs;
}
public void textContent(XmlWriter xw, Element e,
ElementMetadata<?, ?> metadata) throws IOException {
Object value = (metadata == null) ? e.getTextValue()
: metadata.generateValue(e, metadata);
if (value != null) {
String valStr = value.toString();
if (valStr.length() > 0) {
xw.characters(valStr);
}
}
}
public void endElement(XmlWriter xw, Element e,
ElementMetadata<?, ?> metadata) throws IOException {
QName name = getName(e, metadata);
xw.endElement(name.getNs(), name.getLocalName());
}
}
// A singleton default generator that is used in all cases where element
// generation has not been customized via metadata.
private static final ElementGenerator DEFAULT_GENERATOR =
new XmlElementGenerator();
public void generate(Element element) throws IOException {
generate(element, rootMetadata);
}
public void generate(Element element, ElementMetadata<?, ?> metadata)
throws IOException {
if (metadata != null &&
!metadata.getKey().equals(element.getElementKey())) {
throw new IllegalStateException(
"Element key (" + element.getElementKey() +
") does not match metadata key (" + metadata.getKey() + ")");
}
try {
element.visit(this, metadata);
} catch (StoppedException se) {
Throwable cause = se.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
}
throw se; // unexpected
}
}
/**
* Returns the {@link ElementGenerator} that should be used to generator
* the specified element. The method will return the custom generator
* configured in the {@link XmlWireFormatProperties} of element metadata, or
* the default generator if none has been configured.
*
* @param metadata the element metadata
* @return the element generator for elements of this type.
*/
private ElementGenerator getElementGenerator(ElementMetadata<?, ?> metadata) {
if (metadata != null) {
XmlWireFormatProperties xmlProperties =
(XmlWireFormatProperties) metadata.getProperties();
if (xmlProperties != null) {
ElementGenerator elementGenerator = xmlProperties.getElementGenerator();
if (elementGenerator != null) {
return elementGenerator;
}
}
}
return DEFAULT_GENERATOR;
}
public boolean visit(Element parent, Element e,
ElementMetadata<?, ?> metadata) throws StoppedException {
try {
if (parent == null) {
setRootNamespace(metadata, e);
}
if (metadata == null || metadata.isSelected(e)) {
ElementGenerator gen = getElementGenerator(metadata);
return gen.startElement(xw, parent, e, metadata);
}
} catch (IOException ioe) {
throw new StoppedException(ioe);
}
return false;
}
/**
* Sets the root element for generation. This is used to derive the default
* metadata that should be used.
*/
private void setRootNamespace(ElementMetadata<?, ?> meta, Element e) {
XmlNamespace rootNs = defaultNamespace;
// If no default has been set, we use the namespace of the root element as
// the default namespace.
if (rootNs == USE_ROOT_ELEMENT_NAMESPACE) {
if (meta != null) {
rootNs = meta.getDefaultNamespace();
} else {
rootNs = e.getElementId().getNs();
}
}
if (rootNs != null) {
xw.setDefaultNamespace(rootNs);
}
}
public void visitComplete(Element parent, Element e,
ElementMetadata<?, ?> metadata) throws StoppedException {
try {
if (metadata == null || metadata.isSelected(e)) {
ElementGenerator elementGenerator = getElementGenerator(metadata);
elementGenerator.textContent(xw, e, metadata);
elementGenerator.endElement(xw, e, metadata);
}
} catch (IOException ioe) {
throw new StoppedException(ioe);
}
}
}