/* 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. *
* It is capable of parsing the Atom XML for an {@code
* 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.
*
* An {@link Kind.Adaptor} subclass of BaseEntry should do the following:
*
* 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
* 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 getLinks(String relToMatch, String typeToMatch) {
List result = new ArrayList();
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 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
*
*
* Here is the Relax-NG schema that represents an Atom 1.0 entry:
*
* atomEntry =
* element atom:entry {
* atomCommonAttributes,
* (atomAuthor*
* & atomCategory*
* & atomContent?
* & atomContributor*
* & atomId
* & atomLink*
* & atomPublished?
* & atomRights?
* & atomSource?
* & atomSummary?
* & atomTitle
* & atomUpdated
* & extensionElement*)
*
*
* @param