/* 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.client.GDataProtocol;
import com.google.gdata.client.Service;
import com.google.gdata.data.batch.BatchInterrupted;
import com.google.gdata.data.batch.BatchStatus;
import com.google.gdata.util.EventSourceParser;
import com.google.gdata.util.Namespaces;
import com.google.gdata.util.NotModifiedException;
import com.google.gdata.util.ParseException;
import com.google.gdata.util.ParseUtil;
import com.google.gdata.util.ServiceException;
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.InputStream;
import java.io.Reader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.Vector;
/**
* The BaseEntry class is an abstract base class that defines the
* in-memory object model for GData entries.
* <p>
* It is capable of parsing the Atom XML for an {@code <atom:entry>}
* element as well as any contained Extension elements. It can generate
* both Atom and RSS 2.0 representations of the entry from the object
* model.
* <p>
* The BaseEntry class implements the {@link Kind.Adaptable} interface, meaning
* it is possible to create new {@link Kind.Adaptor} subtypes that defines
* a custom extension model (and associated convenience APIs) for a BaseEntry
* subtypes that use Atom/RSS extensions to extend the content model for a
* particular type of data.
* <p>
* An {@link Kind.Adaptor} subclass of BaseEntry should do the following:
* <ul>
* <li>Include a {@link Kind.Term} annotation on the class declaration that
* defines the {@link Category} term value for the GData kind handled by the
* adaptor.</li>
* <li>Provide a constructor that takes a single BaseEntry parameter as an
* argument that is used when adapting a generic entry type to a more specific
* one.</li>
* <li>Implement the {@link Kind.Adaptor#declareExtensions(ExtensionProfile)}
* method and use it to declare the extension model for the adapted instance
* within the profile passed as a parameter. This is used to auto-extend
* an extension profile when kind Category tags are found during parsing of
* content.</li>
* <li>Expose convenience APIs to retrieve and set extension attributes, with
* an implementions that delegates to {@link ExtensionPoint} methods to
* store/retrieve the extension data.
* </ul>
*
* Here is the Relax-NG schema that represents an Atom 1.0 entry:
* <pre>
* atomEntry =
* element atom:entry {
* atomCommonAttributes,
* (atomAuthor*
* & atomCategory*
* & atomContent?
* & atomContributor*
* & atomId
* & atomLink*
* & atomPublished?
* & atomRights?
* & atomSource?
* & atomSummary?
* & atomTitle
* & atomUpdated
* & extensionElement*)
* </pre>
*
* @param <E> the entry type associated with the bound subtype.
* @see Kind.Adaptor
* @see Kind.Adaptable
*
*
*
*/
public abstract class BaseEntry<E extends BaseEntry>
extends ExtensionPoint
implements Kind.Adaptable, Kind.Adaptor, IEntry {
/**
* The EntryState class provides a simple structure that encapsulates
* the attributes of an Atom entry that should be shared with a shallow
* copy if the entry is adapted to a more specific BaseEntry
* {@link Kind.Adaptor} subtypes.
*
* @see BaseEntry#BaseEntry(BaseEntry)
*/
protected static class EntryState {
/** Entry id. */
public String id;
/**
* Version ID. This is a unique number representing this particular
* entry. Every update changes the version ID (unless the update
* doesn't modify anything, in which case it's permissible for
* version ID to stay the same). Services are free to interpret this
* string in the most convenient way. Some services may choose to use
* a monotonically increasing sequence of version IDs. Other services
* may compute a hash of entry properties and use that.
* <p>
* This property is only used for services to communicate the current
* version ID back to the servlet. It is NOT set when entries are
* parsed (either from requests or from arbitrary XML).
*/
public String versionId;
/**
* Etag. See RFC 2616, Section 3.11.
* If there is no entity tag, this variable is null.
* Etags are provided not only on top-level entries,
* but also on entries within feeds (in the form of
* a gd:etag attribute).
*/
public String etag;
/**
* gd:fields. This is the field selection associated with this entry.
* If not {@code null} then this entry represents a partial entry.
*/
public String fields;
/**
* gd:kind. This is the kind attribute for this entry. If there is no kind
* attribute for this entry, this variable is null.
*/
public String kind;
/** Creation timestamp. Ignored on updates. */
public DateTime published;
/** Last updated timestamp. */
public DateTime updated;
/** Last edit timestamp */
public DateTime edited;
/** Categories of entry. */
public HashSet<Category> categories = new HashSet<Category>();
/** Title of entry. */
public TextConstruct title;
/** Summary of entry. */
public TextConstruct summary;
/** Rights of entry. */
public TextConstruct rights;
/** Content of entry. */
public Content content;
/** Links of entry. */
public LinkedList<Link> links = new LinkedList<Link>();
/** Authors of entry. */
public LinkedList<Person> authors = new LinkedList<Person>();
/** Contributors of entry. */
public LinkedList<Person> contributors = new LinkedList<Person>();
/** Source. */
public Source source;
/** Service. */
public Service service;
/** {code true} if the entry can be modified by a client. */
public boolean canEdit = true;
/**
* Atom publication control status, which contains the draft status.
*/
public PubControl pubControl;
/** Adaptable helper. */
public Kind.Adaptable adaptable = new Kind.AdaptableHelper();
}
/**
* Basic state for this entry. May be shared across multiple adapted
* instances associated with the same logical entry.
*/
protected EntryState state;
/**
* Constructs a new BaseEntry instance.
*/
protected BaseEntry() {
state = new EntryState();
}
// Locally cache the atomPub namespace value, which is dynamic based upon
// the in-use version but constant for any instance.
private XmlNamespace atomPubNs;
private XmlNamespace getAtomPubNs() {
if (atomPubNs == null) {
atomPubNs = Namespaces.getAtomPubNs();
}
return atomPubNs;
}
/**
* Copy constructor that initializes a new BaseEntry instance to have
* identical contents to another instance, using a shared reference to
* the same {@link EntryState}. {@link Kind.Adaptor} subclasses
* of {@code BaseEntry} can use this constructor to create adaptor
* instances of an entry that share state with the original.
*/
protected BaseEntry(BaseEntry<?> sourceEntry) {
super(sourceEntry);
state = sourceEntry.state;
}
public String getId() { return state.id; }
public void setId(String v) {
if (v != null && "-".equals(v)) {
// Disallow dash as an entry id. It leads to ambiguity because
// we use a dash to separate category queries in a feed URI.
// Does /feeds/feed-id/-/X mean a feed request with a category
// query "X", or an entry request with "-" as the entry ID
// and X as the version number. In {@link UriTemplate} we've
// made the choice that it means a feed request. Therefore "-"
// cannot be an entry ID.
throw new IllegalArgumentException("Entry.id must not be equal to '-'.");
}
state.id = v;
}
public String getVersionId() { return state.versionId; }
public void setVersionId(String v) { state.versionId = v; }
public String getEtag() { return state.etag; }
public void setEtag(String v) { state.etag = v; }
/**
* Returns the current fields selection for this partial entry. A
* value of {@code null} indicates the entry is not a partial entry.
*/
public String getSelectedFields() { return state.fields; }
/**
* Sets the current fields selection for this partial entry. A
* value of {@code null} indicates the entry is not a partial entry.
*/
public void setSelectedFields(String v) { state.fields = v; }
public String getKind() { return state.kind; }
public void setKind(String v) { state.kind = v; }
public DateTime getPublished() { return state.published; }
public void setPublished(DateTime v) {
if (v != null && v.getTzShift() == null) {
throw new IllegalArgumentException(
"Entry.published must have a timezone.");
}
state.published = v;
}
public DateTime getUpdated() { return state.updated; }
public void setUpdated(DateTime v) {
if (v != null && v.getTzShift() == null) {
throw new IllegalArgumentException("Entry.updated must have a timezone.");
}
state.updated = v;
}
public DateTime getEdited() { return state.edited; }
public void setEdited(DateTime v) {
if (v != null && v.getTzShift() == null) {
throw new IllegalArgumentException("Entry.edited must have a timezone.");
}
state.edited = v;
}
public Set<Category> getCategories() { return state.categories; }
public TextConstruct getTitle() { return state.title; }
public void setTitle(TextConstruct v) { state.title = v; }
public TextConstruct getSummary() { return state.summary; }
public void setSummary(TextConstruct v) { state.summary = v; }
public TextConstruct getRights() { return state.rights; }
public void setRights(TextConstruct v) { state.rights = v; }
public Content getContent() { return state.content; }
public void setContent(Content v) { state.content = v; }
/**
* Assumes the content element's contents are text and
* returns them as a TextContent.
*
* @return A TextContent containing the value of the content tag.
*
* @throws IllegalStateException
* If the content element is not a text type.
*/
public TextContent getTextContent() {
Content content = getContent();
if (!(content instanceof TextContent)) {
throw new IllegalStateException("Content object is not a TextContent");
}
return (TextContent) getContent();
}
/**
* Assumes the <content> element's contents are plain-text and
* returns its value as a string
*
* @return A string containing the plain-text value of the content tag.
*
* @throws IllegalStateException
* If the content element is not a text type.
*/
public String getPlainTextContent() {
TextConstruct textConstruct = getTextContent().getContent();
if (!(textConstruct instanceof PlainTextConstruct)) {
throw new IllegalStateException(
"TextConstruct object is not a PlainTextConstruct");
}
return textConstruct.getPlainText();
}
public void setContent(TextConstruct tc) {
state.content = new TextContent(tc);
}
public List<Link> getLinks() { return state.links; }
public void addLink(Link link) {
state.links.add(link);
}
public Link addLink(String rel, String type, String href) {
Link link = new Link(rel, type, href);
addLink(link);
return link;
}
public List<Person> getAuthors() { return state.authors; }
public List<Person> getContributors() { return state.contributors; }
public Source getSource() { return state.source; }
public void setSource(Source v) { state.source = v; }
/**
* Set draft status. Passing a null value means unsetting the draft
* status.
*
* @param v Draft status, or null to unset.
*/
public void setDraft(Boolean v) {
if (state.pubControl == null) {
if (!Boolean.TRUE.equals(v)) {
// No need to create a PubControl entry for that
return;
}
state.pubControl = new PubControl();
}
state.pubControl.setDraft(v);
}
/**
* Draft status.
*
* @return True if draft status is set and equals true.
*/
public boolean isDraft() {
return state.pubControl != null ? state.pubControl.isDraft() : false;
}
/**
* Gets the app:control tag.
*
* @return pub control tag or null if unset
*/
public PubControl getPubControl() { return state.pubControl; }
/**
* Sets the app:control tag, which usually contains app:draft.
*
* @param value PubControl the new object or null
*/
public void setPubControl(PubControl value) { state.pubControl = value; }
public void setService(Service s) { state.service = s; }
public Service getService() { return state.service; }
public boolean getCanEdit() { return state.canEdit; }
public void setCanEdit(boolean v) { state.canEdit = v; }
public void addAdaptor(Kind.Adaptor adaptor) {
state.adaptable.addAdaptor(adaptor);
}
public Collection<Kind.Adaptor> getAdaptors() {
return state.adaptable.getAdaptors();
}
public <A extends Kind.Adaptor> A getAdaptor(Class<A> adaptorClass) {
return state.adaptable.getAdaptor(adaptorClass);
}
/**
* Retrieves the first link with the supplied {@code rel} and/or
* {@code type} value.
* <p>
* If either parameter is {@code null}, doesn't return matches
* for that parameter.
*/
public Link getLink(String rel, String type) {
for (Link link : state.links) {
if (link.matches(rel, type)) {
return link;
}
}
return null;
}
/**
* Return the links that match the given {@code rel} and {@code type} values.
*
* @param relToMatch {@code rel} value to match or {@code null} to match any
* {@code rel} value.
* @param typeToMatch {@code type} value to match or {@code null} to match any
* {@code type} value.
* @return matching links.
*/
public List<Link> getLinks(String relToMatch, String typeToMatch) {
List<Link> result = new ArrayList<Link>();
for (Link link : state.links) {
if (link.matches(relToMatch, typeToMatch)) {
result.add(link);
}
}
return result;
}
/**
* Remove all links that match the given {@code rel} and {@code type} values.
*
* @param relToMatch {@code rel} value to match or {@code null} to match any
* {@code rel} value.
* @param typeToMatch {@code type} value to match or {@code null} to match any
* {@code type} value.
*/
public void removeLinks(String relToMatch, String typeToMatch) {
for (Iterator<Link> iterator = state.links.iterator();
iterator.hasNext();) {
Link link = iterator.next();
if (link.matches(relToMatch, typeToMatch)) {
iterator.remove();
}
}
}
/**
* Remove all links.
*/
public void removeLinks() {
state.links.clear();
}
/**
* Adds a link pointing to an HTML representation.
*
* @param htmlUri
* Link URI.
*
* @param lang
* Optional language code.
*
* @param title
* Optional title.
*/
public void addHtmlLink(String htmlUri, String lang, String title) {
Link link = new Link();
link.setRel(Link.Rel.ALTERNATE);
link.setType(Link.Type.HTML);
link.setHref(htmlUri);
if (lang != null) {
link.setHrefLang(lang);
}
if (title != null) {
link.setTitle(title);
}
state.links.add(link);
}
/** Retrieves the resource access link. */
public Link getSelfLink() {
Link selfLink = getLink(Link.Rel.SELF, Link.Type.ATOM);
return selfLink;
}
/** Retrieves the resource edit link. */
public Link getEditLink() {
Link editLink = getLink(Link.Rel.ENTRY_EDIT, Link.Type.ATOM);
return editLink;
}
/** Retrieves the media resource edit link. */
@SuppressWarnings("deprecation")
public Link getMediaEditLink() {
Link mediaLink = getLink(Link.Rel.MEDIA_EDIT, null);
if (mediaLink == null) {
// Temporary back compat support for old incorrect media link value.
// to the new value.
mediaLink = getLink(Link.Rel.MEDIA_EDIT_BACKCOMPAT, null);
}
return mediaLink;
}
/** Retrieves the media resource resumable upload link. */
public Link getResumableEditMediaLink() {
return getLink(Link.Rel.RESUMABLE_EDIT_MEDIA, null);
}
/** Retrieves the first HTML link. */
public Link getHtmlLink() {
Link htmlLink = getLink(Link.Rel.ALTERNATE, Link.Type.HTML);
return htmlLink;
}
/**
* Retrieves the current version of this Entry by requesting it from
* the associated GData service.
*
* @return the current version of the entry.
*/
public E getSelf() throws IOException, ServiceException {
if (state.service == null) {
throw new ServiceException(
CoreErrorDomain.ERR.entryNotAssociated);
}
Link selfLink = getSelfLink();
if (selfLink == null) {
throw new UnsupportedOperationException("Entry cannot be retrieved");
}
URL entryUrl = new URL(selfLink.getHref());
try {
// If an etag is available, use it to conditionalize the retrieval,
// otherwise, use time of last edit or update.
if (state.etag != null) {
return (E) state.service.getEntry(entryUrl, this.getClass(),
state.etag);
} else {
return (E) state.service.getEntry(entryUrl, this.getClass(),
(state.edited != null ? state.edited : state.updated));
}
} catch (NotModifiedException e) {
return (E) this;
}
}
/**
* Updates this entry by sending the current representation to the
* associated GData service.
*
* @return the updated entry returned by the Service.
*
* @throws ServiceException
* If there is no associated GData service or the service is
* unable to perform the update.
*
* @throws UnsupportedOperationException
* If update is not supported for the target entry.
*
* @throws IOException
* If there is an error communicating with the GData service.
*/
public E update() throws IOException, ServiceException {
if (state.service == null) {
throw new ServiceException(
CoreErrorDomain.ERR.entryNotAssociated);
}
Link editLink = getEditLink();
if (editLink == null) {
throw new UnsupportedOperationException("Entry cannot be updated");
}
URL editUrl = new URL(editLink.getHref());
return (E) state.service.update(editUrl, this);
}
/**
* Deletes this entry by sending a request to the associated GData
* service.
*
* @throws ServiceException
* If there is no associated GData service or the service is
* unable to perform the deletion.
*
* @throws UnsupportedOperationException
* If deletion is not supported for the target entry.
*
* @throws IOException
* If there is an error communicating with the GData service.
*/
public void delete() throws IOException, ServiceException {
if (state.service == null) {
throw new ServiceException(
CoreErrorDomain.ERR.entryNotAssociated);
}
Link editLink = getEditLink();
if (editLink == null) {
throw new UnsupportedOperationException("Entry cannot be deleted");
}
// Delete the entry, using strong etag (if available) as a precondition.
URL editUrl = new URL(editLink.getHref());
state.service.delete(editUrl,
GDataProtocol.isWeakEtag(state.etag) ? null : state.etag);
}
/**
* The OutOfLineReference class adapts an {@link OutOfLineContent} instance
* to implement the {@link Reference} interface so nested content references
* will be post-processed.
*/
private class OutOfLineReference implements Reference, Extension {
// This ugliness is necessary because there's no unifying base abstraction
// for all data elements and the current visitor model only acts upon
// Extension types (which Content is not). This is fixed in the new data
// model, at which time we can just have OolContent be visited and wrap it
// in something that implement the Reference interface inside of
// ReferenceVisitor.visit().
private OutOfLineContent oolContent;
private OutOfLineReference(OutOfLineContent oolContent) {
this.oolContent = oolContent;
}
public String getHref() {
return oolContent.getUri();
}
public void setHref(String href) {
oolContent.setUri(href);
}
public void generate(XmlWriter w, ExtensionProfile extProfile) {
throw new IllegalStateException("Should not be generated");
}
public ElementHandler getHandler(ExtensionProfile extProfile,
String namespace, String localName, Attributes attrs) {
throw new IllegalStateException("Should not be parsed");
}
}
@Override
protected void visitChildren(ExtensionVisitor ev)
throws ExtensionVisitor.StoppedException {
// Add out of line content to the visitor pattern by wrapping in an
// adaptor. This is necessary so the src reference can be processed.
if (state.content instanceof OutOfLineContent) {
this.visitChild(ev,
new OutOfLineReference((OutOfLineContent) state.content));
}
// Add nested links to the visitor pattern
for (Link link : getLinks()) {
this.visitChild(ev, link);
}
super.visitChildren(ev);
}
@Override
public void generate(XmlWriter w, ExtensionProfile p) throws IOException {
generateAtom(w, p);
}
/**
* Generates XML in the Atom format.
*
* @param w
* Output writer.
*
* @param extProfile
* Extension profile.
*
* @throws IOException
*/
public void generateAtom(XmlWriter w, ExtensionProfile extProfile)
throws IOException {
Set<XmlNamespace> nsDecls =
new LinkedHashSet<XmlNamespace>(namespaceDeclsAtom);
nsDecls.addAll(extProfile.getNamespaceDecls());
ArrayList<XmlWriter.Attribute> attrs =
new ArrayList<XmlWriter.Attribute>(3);
if (state.kind != null
&& Service.getVersion().isAfter(Service.Versions.V1)) {
nsDecls.add(Namespaces.gNs);
attrs.add(new XmlWriter.Attribute(Namespaces.gAlias, "kind", state.kind));
}
if (state.etag != null &&
!Service.getVersion().isCompatible(Service.Versions.V1)) {
nsDecls.add(Namespaces.gNs);
attrs.add(new XmlWriter.Attribute(Namespaces.gAlias, "etag", state.etag));
}
if (state.fields != null &&
Service.getVersion().isAfter(Service.Versions.V1)) {
nsDecls.add(Namespaces.gNs);
attrs.add(new XmlWriter.Attribute(
Namespaces.gAlias, "fields", state.fields));
}
// Add any attribute extensions.
AttributeGenerator generator = new AttributeGenerator();
putAttributes(generator);
generateAttributes(attrs, generator);
generateStartElement(w, Namespaces.atomNs, "entry", attrs, nsDecls);
if (state.id != null) {
w.simpleElement(Namespaces.atomNs, "id", null, state.id);
}
if (state.published != null) {
w.simpleElement(Namespaces.atomNs, "published", null,
state.published.toString());
}
if (state.updated != null) {
w.simpleElement(Namespaces.atomNs, "updated", null,
state.updated.toString());
}
if (state.edited != null) {
w.simpleElement(getAtomPubNs(), "edited", null,
state.edited.toString());
}
if (state.pubControl != null) {
state.pubControl.generateAtom(w, extProfile);
}
w.startRepeatingElement();
for (Category cat : state.categories) {
cat.generateAtom(w);
}
w.endRepeatingElement();
if (state.title != null) {
state.title.generateAtom(w, "title");
}
if (state.summary != null) {
state.summary.generateAtom(w, "summary");
}
if (state.rights != null) {
state.rights.generateAtom(w, "rights");
}
if (state.content != null) {
state.content.generateAtom(w, extProfile);
}
w.startRepeatingElement();
for (Link link : state.links) {
link.generateAtom(w, extProfile);
}
w.endRepeatingElement();
w.startRepeatingElement();
for (Person author : state.authors) {
author.generateAtom(extProfile, w, "author");
}
w.endRepeatingElement();
w.startRepeatingElement();
for (Person contributor : state.contributors) {
contributor.generateAtom(extProfile, w, "contributor");
}
w.endRepeatingElement();
if (state.source != null) {
state.source.generateAtom(w, extProfile);
}
// Invoke ExtensionPoint.
generateExtensions(w, extProfile);
w.endElement(Namespaces.atomNs, "entry");
}
/**
* Generates XML in the RSS format.
*
* @param w
* Output writer.
*
* @param extProfile
* Extension profile.
*
* @throws IOException
*/
public void generateRss(XmlWriter w,
ExtensionProfile extProfile) throws IOException {
Vector<XmlNamespace> nsDecls = new Vector<XmlNamespace>(namespaceDeclsRss);
nsDecls.addAll(extProfile.getNamespaceDecls());
generateStartElement(w, Namespaces.rssNs, "item", null, nsDecls);
if (state.id != null) {
List<Attribute> attrs = new ArrayList<Attribute>(1);
attrs.add(new Attribute("isPermaLink", "false"));
w.simpleElement(Namespaces.rssNs, "guid", attrs, state.id);
}
String lang = null;
if (state.content != null) {
lang = state.content.getLang();
}
if (lang == null && state.summary != null) {
lang = state.summary.getLang();
}
if (lang == null && state.title != null) {
lang = state.title.getLang();
}
if (lang != null) {
w.simpleElement(Namespaces.rssNs, "language", null, lang);
}
if (state.published != null) {
w.simpleElement(Namespaces.rssNs, "pubDate", null,
state.published.toStringRfc822());
}
if (state.updated != null) {
w.simpleElement(Namespaces.atomNs, "updated", null,
state.updated.toString());
}
w.startRepeatingElement();
for (Category cat : state.categories) {
cat.generateRss(w);
}
w.endRepeatingElement();
if (state.title != null) {
state.title.generateRss(w, "title", TextConstruct.RssFormat.PLAIN_TEXT);
}
if (state.summary != null) {
state.summary.generateAtom(w, "summary");
}
if (state.content != null) {
state.content.generateRss(w, extProfile);
}
w.startRepeatingElement();
for (Link link : state.links) {
link.generateRss(w);
}
w.endRepeatingElement();
w.startRepeatingElement();
for (Person author : state.authors) {
author.generateRss(w, "author");
}
w.endRepeatingElement();
w.startRepeatingElement();
for (Person contributor : state.contributors) {
contributor.generateRss(w, "author");
}
w.endRepeatingElement();
// Invoke ExtensionPoint.
generateExtensions(w, extProfile);
w.endElement(Namespaces.rssNs, "item");
}
/** Top-level namespace declarations for generated XML. */
private static final Collection<XmlNamespace> namespaceDeclsAtom =
new Vector<XmlNamespace>(1);
private static final Collection<XmlNamespace> namespaceDeclsRss =
new Vector<XmlNamespace>(1);
static {
namespaceDeclsAtom.add(Namespaces.atomNs);
namespaceDeclsRss.add(Namespaces.atomNs);
}
/**
* Reads an entry representation from the provided {@link ParseSource}.
* The return type of the entry will be determined using dynamic adaptation
* based upon any {@link Kind} category tag found in the input content. If
* no kind tag is found an {@link Entry} instance will be returned.
*/
public static BaseEntry<?> readEntry(ParseSource source)
throws IOException, ParseException, ServiceException {
return readEntry(source, null, null);
}
/**
* Reads an entry of type {@code T} using the given extension profile.
*/
public static <T extends BaseEntry> T readEntry(ParseSource source,
Class <T> entryClass, ExtensionProfile extProfile)
throws IOException, ParseException, ServiceException {
return ParseUtil.readEntry(source, entryClass, extProfile, null);
}
/**
* Parses XML in the Atom format.
*
* @param extProfile
* Extension profile.
*
* @param input
* XML input stream.
*/
public void parseAtom(ExtensionProfile extProfile,
InputStream input) throws IOException,
ParseException {
AtomHandler handler = new AtomHandler(extProfile);
new XmlParser().parse(input, handler, Namespaces.atom, "entry");
}
/**
* Parses XML in the Atom format.
*
* @param extProfile
* Extension profile.
*
* @param reader
* XML Reader. The caller is responsible for ensuring
* that the character encoding is correct.
*/
public void parseAtom(ExtensionProfile extProfile,
Reader reader) throws IOException,
ParseException {
AtomHandler handler = new AtomHandler(extProfile);
new XmlParser().parse(reader, handler, Namespaces.atom, "entry");
}
/**
* Parses XML in the Atom format from a parser-defined content source.
*
* @param extProfile
* Extension profile.
* @param eventSource
* XML event source.
*/
public void parseAtom(ExtensionProfile extProfile,
XmlEventSource eventSource) throws IOException, ParseException {
AtomHandler handler = new AtomHandler(extProfile);
new EventSourceParser(handler, Namespaces.atom, "entry")
.parse(eventSource);
}
/** Returns information about the content element processing. */
protected Content.ChildHandlerInfo getContentHandlerInfo(
ExtensionProfile extProfile, Attributes attrs)
throws ParseException, IOException {
return Content.getChildHandler(extProfile, attrs);
}
@Override
public ElementHandler getHandler(ExtensionProfile p, String namespace,
String localName, Attributes attrs) {
return new AtomHandler(p);
}
/** {@code <atom:entry>} parser. */
public class AtomHandler extends ExtensionPoint.ExtensionHandler {
public AtomHandler(ExtensionProfile extProfile) {
super(extProfile, BaseEntry.this.getClass());
}
@Override
public void processAttribute(String namespace, String localName,
String value) throws ParseException {
if (namespace.equals(Namespaces.g)) {
if (localName.equals("etag")) {
setEtag(value);
return;
}
if (localName.equals("fields")) {
setSelectedFields(value);
return;
}
if (localName.equals("kind")) {
setKind(value);
return;
}
}
super.processAttribute(namespace, localName, value);
}
@Override
public XmlParser.ElementHandler getChildHandler(String namespace,
String localName,
Attributes attrs)
throws ParseException, IOException {
if (namespace.equals(Namespaces.atom)) {
if (localName.equals("id")) {
return new IdHandler();
} else if (localName.equals("published")) {
return new PublishedHandler();
} else if (localName.equals("updated")) {
return new UpdatedHandler();
} else if (localName.equals("title")) {
TextConstruct.ChildHandlerInfo chi =
TextConstruct.getChildHandler(attrs);
if (state.title != null) {
throw new ParseException(
CoreErrorDomain.ERR.duplicateTitle);
}
state.title = chi.textConstruct;
return chi.handler;
} else if (localName.equals("summary")) {
TextConstruct.ChildHandlerInfo chi =
TextConstruct.getChildHandler(attrs);
if (state.summary != null) {
throw new ParseException(
CoreErrorDomain.ERR.duplicateSummary);
}
state.summary = chi.textConstruct;
return chi.handler;
} else if (localName.equals("rights")) {
TextConstruct.ChildHandlerInfo chi =
TextConstruct.getChildHandler(attrs);
if (state.rights != null) {
throw new ParseException(
CoreErrorDomain.ERR.duplicateRights);
}
state.rights = chi.textConstruct;
return chi.handler;
} else if (localName.equals("content")) {
if (state.content != null) {
throw new ParseException(
CoreErrorDomain.ERR.duplicateContent);
}
Content.ChildHandlerInfo chi =
getContentHandlerInfo(extProfile, attrs);
state.content = chi.content;
return chi.handler;
} else if (localName.equals("category")) {
Category cat = new Category();
return cat.new AtomHandler(extProfile, state.categories,
BaseEntry.this);
} else if (localName.equals("link")) {
Link link = new Link();
state.links.add(link);
return link.new AtomHandler(extProfile);
} else if (localName.equals("author")) {
// check for an author extension
Person author;
ExtensionDescription extDescription = getExtensionDescription(
extProfile, extendedClass, namespace, localName);
if (extDescription != null
&& extDescription.getExtensionClass() != null) {
author = (Person) createExtensionInstance(
extDescription.getExtensionClass());
} else {
author = new Person();
}
state.authors.add(author);
return author.getHandler(extProfile, namespace, localName, attrs);
} else if (localName.equals("contributor")) {
Person contributor = new Person();
state.contributors.add(contributor);
return contributor.new AtomHandler(extProfile);
} else if (localName.equals("source")) {
state.source = new Source();
return state.source.new SourceHandler(extProfile);
}
} else if (namespace.equals(getAtomPubNs().getUri())) {
if (localName.equals("control")) {
state.pubControl = new PubControl();
return state.pubControl.new AtomHandler(extProfile);
} else if (localName.equals("edited")) {
return new EditedHandler();
}
} else {
return super.getChildHandler(namespace, localName, attrs);
}
return null;
}
@Override
public void processEndElement() throws ParseException {
// Skip extension point validation for batch response entries
if (getExtension(BatchStatus.class) == null &&
getExtension(BatchInterrupted.class) == null) {
super.processEndElement();
}
}
/** {@code <atom:id>} parser. */
class IdHandler extends XmlParser.ElementHandler {
@Override
public void processEndElement() throws ParseException {
if (state.id != null) {
throw new ParseException(
CoreErrorDomain.ERR.duplicateEntryId);
}
if (value == null) {
throw new ParseException(
CoreErrorDomain.ERR.idValueRequired);
}
state.id = value;
}
}
/** {@code <atom:published>} parser. */
class PublishedHandler extends Rfc3339Handler {
@Override
public void processEndElement() throws ParseException {
super.processEndElement();
state.published = getDateTime();
}
}
/** {@code <atom:updated>} parser. */
class UpdatedHandler extends Rfc3339Handler {
@Override
public void processEndElement() throws ParseException {
super.processEndElement();
state.updated = getDateTime();
}
}
/** {@code <app:edited>} parser. */
class EditedHandler extends Rfc3339Handler {
@Override
public void processEndElement() throws ParseException {
super.processEndElement();
state.edited = getDateTime();
}
}
}
/**
* Locates and returns the most specific {@link Kind.Adaptor} BaseEntry
* subtype for this entry. If none can be found for the current class,
* {@code null} will be returned.
*
* @throws Kind.AdaptorException for subclasses to throw.
*/
public BaseEntry<?> getAdaptedEntry() throws Kind.AdaptorException {
BaseEntry<?> adaptedEntry = null;
// Find the BaseEntry adaptor instance that is most specific.
for (Kind.Adaptor adaptor : getAdaptors()) {
if (!(adaptor instanceof BaseEntry)) {
continue;
}
// If first matching adaptor or a narrower subtype of the current one,
// then use it.
if (adaptedEntry == null ||
adaptedEntry.getClass().isAssignableFrom(adaptor.getClass())) {
adaptedEntry = (BaseEntry<?>) adaptor;
}
}
return adaptedEntry;
}
}