/* 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.xml.XmlNamespace;
import com.google.gdata.util.common.xml.XmlWriter;
import com.google.gdata.util.common.xml.XmlWriter.Attribute;
import com.google.gdata.client.CoreErrorDomain;
import com.google.gdata.util.Namespaces;
import com.google.gdata.util.ParseException;
import org.xml.sax.Attributes;
import java.io.IOException;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
/**
* The ExtensionDescription class describes the attributes of an XML extension
* type. This description can be declared within an {@link ExtensionProfile}
* to indicate that the extension is expected within a particular
* {@link ExtensionPoint}.
*
*
*
*
* @see ExtensionProfile#declare(Class, ExtensionDescription)
*/
public class ExtensionDescription extends ExtensionPoint
implements Comparable<ExtensionDescription> {
/**
* The namespace of the XML extension type.
*/
private XmlNamespace namespace;
/**
* Local name of the XML extension type. A value of '*'
* indicates that all elements in the namespace will be handled
* by the Extension class.
*/
private String localName;
/**
* The Extension class used to parse and generate the extension type
*/
private Class<? extends Extension> extensionClass;
/**
* Specifies whether the extension is required within its parent extension
* point.
*/
private boolean required = false;
/**
* Specifies whether the extension type can be repeated within its parent
* extension point.
*/
private boolean repeatable = false;
/**
* Specifies whather the extension type aggregates the contents of multiple
* elements within its parent.
*/
private boolean aggregate = false;
/**
* The Default interface defines a simple annotation model for describing
* the default {@link ExtensionDescription} of an {@link Extension} class. If
* this annotation is place on an @{link Extension} class, the
* {@link ExtensionDescription#getDefaultDescription(Class)} method can be
* used to retrieve default description for the class.
*
* @see ExtensionDescription#getDefaultDescription(Class)
*/
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Default {
/**
* The default namespace alias associated with this extension.
*/
public String nsAlias();
/**
* The default namespace uri associated with this extension.
*/
public String nsUri();
/**
* The default XML element local name associated with this extension.
*/
public String localName();
/**
* {@code true} if the extension is required by default, {@code false}
* otherwise.
*/
public boolean isRequired() default false;
/**
* {@code true} if the extension is repeatable by default, {@code false}
* otherwise.
*/
public boolean isRepeatable() default false;
/**
* {@code true} if the extension is aggregate by default, {@code false}
* otherwise.
*/
public boolean isAggregate() default false;
}
/**
* Returns the default {@link ExtensionDescription} for the specified
* Extension class.
*
* @param extensionClass the target extension class.
* @return default description for the target extension class.
*
* @throws IllegalArgumentException if a default description could not be
* fourn for the extension class.
*/
public static ExtensionDescription getDefaultDescription(
Class<? extends Extension> extensionClass) {
Default defAnnot = extensionClass.getAnnotation(Default.class);
if (defAnnot == null) {
throw new IllegalArgumentException("No default description found for "
+ extensionClass);
}
return new ExtensionDescription(
extensionClass,
new XmlNamespace(defAnnot.nsAlias(), defAnnot.nsUri()),
defAnnot.localName(),
defAnnot.isRequired(),
defAnnot.isRepeatable(),
defAnnot.isAggregate());
}
/**
* Constructs an uninitialized ExtensionDescription.
*/
public ExtensionDescription() {}
/**
* Constructs a new ExtensionDescription populated with the parameter
* values.
*/
public ExtensionDescription(Class<? extends Extension> extensionClass,
XmlNamespace namespace,
String localName,
boolean required,
boolean repeatable,
boolean aggregate) {
this.namespace = namespace;
this.localName = localName;
this.extensionClass = extensionClass;
this.required = required;
this.repeatable = repeatable;
this.aggregate = aggregate;
}
/**
* Constructs a new ExtensionDescription for an optional, non-repeating
* simple element.
*/
public ExtensionDescription(Class<? extends Extension> extensionClass,
XmlNamespace namespace,
String localName) {
this(extensionClass, namespace, localName, false, false, false);
}
public void setNamespace(XmlNamespace namespace) {
this.namespace = namespace;
}
final public XmlNamespace getNamespace() { return namespace; }
public void setLocalName(String localName) {
this.localName = localName;
}
final public String getLocalName() { return localName; }
public void setExtensionClass(Class<? extends Extension> extensionClass) {
this.extensionClass = extensionClass;
}
final public Class<? extends Extension> getExtensionClass() {
return extensionClass;
}
public void setRequired(boolean required) {
this.required = required;
}
final public boolean isRequired() { return required; }
public void setRepeatable(boolean repeatable) {
this.repeatable = repeatable;
}
final public boolean isRepeatable() { return repeatable; }
public void setAggregate(boolean aggregate) {
this.aggregate = aggregate;
}
final public boolean isAggregate() { return aggregate; }
/**
* Defines a natural ordering for ExtensionDescription based upon
* the qualified name of the mapped XML element. Elements with no
* namespace are considered to precede all others.
*/
public int compareTo(ExtensionDescription desc) {
String ns1 = namespace.getUri();
if (ns1 == null) {
ns1 = "";
}
String ns2 = desc.namespace.getUri();
if (ns2 == null) {
ns2 = "";
}
int nscomp = ns1.compareTo(ns2);
if (nscomp != 0) {
return nscomp;
}
return localName.compareTo(desc.localName);
}
/** Namespace of the corresponding XML element. */
/**
* Reads the ExtensionDescription XML format
*/
public class Handler extends ExtensionPoint.ExtensionHandler {
public Handler(ExtensionProfile configProfile, ClassLoader configLoader,
List<XmlNamespace> namespaces, Attributes attrs)
throws ParseException {
super(configProfile, ExtensionDescription.class);
String nsValue = attrs.getValue("", "namespace");
if (nsValue == null) {
throw new ParseException(
CoreErrorDomain.ERR.missingNamespace);
}
// Find the namespace in the list of declared NamespaceDescriptions.
// The attribute value can match either the alias or the uri
for (XmlNamespace declaredNs : namespaces) {
if (declaredNs.getAlias().equals(nsValue) ||
declaredNs.getUri().equals(nsValue)) {
namespace = declaredNs;
break;
}
}
if (namespace == null) {
ParseException pe = new ParseException(
CoreErrorDomain.ERR.missingNamespaceDescription);
pe.setInternalReason("No matching NamespaceDescription for " +
nsValue);
throw pe;
}
localName = attrs.getValue("", "localName");
if (localName == null) {
throw new ParseException(
CoreErrorDomain.ERR.missingLocalName);
}
String extensionClassName = attrs.getValue("", "extensionClass");
if (extensionClassName == null) {
throw new ParseException(
CoreErrorDomain.ERR.missingExtensionClass);
}
try {
Class<?> extClass = configLoader.loadClass(extensionClassName);
if (!Extension.class.isAssignableFrom(extClass)) {
throw new ParseException(
CoreErrorDomain.ERR.mustImplementExtension);
}
extensionClass = (Class<? extends Extension>) extClass;
} catch (ClassNotFoundException e) {
ParseException pe = new ParseException(
CoreErrorDomain.ERR.cantLoadExtensionClass, e);
pe.setInternalReason("Unable to load extensionClass: " +
extensionClassName);
throw pe;
}
Boolean bool = getBooleanAttribute(attrs, "required");
required = (bool != null) && bool.booleanValue();
bool = getBooleanAttribute(attrs, "repeatable");
repeatable = (bool != null) && bool.booleanValue();
bool = getBooleanAttribute(attrs, "aggregate");
aggregate = (bool != null) && bool.booleanValue();
}
}
/**
* Generates XML in the external config format.
*
* @param w
* Output writer.
*
* @param extProfile
* Extension profile.
*
* @throws IOException
*/
public void generateConfig(XmlWriter w,
ExtensionProfile extProfile) throws IOException {
List<Attribute> attrs = new ArrayList<Attribute>();
attrs.add(new Attribute("namespace", namespace.getUri()));
attrs.add(new Attribute("localName", localName));
attrs.add(new Attribute("extensionClass", extensionClass.getName()));
attrs.add(new Attribute("required", required));
attrs.add(new Attribute("repeatable", repeatable));
attrs.add(new Attribute("aggregate", aggregate));
generateStartElement(w, Namespaces.gdataConfigNs, "extensionDescription",
attrs, null);
generateExtensions(w, extProfile);
w.endElement(Namespaces.gdataConfigNs, "extensionDescription");
}
}