/* 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.data;
import com.google.gdata.util.common.base.Pair;
import com.google.gdata.util.common.xml.XmlNamespace;
import com.google.gdata.util.common.xml.XmlWriter;
import com.google.gdata.client.CoreErrorDomain;
import com.google.gdata.util.ParseException;
import com.google.gdata.util.XmlBlob;
import com.google.gdata.util.XmlParser;
import com.google.gdata.util.XmlParser.ElementHandler;
import org.xml.sax.Attributes;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Base class for GData data types that support hosting extensions.
* <p>
* For example, a calendar {@code <atom:entry>} supports hosting
* {@code <gd:when>}.
* <p>
* The set of accepted extensions is defined within {@link ExtensionManifest}.
*/
public class ExtensionPoint extends AbstractExtension {
/**
* Collection of non-repeating extensions. Uses {@link LinkedHashMap} in order
* to provide a predictable generation output order based upon insertion
* order.
*/
private Map<Class<? extends Extension>, Extension> nonRepeatingExtensionMap =
new LinkedHashMap<Class<? extends Extension>, Extension>();
/**
* Collection of repeating extensions. Uses {@link LinkedHashMap} in order to
* provide a predictable generation output order based upon insertion order.
*/
private Map<Class<? extends Extension>, List<Extension>>
repeatingExtensionMap =
new LinkedHashMap<Class<? extends Extension>, List<Extension>>();
/** Arbitrary XML (unrecognized extensions). */
protected XmlBlob xmlBlob = new XmlBlob();
/** Manifest for this instance. Filled on-demand. */
private ExtensionManifest manifest;
/**
* Simple constructor to create a new (empty) ExtensionPoint.
*/
public ExtensionPoint() {}
/**
* Simple copy constructor that does a shallow copy of extension and manifest
* data from an existing ExtensionPoint to the constructed instance.
*/
protected ExtensionPoint(ExtensionPoint sourcePoint) {
// WARNING: ANY NON-STATIC FIELDS ADDED ABOVE NEED TO BE COPIED HERE.
nonRepeatingExtensionMap = sourcePoint.nonRepeatingExtensionMap;
repeatingExtensionMap = sourcePoint.repeatingExtensionMap;
xmlBlob = sourcePoint.xmlBlob;
manifest = sourcePoint.manifest;
}
/**
* Declares the set of expected Extension types for an ExtensionPoint within
* the target extension profile. The base implementation does not declare any
* extensions, but can be overridden by specific types of ExtensionPoints that
* always contain a well-defined set of extensions.
*
* @param extProfile the ExtensionProfile to initialize.
*/
public void declareExtensions(ExtensionProfile extProfile) {
// The default implementation does not register any extensions.
}
/** Returns whether the non-repeating extension is present. */
public final <T extends Extension> boolean hasExtension(
Class<T> extensionClass) {
return nonRepeatingExtensionMap.containsKey(extensionClass);
}
/** Returns whether the repeating extension is present. */
@SuppressWarnings("unchecked")
public final <T extends Extension> boolean hasRepeatingExtension(
Class<T> extensionClass) {
List<T> ret = (List<T>) repeatingExtensionMap.get(extensionClass);
return ret != null && !ret.isEmpty();
}
/** Retrieves a non-repeating extension or {@code null} if not present. */
@SuppressWarnings("unchecked")
public <T extends Extension> T getExtension(Class<T> extensionClass) {
return (T) nonRepeatingExtensionMap.get(extensionClass);
}
/**
* Returns an unmodifiable collection of non-repeating extensions in this
* ExtensionPoint.
*
* @return Collection of non-repeating extensions.
*/
public Collection<Extension> getExtensions() {
return Collections
.unmodifiableCollection(nonRepeatingExtensionMap.values());
}
/** Retrieves a repeating extension list (an empty list if not present). */
@SuppressWarnings("unchecked")
public <T extends Extension> List<T> getRepeatingExtension(
Class<T> extensionClass) {
List<T> ret = (List<T>) repeatingExtensionMap.get(extensionClass);
if (ret == null) {
ret = new ArrayList<T>();
repeatingExtensionMap.put(extensionClass, (List<Extension>) ret);
}
return ret;
}
/**
* Returns an unmodifiable collection of lists of repeating extensions in this
* ExtensionPoint. The Extensions that are of the same type are grouped
* together in lists within the collection.
*
* @return Collection of lists of repeating extensions.
*/
public Collection<List<Extension>> getRepeatingExtensions() {
return Collections.unmodifiableCollection(repeatingExtensionMap.values());
}
/** Internal helper method. */
protected boolean addExtension(Extension ext,
Class<? extends Extension> extClass) {
if (nonRepeatingExtensionMap.containsKey(extClass)) {
return false;
}
nonRepeatingExtensionMap.put(extClass, ext);
return true;
}
/** Adds an extension object. */
public void addExtension(Extension ext) {
addExtension(ext, ext.getClass());
}
/** Sets an extension object. If one exists of this type, it's replaced. */
public void setExtension(Extension ext) {
nonRepeatingExtensionMap.remove(ext.getClass());
addExtension(ext, ext.getClass());
}
/** Internal helper method. */
protected void addRepeatingExtension(Extension ext,
Class<? extends Extension> extClass) {
List<Extension> extList = repeatingExtensionMap.get(extClass);
if (extList == null) {
extList = new ArrayList<Extension>();
}
extList.add(ext);
repeatingExtensionMap.put(extClass, extList);
}
/** Adds a repeating extension object. */
public void addRepeatingExtension(Extension ext) {
addRepeatingExtension(ext, ext.getClass());
}
/** Removes an extension object. */
public void removeExtension(Extension ext) {
nonRepeatingExtensionMap.remove(ext.getClass());
}
/** Removes an extension object based on its class. */
public void removeExtension(Class<? extends Extension> extensionClass) {
nonRepeatingExtensionMap.remove(extensionClass);
}
/** Removes a repeating extension object. */
public void removeRepeatingExtension(Extension ext) {
List<Extension> extList = repeatingExtensionMap.get(ext.getClass());
if (extList == null) {
return;
}
extList.remove(ext);
}
/**
* Called to visit a child of this extension point.
* @param ev the extension visitor
* @param child the child extension
*/
protected void visitChild(ExtensionVisitor ev, Extension child)
throws ExtensionVisitor.StoppedException {
// Recurse for nested extension points or do a visit for simple extensions
if (child instanceof ExtensionPoint) {
((ExtensionPoint) child).visit(ev, this);
} else {
ev.visit(this, child);
}
}
/**
* Called to visit all children of this extension point.
*
* @param ev the extension visitor.
*/
protected void visitChildren(ExtensionVisitor ev)
throws ExtensionVisitor.StoppedException {
// Visit children
for (Extension ext : nonRepeatingExtensionMap.values()) {
visitChild(ev, ext);
}
for (List<Extension> extList : repeatingExtensionMap.values()) {
for (Extension ext : extList) {
visitChild(ev, ext);
}
}
}
/**
* Visits the tree of extension data associated with this extension point
* instance using the specified {@link ExtensionVisitor}, starting at this
* extension point.
*
* @param ev the extension visitor instance to use.
* @param parent the parent of this extension point (or {@code null} if no
* parent or unspecified.
* @returns the action to take for sibling nodes.
*/
public void visit(ExtensionVisitor ev, ExtensionPoint parent)
throws ExtensionVisitor.StoppedException {
// Visit the current extension point
boolean visitChildren = ev.visit(parent, this);
if (visitChildren) {
visitChildren(ev);
}
ev.visitComplete(this);
}
/**
* Retrieves the XML blob containing arbitrary (unrecognized) extensions.
*/
public XmlBlob getXmlBlob() {
return xmlBlob;
}
/** Sets the XML blob containing arbitrary (unrecognized) extensions. */
public void setXmlBlob(XmlBlob xmlBlob) {
this.xmlBlob = xmlBlob;
}
/**
* Generates an XML blob containing all recognized and unrecognized
* extensions. This can be used by applications that persist data in a store
* that might be accessed by other applications--ones that don't necessarily
* recognize the same set of extensions.
*/
public XmlBlob generateCumulativeXmlBlob(ExtensionProfile extProfile)
throws IOException {
XmlBlob cumulative = new XmlBlob();
Collection<XmlNamespace> namespaces = cumulative.getNamespaces();
StringWriter w = new StringWriter();
XmlWriter xw = new XmlWriter(w);
if (xmlBlob != null) {
cumulative.setLang(xmlBlob.getLang());
cumulative.setBase(xmlBlob.getBase());
namespaces.addAll(xmlBlob.getNamespaces());
w.write(xmlBlob.getBlob());
}
if (manifest != null) {
for (XmlNamespace ns : manifest.getNamespaceDecls()) {
XmlNamespace newNs = new XmlNamespace(ns.getAlias(), ns.getUri());
if (!namespaces.contains(newNs)) {
namespaces.add(newNs);
}
}
}
for (Extension ext : nonRepeatingExtensionMap.values()) {
ext.generate(xw, extProfile);
}
for (List<Extension> extList : repeatingExtensionMap.values()) {
xw.startRepeatingElement();
for (Extension ext : extList) {
ext.generate(xw, extProfile);
}
xw.endRepeatingElement();
}
cumulative.setBlob(w.toString());
return cumulative;
}
/**
* Reverses {@link #generateCumulativeXmlBlob(ExtensionProfile)}. This
* operation overwrites the current contents of this extension point.
*/
public void parseCumulativeXmlBlob(XmlBlob blob,
ExtensionProfile extProfile,
Class<? extends ExtensionPoint> extendedClass)
throws IOException, ParseException {
this.xmlBlob = new XmlBlob();
nonRepeatingExtensionMap.clear();
repeatingExtensionMap.clear();
// Prepare a fake XML document from the blob.
StringWriter sw = new StringWriter();
XmlWriter w = new XmlWriter(sw);
XmlBlob.startElement(w, null, "CUMULATIVE_BLOB", blob, null, null);
XmlBlob.endElement(w, null, "CUMULATIVE_BLOB", blob);
// Now parse it.
StringReader sr = new StringReader(sw.toString());
XmlParser parser = new XmlParser();
parser.parse(sr, new CumulativeBlobHandler(extProfile, extendedClass), "",
"CUMULATIVE_BLOB");
}
/** Parser class for cumulative XML blobs. */
public class CumulativeBlobHandler extends ElementHandler {
public CumulativeBlobHandler(ExtensionProfile extProfile,
Class<? extends ExtensionPoint> extendedClass) {
this.extProfile = extProfile;
this.extendedClass = extendedClass;
initializeArbitraryXml(extProfile, extendedClass, this);
}
private final ExtensionProfile extProfile;
private final Class<? extends ExtensionPoint> extendedClass;
@Override
public ElementHandler getChildHandler(String namespace, String localName,
Attributes attrs) throws ParseException, IOException {
// Try ExtensionPoint. It returns {@code null} if there's no handler.
ElementHandler extensionHandler =
getExtensionHandler(extProfile, extendedClass, namespace, localName,
attrs);
if (extensionHandler != null) {
return extensionHandler;
}
return super.getChildHandler(namespace, localName, attrs);
}
}
/** Retrieves the manifest for the specified class. */
protected ExtensionManifest getManifest(ExtensionProfile extProfile,
Class<? extends ExtensionPoint> extendedClass) {
if (manifest == null) {
manifest = extProfile.getManifest(extendedClass);
}
return manifest;
}
@Override
protected void generate(XmlWriter w, ExtensionProfile p,
XmlNamespace namespace, String localName,
List<XmlWriter.Attribute> attrs, AttributeGenerator generator)
throws IOException {
// validate
if (generator.getContent() != null) {
throw new IllegalStateException(
"No content allowed on an extension point");
}
try {
ExtensionManifest profManifest = p.getManifest(this.getClass());
if (profManifest != null) {
checkRequiredExtensions(profManifest);
}
} catch (ParseException e) {
throw new IllegalStateException(e.getMessage());
}
// generate XML
generateStartElement(w, namespace, localName, attrs, null);
generateExtensions(w, p);
w.endElement(namespace, localName);
}
@SuppressWarnings("unused")
@Override
public XmlParser.ElementHandler getHandler(ExtensionProfile p,
String namespace, String localName, Attributes attrs)
throws ParseException {
return new ExtensionHandler(p, this.getClass(), attrs);
}
/**
* Generates XML corresponding to the type implementing {@link
* ExtensionPoint}. The reason this routine is necessary is that the embedded
* XML blob may contain namespace declarations.
*/
protected void generateStartElement(XmlWriter w,
XmlNamespace namespace, String elementName,
Collection<XmlWriter.Attribute> additionalAttrs,
Collection<XmlNamespace> additionalNs) throws IOException {
XmlBlob.startElement(w, namespace, elementName, xmlBlob, additionalAttrs,
additionalNs);
}
/**
* Generates XML corresponding to extended properties. Implementations in
* extended classes should always call the base class to allow for nested
* extensions.
*
* @param w Output writer.
*
* @param extProfile Extension profile for use by nested extensions.
*
* @throws IOException
*/
protected void generateExtensions(XmlWriter w, ExtensionProfile extProfile)
throws IOException {
for (Extension ext : nonRepeatingExtensionMap.values()) {
ext.generate(w, extProfile);
}
for (List<Extension> extList : repeatingExtensionMap.values()) {
w.startRepeatingElement();
for (Extension ext : extList) {
ext.generate(w, extProfile);
}
w.endRepeatingElement();
}
if (xmlBlob != null) {
w.innerXml(xmlBlob.getBlob());
}
}
/**
* Initializes parser handler's XML blob state. Should be called by the
* handler's constructor in order to honor
* {@link ExtensionProfile#declareArbitraryXmlExtension(Class)}.
*/
protected void initializeArbitraryXml(ExtensionProfile profile,
Class<? extends ExtensionPoint> extPoint, ElementHandler handler) {
boolean arbitraryXml = profile.allowsArbitraryXml();
if (!arbitraryXml) {
ExtensionManifest profManifest = getManifest(profile, extPoint);
arbitraryXml = profManifest != null && profManifest.arbitraryXml;
}
if (arbitraryXml) {
handler.initializeXmlBlob(xmlBlob,
/* mixedContent */false,
/* fullTextIndex */false);
}
}
/**
* XML parser callback for extended properties. Implementations in extended
* classes should always call the base class to allow for nested extensions.
*
* @param extProfile Extension profile for use by nested element handlers.
*
* @param extPoint Current active ExtensionPoint class within which you're
* looking for a handler for a nested extension element.
*
* @param namespaceUri Namespace URI of the XML element.
*
* @param localName Name of the XML element.
*
* @param attrs Child element attributes. These attributes will be
* communicated to the returned {@link ElementHandler} through its
* {@link ElementHandler#processAttribute(String, String, String)}
* method. They are passed here because sometimes the value of some
* attribute determines the element's content type, so different
* element handlers may be needed.
*
* @return Element handler for the custom tag or {@code null} if the tag is
* not recognized. Unrecognized tags are stored in the XML blob.
*
* @throws ParseException XML schema error. Could be a result of having a
* duplicate entry, illegal contents (such as unrecognized attributes
* or nested elements), etc.
*/
protected ElementHandler getExtensionHandler(ExtensionProfile extProfile,
Class<? extends ExtensionPoint> extPoint, String namespaceUri,
String localName, Attributes attrs) throws ParseException, IOException {
ExtensionManifest profManifest = getManifest(extProfile, extPoint);
if (profManifest == null) {
return null;
}
// Look for an explicit match, followed by a wildcarded namespace match.
ExtensionDescription extDescription =
profManifest.supportedExtensions.get(
new Pair<String, String>(namespaceUri, localName));
if (extDescription == null) {
extDescription =
profManifest.supportedExtensions.get(new Pair<String, String>(
namespaceUri, "*"));
if (extDescription == null) {
return null;
}
}
Class<? extends Extension> extClass = extDescription.getExtensionClass();
if (extClass == null) {
return null;
}
Extension extension = null;
// If an aggregate extension type, retrieve existing instance (if any)
if (extDescription.isAggregate()) {
extension = getExtension(extClass);
}
boolean needsAdd = true;
if (extension == null) {
// Create an extension instance.
try {
extension = extClass.newInstance();
} catch (InstantiationException e) {
throw new ParseException(
CoreErrorDomain.ERR.cantCreateExtension, e);
} catch (IllegalAccessException e) {
throw new ParseException(
CoreErrorDomain.ERR.cantCreateExtension, e);
}
} else {
needsAdd = false;
}
// Retrieve the handler.
ElementHandler handler =
extension.getHandler(extProfile, namespaceUri, localName, attrs);
// Store the new extension instance.
if (needsAdd) {
if (extDescription.isRepeatable()) {
addRepeatingExtension(extension, extClass);
} else {
boolean added = addExtension(extension, extClass);
if (!added) {
ParseException pe = new ParseException(
CoreErrorDomain.ERR.duplicateExtension);
pe.setInternalReason("Duplicate extension element " +
namespaceUri + ":" + localName);
throw pe;
}
}
}
return handler;
}
/** Checks whether all required extensions are present. */
protected void checkRequiredExtensions(ExtensionManifest profManifest)
throws ParseException {
for (ExtensionDescription extDescription : profManifest.supportedExtensions
.values()) {
if (extDescription.isRequired()) {
Class<? extends Extension> extClass =
extDescription.getExtensionClass();
boolean found =
(extDescription.isRepeatable() ? repeatingExtensionMap
.containsKey(extClass) : nonRepeatingExtensionMap
.containsKey(extClass));
if (!found) {
ParseException pe = new ParseException(
CoreErrorDomain.ERR.missingExtensionElement);
pe.setInternalReason("Required extension element " +
extDescription.getNamespace().getUri() + ":" +
extDescription.getLocalName() + " not found.");
throw pe;
}
}
}
}
/**
* ElementHandler implementation for handlers associated with an
* ExtensionPoint class. Provides common initialization and code for looking
* up handlers defined within the ExtensionProfile associated with the
* ExtensionPoint.
*/
public class ExtensionHandler extends AbstractExtension.AttributesHandler {
protected ExtensionProfile extProfile;
protected Class<? extends ExtensionPoint> extendedClass;
protected boolean hasExtensions;
protected ExtensionManifest extManifest;
/**
* Constructs a new Handler instance that process extensions on a class
* associated with the ExtensionPoint. e
*
* @param profile The extension profile associatd with the Handler.
* @param extendedClass The extended class within the profile for this
* handler
*/
public ExtensionHandler(ExtensionProfile profile,
Class<? extends ExtensionPoint> extendedClass) {
this(profile, extendedClass, null);
}
/**
* Constructs a new Handler instance that process extensions on a class
* associated with the ExtensionPoint, and consumes the attributes.
*
* @param profile The extension profile associatd with the Handler.
* @param extendedClass The extended class within the profile for this
* handler
* @param attrs XML attributes or <code>null</code> to suppress the use of
* {@link AttributeHelper}
*/
public ExtensionHandler(ExtensionProfile profile,
Class<? extends ExtensionPoint> extendedClass, Attributes attrs) {
super(attrs);
this.extProfile = profile;
this.extendedClass = extendedClass;
this.extManifest = profile.getManifest(extendedClass);
if (this.extManifest != null) {
hasExtensions = true;
}
initializeArbitraryXml(extProfile, extendedClass, this);
}
@Override
public XmlParser.ElementHandler getChildHandler(String namespace,
String localName, Attributes attrs) throws ParseException, IOException {
// If extensions have been defined for the extended class, then
// look for a handler.
if (hasExtensions) {
XmlParser.ElementHandler extensionHandler =
getExtensionHandler(extProfile, extendedClass, namespace,
localName, attrs);
if (extensionHandler != null) {
return extensionHandler;
}
}
return super.getChildHandler(namespace, localName, attrs);
}
@Override
public void processEndElement() throws ParseException {
super.processEndElement();
if (this.extManifest != null) {
checkRequiredExtensions(this.extManifest);
}
//
// Iterate through all contained Extension instances and enable them
// to validate against the full ExtensionPoint state (including
// sibling Extension instances).
//
for (Extension extension : nonRepeatingExtensionMap.values()) {
if (extension instanceof ValidatingExtension) {
((ValidatingExtension) extension).validate(ExtensionPoint.this);
}
}
for (List<Extension> extList : repeatingExtensionMap.values()) {
for (Extension extension : extList) {
if (extension instanceof ValidatingExtension) {
((ValidatingExtension) extension).validate(ExtensionPoint.this);
}
}
}
}
}
}