/* 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.ParseException;
import com.google.gdata.util.XmlParser;
import org.xml.sax.Attributes;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Common extension implementation for sharing code among implementers of
* {@link Extension}. The information contained in this class is:
* <ol>
* <li>XML namespace of the extension</li>
* <li>XML local name of the extension</li>
* <li>if the extension is mutable</li>
* </ol>
*
*
*/
public abstract class AbstractExtension implements Extension {
/** XML namespace for this extension or <code>null</code> if not defined */
protected final XmlNamespace namespace;
/** XML local name for this extension or <code>null</code> if not defined */
protected final String localName;
/**
* Indicates that the extension is constant after construction ({@code false}
* by default).
*/
private boolean immutable;
public final boolean isImmutable() { return immutable; }
public final void setImmutable(boolean isImmutable) {
this.immutable = isImmutable;
}
/**
* Constructs an extension bound to a specific XML representation. The
* concrete subclass must have an {@link ExtensionDescription.Default}
* attribute defined to use this constructor.
*/
protected AbstractExtension() {
Class<? extends AbstractExtension> extensionClass = this.getClass();
ExtensionDescription.Default defAnnot = extensionClass
.getAnnotation(ExtensionDescription.Default.class);
if (defAnnot != null) {
this.namespace = new XmlNamespace(defAnnot.nsAlias(),
defAnnot.nsUri());
this.localName = defAnnot.localName();
} else {
this.namespace = null;
this.localName = null;
}
}
/**
* Constructs an extension bound to a specific XML representation. Note: this
* is here for backwards compatibility and may be removed at some point in the
* future.
*
* @param namespace the namespace of the value element
* @param localName the local name of the value element
*/
protected AbstractExtension(XmlNamespace namespace, String localName) {
this.namespace = namespace;
this.localName = localName;
}
/**
* Gets the extension's namespace.
*/
public final XmlNamespace getExtensionNamespace() {
return namespace;
}
/**
* Gets the extension's localname.
*/
public final String getExtensionLocalName() {
return localName;
}
/**
* Checks the attributes to see if there are any problems. Default
* implementation does nothing, though generally this is discouraged unless
* there really are no restrictions.
*
* @throws IllegalStateException if any problems are found with the
* attributes
*/
protected void validate() throws IllegalStateException {
}
/**
* Puts attributes into the attribute generator. Called from
* {@link #generate(XmlWriter,ExtensionProfile)}. Default implementation
* does nothing, though generally this is discouraged unless there really are
* no attributes.
*
* @param generator attribute generator
*/
protected void putAttributes(AttributeGenerator generator) {
}
/**
* Consumes attributes from the attribute helper. May also use
* {@link AttributeHelper#consumeContent} to consume the element's text
* content. Called from {@link #getHandler}. Default implementation does
* nothing, though generally this is discouraged unless there really are no
* attributes.
*
* @param helper attribute helper
* @throws ParseException any parsing exception
*/
protected void consumeAttributes(AttributeHelper helper)
throws ParseException {
}
/**
* Generates the XML into the XML writer. Default implementation generates a
* "simple" element with the attributes and content found in the attribute
* generator.
*
* @param w XML writer
* @param p extension profile
* @param namespace XML namespace for this extension
* @param localName XML local name for this extension
* @param attrs list of XML attributes
* @param generator attribute generator
* @throws IOException any I/O exception
*/
protected void generate(XmlWriter w, ExtensionProfile p,
XmlNamespace namespace, String localName,
List<XmlWriter.Attribute> attrs, AttributeGenerator generator)
throws IOException {
w.simpleElement(namespace, localName, attrs, generator.getContent());
}
public void generate(XmlWriter w, ExtensionProfile p)
throws IOException {
// validate
if (namespace == null) {
String name = this.getClass().getName();
throw new IllegalStateException(
"No @ExtensionDescription.Default annotation found on subclass "
+ name.substring(name.lastIndexOf('.') + 1));
}
validate();
// generate attributes
AttributeGenerator generator = new AttributeGenerator();
putAttributes(generator);
List<XmlWriter.Attribute> attrs = new ArrayList<XmlWriter.Attribute>();
generateAttributes(attrs, generator);
// generate XML
generate(w, p, namespace, localName, attrs, generator);
}
/**
* Generates the attributes in the generator into the list of attributes.
*/
protected void generateAttributes(List<XmlWriter.Attribute> attrs,
AttributeGenerator generator) {
for (Map.Entry<String, String> entry : generator.entrySet()) {
String value = entry.getValue();
if (value != null) {
attrs.add(new XmlWriter.Attribute(entry.getKey(), value));
}
}
}
/**
* The default implementation uses the {@link AttributesHandler} to handle
* parsing the extension.
*
* @throws ParseException when an unexpected tag or badly-formatted
* XML is detected
*/
public XmlParser.ElementHandler getHandler(ExtensionProfile p,
String namespace, String localName, Attributes attrs)
throws ParseException {
return new AttributesHandler(attrs);
}
/**
* Base class for custom element handlers that uses {@link AttributeHelper}
* to consume the attributes and the element's text content.
*
*
*/
protected class AttributesHandler extends XmlParser.ElementHandler {
/** attribute helper or <code>null</code> to suppress its use */
private final AttributeHelper helper;
/**
* Constructor.
*
* @param attrs XML attributes or <code>null</code> to suppress the use of
* {@link AttributeHelper}
*/
public AttributesHandler(Attributes attrs) {
helper = attrs == null ? null : new AttributeHelper(attrs);
if (immutable) {
throw new IllegalStateException("Cannot parse into immutable instance");
}
}
@Override
public void processEndElement() throws ParseException {
/* don't call super.processEndElement() because it doesn't allow text()
data */
// consume attributes
if (helper != null) {
helper.setContent(value);
consumeAttributes(helper);
helper.assertAllConsumed();
}
// validate
try {
validate();
} catch (IllegalStateException e) {
throw new ParseException(e.getMessage(), e);
}
}
}
/**
* Throws an {@link IllegalStateException} if this instance is immutable.
* Should only be used in a value-setter method.
*/
protected final void throwExceptionIfImmutable() {
if (immutable) {
throw new IllegalStateException(localName + " instance is read only");
}
}
/**
* Throws an {@link IllegalStateException} if the value is required
* and it is missing.
*
* @param attrName attribute name
* @throws IllegalStateException to indicate that there are problems with the
* attributes
*/
protected static final void throwExceptionForMissingAttribute(
String attrName) {
throw new IllegalStateException("Missing attribute: " + attrName);
}
/**
* @param o given object
* @return true if the given object is not null and is the same concrete class
* as this one
*/
protected boolean sameClassAs(Object o) {
return o != null && getClass().equals(o.getClass());
}
/**
* @param o1 object 1 or <code>null</code>
* @param o2 object 2 or <code>null</code>
* @return true if the specified arguments are equal, or both null
*/
protected static boolean eq(Object o1, Object o2) {
return o1 == null ? o2 == null : o1.equals(o2);
}
}