/* 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.client.CoreErrorDomain;
import com.google.gdata.client.Query;
import com.google.gdata.client.Service;
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.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.Vector;
/**
* The BaseFeed class is an abstract base class that represents a
* generic GData feed object, based primarily on the data model for
* an {@code <atom:feed>} element. It is extended to represent
* OpenSearch RSS channel elements, and also supports generalized
* extensibility using a defined ExtensionProfile and/or by stored
* extended data as an XmlBlob.
* <p>
* The BaseFeedClass is a generic class that is parameterized by the
* type of Entry that will be contained within the feed. The base
* class contains all the necessary parsing and generation code for
* feed extension data, but can be subclassed to create subtypes that
* contain convenience APIs for accessing extension elements and
* entries.
* <p>
* An instance can be initialized by parsing an Atom 1.0 feed
* from a Reader or by directly initializing its component
* elements. It can generate an XML representation of the feed
* to an XmlWriter in either Atom 1.0 or RSS 2.0 format.
* <p>
* Here is the Relax-NG schema that represents an Atom 1.0
* feed:
* <pre>
* AtomFeed =
* element atom:feed {
* atomCommonAttributes,
* (atomAuthor*
* atomCategory*
* atomContributor*
* atomGenerator?
* atomIcon?
* atomId
* atomLink*
* atomLogo?
* atomRights?
* atomSubtitle?
* atomTitle
* atomUpdated
* extensionElement*),
* atomEntry*
* }
* </pre>
*
* Because the Feed schema differs from the Source schema only by the
* presence of the entries, the Feed class derives its base property
* model and parsing/generation implementations from the Source class.
* <p>
* The BaseFeed 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 BaseFeed
* subtypes that use Atom/RSS extensions to extend the content model for a
* particular type of data.
* <p>
* An {@link Kind.Adaptor} subclass of BaseFeed 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 {@code Class} and {@code BaseFeed}
* parameter as an argument that is used when adapting a generic feed 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>
*
* @param <F> feed type associated with bound subtype.
* @param <E> entry type associated with bound subtype.
*/
public abstract class BaseFeed<F extends BaseFeed, E extends BaseEntry>
extends Source
implements Kind.Adaptable, Kind.Adaptor, IFeed {
/**
* The FeedState class provides a simple structure that encapsulates
* the attributes of an Atom feed that should be shared with a shallow
* copy if the feed is adapted to a more specific BaseFeed
* {@link Kind.Adaptor} subtypes.
* <p>
* <b>Note: Feed entries are not part of feed shared state, because
* the entry lists will need to be typed differently for adapted
* instances.</b> This means that entries that are created, updated, or
* deleted in an adapted feed will not be reflected in the base feed
* used to construct it. The reverse is also true: changes made to a base
* feed will not be reflected in any adapted instances of the feed.
*
* @see BaseFeed#BaseFeed(Class, BaseFeed)
*/
protected static class FeedState {
/** Service associated with the feed. */
public Service service;
/** Specifies whether the feed can be posted to. */
public boolean canPost = true;
/** OpenSearch: number of search results (feed entries). */
public int totalResults = Query.UNDEFINED;
/** OpenSearch: start index. */
public int startIndex = Query.UNDEFINED;
/** OpenSearch: items per page. */
public int itemsPerPage = Query.UNDEFINED;
/** Adaptable helper */
public Kind.Adaptable adaptable = new Kind.AdaptableHelper();
/** Etag.
* Etag. See RFC 2616, Section 3.11.
* If there is no entity tag, this variable is null.
*/
public String etag;
}
/**
* Basic state for this feed. May be shared across multiple adapted
* instances associated with the same logical feed.
*/
protected FeedState feedState;
/**
* Class used to construct new entry instance, initialized at construction.
*/
protected Class<? extends E> entryClass;
/** Feed entries. */
protected List<E> entries = new LinkedList<E>();
/**
* Copy constructor that initializes a new BaseFeed instance to have
* identical contents to another instance, using a shared reference to
* the same {@link FeedState}. {@link Kind.Adaptor} subclasses
* of {@code BaseFeed} can use this constructor to create adaptor
* instances of an entry that share state with the original.
*
* @param entryClass
* Class used to construct new Entry instances for the Feed.
*/
protected BaseFeed(Class<? extends E> entryClass) {
feedState = new FeedState();
this.entryClass = entryClass;
}
/**
* Copy constructor that initializes a new BaseFeed instance to have
* identical contents to another instance, using a shared reference to
* the same {@link FeedState}. {@link Kind.Adaptor} subclasses
* of {@code BaseFeed} can use this constructor to create adaptor
* instances of a feed that share state with the original.
*/
protected BaseFeed(Class<? extends E> entryClass, BaseFeed<?, ?> sourceFeed) {
super(sourceFeed);
feedState = sourceFeed.feedState;
this.entryClass = entryClass;
}
/**
* {@inheritDoc}
* <p>
* The implementation of this method for BaseFeed will declare any
* extensions associated with the contained entry type.
*/
@Override
public void declareExtensions(ExtensionProfile extProfile) {
// Create an instance of the associated entry class and declare its
// extensions.
E entry = createEntry();
extProfile.addDeclarations(entry);
}
/**
* Returns that GData {@link Service} instance tassociated with this feed.
*/
public Service getService() { return feedState.service; }
/**
* Sets that GData {@link Service} instance associated with this feed.
*/
public void setService(Service v) {
feedState.service = v;
// Propagate service information to nested entries
for (E entry : entries) {
entry.setService(v);
}
}
/**
* Gets the property that indicates if it is possible to post new entries
* to the feed.
*/
public boolean getCanPost() { return feedState.canPost; }
/**
* Sets the property that indicates if it is possible to post new entries
* to the feed.
*/
public void setCanPost(boolean v) { feedState.canPost = v; }
/**
* Returns the current entity tag value for this feed. A value of
* {@code null} indicates the value is unknown.
*/
public String getEtag() { return feedState.etag; }
/**
* Sets the current entity tag value (for this feed. A value of
* {@code null} indicates the value is unknown.
*/
public void setEtag(String v) { feedState.etag = v; }
/**
* Gets the total number of results associated with this feed. The value
* may be larger than the number of contained entries for paged feeds.
* A value of {@link Query#UNDEFINED} indicates the total size is undefined.
*/
public int getTotalResults() { return feedState.totalResults; }
/**
* Sets the total number of results associated with this feed. The value
* may be larger than the number of contained entries for paged feeds.
* A value of {@link Query#UNDEFINED} indicates the total size is undefined.
*/
public void setTotalResults(int v) { feedState.totalResults = v; }
/**
* Gets the starting index of the contained entries for paged feeds.
* A value of {@link Query#UNDEFINED} indicates the start index is undefined.
*/
public int getStartIndex() { return feedState.startIndex; }
/**
* Sets the starting index of the contained entries for paged feeds.
* A value of {@link Query#UNDEFINED} indicates the start index is undefined.
*/
public void setStartIndex(int v) { feedState.startIndex = v; }
/**
* Gets the number of items that will be returned per page for paged feeds.
* A value of {@link Query#UNDEFINED} indicates the page item count is
* undefined.
*/
public int getItemsPerPage() { return feedState.itemsPerPage; }
/**
* Sets the number of items that will be returned per page for paged feeds.
* A value of {@link Query#UNDEFINED} indicates the page item count is
* undefined.
*/
public void setItemsPerPage(int v) { feedState.itemsPerPage = v; }
/** Returns the list of entries in this feed */
public List<E> getEntries() { return entries; }
/** Sets the list to use for storing the entry list */
public void setEntries(List<E> entryList) { this.entries = entryList; }
// Implementation of Adaptable methods
//
public void addAdaptor(Kind.Adaptor adaptor) {
feedState.adaptable.addAdaptor(adaptor);
}
public Collection<Kind.Adaptor> getAdaptors() {
return feedState.adaptable.getAdaptors();
}
public <A extends Kind.Adaptor> A getAdaptor(Class<A> adaptorClass) {
return feedState.adaptable.getAdaptor(adaptorClass);
}
/**
* Creates a new entry for the feed.
*/
public E createEntry() {
E entry;
try {
entry = entryClass.newInstance();
} catch (InstantiationException e) {
throw new IllegalStateException(e);
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
// Propagate the associated service (if any)
if (feedState.service != null) {
entry.setService(feedState.service);
}
return entry;
}
/** Returns the entry post link for the feed. */
public Link getEntryPostLink() {
Link postLink = getLink(Link.Rel.ENTRY_POST, Link.Type.ATOM);
return postLink;
}
/** Returns the self link for the feed. */
public Link getSelfLink() {
Link postLink = getLink(Link.Rel.SELF, Link.Type.ATOM);
return postLink;
}
/**
* Returns the link that provides the URI of next page in a paged feed.
*
* @return Link that provides the URI of next page in a paged feed or {@code
* null} for none.
*/
public Link getNextLink() {
return getLink(Link.Rel.NEXT, Link.Type.ATOM);
}
/**
* Returns the link that provides the URI of previous page in a paged feed.
*
* @return Link that provides the URI of previous page in a paged feed or
* {@code null} for none.
*/
public Link getPreviousLink() {
return getLink(Link.Rel.PREVIOUS, Link.Type.ATOM);
}
/**
* Returns the link that provides the URI that can be used to batch operations
* to query, insert, update and delete entries on this feed.
*
* @return Link that provides the URI that can be used to batch operations to
* query, insert, update and delete entries on this feed or {@code null}
* for none.
*/
public Link getFeedBatchLink() {
return getLink(Link.Rel.FEED_BATCH, Link.Type.ATOM);
}
/**
* Returns the current representation of the feed by requesting it from
* the associated service using the feed's self link.
*
* @return the current state of the feed.
*/
@SuppressWarnings("unchecked")
public F getSelf() throws IOException, ServiceException {
if (feedState.service == null) {
throw new ServiceException(
CoreErrorDomain.ERR.feedNotAssociated);
}
Link selfLink = getSelfLink();
if (selfLink == null) {
throw new UnsupportedOperationException("Feed cannot be retrieved");
}
URL feedUrl = new URL(selfLink.getHref());
try {
// Use Etag if available to conditionalize the retrieval, otherwise use
// the updated value.
if (feedState.etag != null) {
return (F) feedState.service.getFeed(feedUrl, this.getClass(),
feedState.etag);
} else {
return (F) feedState.service.getFeed(feedUrl, this.getClass(),
srcState.updated);
}
} catch (NotModifiedException e) {
return (F) this;
}
}
/**
* Inserts a new Entry into the feed, if the feed is currently
* associated with a Service.
*
* @return the inserted Entry returned by the Service.
*
* @throws ServiceException
* If there is no associated GData service or the service is
* unable to perform the insertion.
*
* @throws UnsupportedOperationException
* If insert is not supported for the target feed.
*
* @throws IOException
* If there is an error communicating with the GData service.
*/
public <T extends E> T insert(T newEntry)
throws ServiceException, IOException {
if (feedState.service == null) {
throw new ServiceException(
CoreErrorDomain.ERR.entryNotAssociated);
}
Link postLink = getEntryPostLink();
if (postLink == null) {
throw new UnsupportedOperationException("Media cannot be inserted");
}
URL postUrl = new URL(postLink.getHref());
return feedState.service.insert(postUrl, newEntry);
}
@Override
protected void visitChildren(ExtensionVisitor ev)
throws ExtensionVisitor.StoppedException {
// Add nested entries and links to the visitor pattern
for (BaseEntry<E> entry : entries) {
this.visitChild(ev, entry);
}
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
*/
@Override
public void generateAtom(XmlWriter w, ExtensionProfile extProfile)
throws IOException {
generateFeedStart(extProfile, w, null);
generateEntries(w, extProfile);
generateFeedEnd(w);
}
private void generateEntries(XmlWriter w, ExtensionProfile extProfile)
throws IOException {
// Generate all entries
w.startRepeatingElement();
for (E entry : entries) {
entry.generateAtom(w, extProfile);
}
w.endRepeatingElement();
}
/**
* Closes everything that was opened by
* {@link #generateFeedStart}.
*
* @param w
* @throws IOException
*/
public void generateFeedEnd(XmlWriter w) throws IOException {
w.endElement(Namespaces.atomNs, "feed");
}
/**
* Generates everything that's in the feed up and not including to the
* entries. The idea is to use generateStart(), write the entries end then
* call {@link #generateFeedEnd(com.google.gdata.util.common.xml.XmlWriter)}
* to avoid having to add entries to a list and keep them in memory.
*
* @param extProfile
* @param w
* @param namespaces extra namespace declarations
* @throws IOException
*/
public void generateFeedStart(ExtensionProfile extProfile,
XmlWriter w,
Collection<XmlNamespace> namespaces) throws
IOException {
XmlNamespace openSearchNs = Namespaces.getOpenSearchNs();
Set<XmlNamespace> nsDecls = new LinkedHashSet<XmlNamespace>();
nsDecls.add(Namespaces.atomNs);
nsDecls.add(openSearchNs);
nsDecls.addAll(extProfile.getNamespaceDecls());
ArrayList<XmlWriter.Attribute> attrs =
new ArrayList<XmlWriter.Attribute>(3);
if (feedState.etag != null &&
!Service.getVersion().isCompatible(Service.Versions.V1)) {
nsDecls.add(Namespaces.gNs);
attrs.add(new XmlWriter.Attribute(Namespaces.gAlias, "etag", feedState.etag));
}
// Add any attribute extensions.
AttributeGenerator generator = new AttributeGenerator();
putAttributes(generator);
generateAttributes(attrs, generator);
generateStartElement(w, Namespaces.atomNs, "feed", attrs, nsDecls);
// Generate base feed elements
generateInnerAtom(w, extProfile);
// Generate OpenSearch elements
if (getTotalResults() != Query.UNDEFINED) {
w.simpleElement(openSearchNs, "totalResults", null,
String.valueOf(feedState.totalResults));
}
if (getStartIndex() != Query.UNDEFINED) {
w.simpleElement(openSearchNs, "startIndex", null,
String.valueOf(feedState.startIndex));
}
if (getItemsPerPage() != Query.UNDEFINED) {
w.simpleElement(openSearchNs, "itemsPerPage", null,
String.valueOf(feedState.itemsPerPage));
}
// Invoke ExtensionPoint.
generateExtensions(w, extProfile);
}
/**
* 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 {
XmlNamespace openSearchNs = Namespaces.getOpenSearchNs();
Vector<XmlNamespace> nsDecls = new Vector<XmlNamespace>();
nsDecls.add(Namespaces.atomNs);
nsDecls.add(openSearchNs);
nsDecls.addAll(extProfile.getNamespaceDecls());
w.startElement(Namespaces.rssNs, "rss", rssHeaderAttrs, nsDecls);
generateStartElement(w, Namespaces.rssNs, "channel", null, null);
if (srcState.id != null) {
w.simpleElement(Namespaces.atomNs, "id", null, srcState.id);
}
if (xmlBlob != null) {
String lang = xmlBlob.getLang();
if (lang != null) {
w.simpleElement(Namespaces.rssNs, "language", null, lang);
}
}
if (srcState.updated != null) {
w.simpleElement(Namespaces.rssNs, "lastBuildDate", null,
srcState.updated.toStringRfc822());
}
w.startRepeatingElement();
for (Category cat : srcState.categories) {
cat.generateRss(w);
}
w.endRepeatingElement();
if (srcState.title != null) {
srcState.title.generateRss(w, "title",
TextConstruct.RssFormat.PLAIN_TEXT);
}
if (srcState.subtitle != null) {
srcState.subtitle.generateRss(w, "description",
TextConstruct.RssFormat.FULL_HTML);
} else {
w.simpleElement(Namespaces.rssNs, "description", null, null);
}
Link htmlLink = getHtmlLink();
if (htmlLink != null) {
w.simpleElement(Namespaces.rssNs, "link", null, htmlLink.getHref());
}
if (srcState.logo != null || srcState.icon != null) {
w.startElement(Namespaces.rssNs, "image", null, null);
w.simpleElement(Namespaces.rssNs, "url", null,
srcState.logo != null ? srcState.logo : srcState.icon);
if (srcState.title != null) {
srcState.title.generateRss(w, "title",
TextConstruct.RssFormat.PLAIN_TEXT);
}
if (htmlLink != null) {
w.simpleElement(Namespaces.rssNs, "link", null, htmlLink.getHref());
}
w.endElement(Namespaces.rssNs, "image");
}
if (srcState.rights != null) {
srcState.rights.generateRss(w, "copyright",
TextConstruct.RssFormat.PLAIN_TEXT);
}
if (srcState.authors.size() > 0) {
srcState.authors.get(0).generateRss(w, "managingEditor");
}
if (srcState.generator != null) {
String name = srcState.generator.getName();
if (name != null) {
w.simpleElement(Namespaces.rssNs, "generator", null, name);
}
}
if (getTotalResults() != Query.UNDEFINED) {
w.simpleElement(openSearchNs, "totalResults", null,
String.valueOf(feedState.totalResults));
}
if (getStartIndex() != Query.UNDEFINED) {
w.simpleElement(openSearchNs, "startIndex", null,
String.valueOf(feedState.startIndex));
}
if (getItemsPerPage() != Query.UNDEFINED) {
w.simpleElement(openSearchNs, "itemsPerPage", null,
String.valueOf(feedState.itemsPerPage));
}
// Invoke ExtensionPoint.
generateExtensions(w, extProfile);
w.startRepeatingElement();
for (E entry : entries) {
entry.generateRss(w, extProfile);
}
w.endRepeatingElement();
w.endElement(Namespaces.rssNs, "channel");
w.endElement(Namespaces.rssNs, "rss");
}
/** Headers that are added to all RSS output */
private static final Collection<XmlWriter.Attribute> rssHeaderAttrs =
new Vector<XmlWriter.Attribute>(1);
static {
rssHeaderAttrs.add(new XmlWriter.Attribute("version", "2.0"));
}
/**
* Reads a feed representation from the provided {@link ParseSource}.
* The return type of the feed will be determined using dynamic adaptation
* based upon any {@link Kind} category tag found in the input content. If
* no kind tag is found a {@link Feed} instance will be returned.
*/
public static BaseFeed<?, ?> readFeed(ParseSource source)
throws IOException, ParseException, ServiceException {
return readFeed(source, null, null);
}
/**
* This method provides the base implementation of feed reading using either
* static or dynamic typing. If feedClass is non-null, the method is
* guaranteed to return an instance of this type, otherwise adaptation will
* be used to determine the type. The source object may be either an
* InputStream, Reader, or XmlParser.
*/
public static <F extends BaseFeed> F readFeed(ParseSource source,
Class <F> feedClass, ExtensionProfile extProfile)
throws IOException, ParseException, ServiceException {
return ParseUtil.readFeed(source, feedClass, extProfile);
}
/**
* Parses XML in the Atom format.
*
* @param extProfile
* Extension profile.
*
* @param input
* XML input stream.
*/
@Override
public void parseAtom(ExtensionProfile extProfile,
InputStream input) throws IOException,
ParseException {
FeedHandler handler = new FeedHandler(extProfile);
new XmlParser().parse(input, handler, Namespaces.atom, "feed");
}
/**
* 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.
*/
@Override
public void parseAtom(ExtensionProfile extProfile,
Reader reader) throws IOException,
ParseException {
FeedHandler handler = new FeedHandler(extProfile);
new XmlParser().parse(reader, handler, Namespaces.atom, "feed");
}
/**
* Parses XML in the Atom format from a parser-defined content source.
*
* @param extProfile
* Extension profile.
*
* @param source
* XML source.
*/
public void parseAtom(ExtensionProfile extProfile, XmlEventSource source)
throws IOException, ParseException {
FeedHandler handler = new FeedHandler(extProfile);
new EventSourceParser(handler, Namespaces.atom, "feed").parse(source);
}
@Override
public ElementHandler getHandler(ExtensionProfile p, String namespace,
String localName, Attributes attrs) throws ParseException {
return new FeedHandler(p);
}
/** {@code <atom:feed>} parser. */
public class FeedHandler extends SourceHandler {
private XmlNamespace openSearchNs = Namespaces.getOpenSearchNs();
public FeedHandler(ExtensionProfile extProfile) {
super(extProfile, BaseFeed.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;
}
}
super.processAttribute(namespace, localName, value);
}
@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, BaseFeed.this.getClass(),
namespace, localName, attrs);
if (extensionHandler != null) {
return extensionHandler;
}
if (namespace.equals(Namespaces.atom)) {
if (localName.equals("entry")) {
E entry = createEntry();
entries.add(entry);
return ((BaseEntry<?>) entry).new AtomHandler(
extProfile);
}
// All other elements in the Atom namespace are handled by
// the SourceHandler superclass
return super.getChildHandler(namespace, localName, attrs);
} else if (namespace.equals(openSearchNs.getUri())) {
if (localName.equals("totalResults")) {
return new TotalResultsHandler();
} else if (localName.equals("startIndex")) {
return new StartIndexHandler();
} else if (localName.equals("itemsPerPage")) {
return new ItemsPerPageHandler();
}
} else {
return super.getChildHandler(namespace, localName, attrs);
}
return null;
}
/** {@code <opensearch:totalResults>} parser. */
private class TotalResultsHandler extends ElementHandler {
@Override
public void processEndElement() throws ParseException {
if (feedState.totalResults != Query.UNDEFINED) {
throw new ParseException(
CoreErrorDomain.ERR.duplicateTotalResults);
}
if (value == null) {
throw new ParseException(
CoreErrorDomain.ERR.logoValueRequired);
}
try {
feedState.totalResults = Integer.valueOf(value).intValue();
} catch (NumberFormatException e) {
throw new ParseException(
CoreErrorDomain.ERR.totalResultsNotInteger);
}
}
}
/** {@code <opensearch:startIndex>} parser. */
private class StartIndexHandler extends ElementHandler {
@Override
public void processEndElement() throws ParseException {
if (feedState.startIndex != Query.UNDEFINED) {
throw new ParseException(
CoreErrorDomain.ERR.duplicateStartIndex);
}
if (value == null) {
throw new ParseException(
CoreErrorDomain.ERR.logoValueRequired);
}
try {
feedState.startIndex = Integer.valueOf(value).intValue();
} catch (NumberFormatException e) {
throw new ParseException(
CoreErrorDomain.ERR.startIndexNotInteger);
}
}
}
/** {@code <opensearch:itemsPerPage>} parser. */
private class ItemsPerPageHandler extends ElementHandler {
@Override
public void processEndElement() throws ParseException {
if (feedState.itemsPerPage != Query.UNDEFINED) {
throw new ParseException(
CoreErrorDomain.ERR.duplicateItemsPerPage);
}
if (value == null) {
throw new ParseException(
CoreErrorDomain.ERR.logoValueRequired);
}
try {
feedState.itemsPerPage = Integer.valueOf(value).intValue();
} catch (NumberFormatException e) {
throw new ParseException(
CoreErrorDomain.ERR.itemsPerPageNotInteger);
}
}
}
@Override
public void processEndElement() {
// Set the canPost flag based upon the presence of an entry post
// link relation in the parsed feed.
feedState.canPost = getEntryPostLink() != null;
}
}
/**
* Locates and returns the most specific {@link Kind.Adaptor} feed
* subtype for this feed. If none can be found for the current class,
* {@code null} will be returned.
*/
public BaseFeed<?, ?> getAdaptedFeed() throws Kind.AdaptorException {
BaseFeed adaptedFeed = null;
// Find the BaseFeed adaptor instance that is most specific.
for (Kind.Adaptor adaptor : getAdaptors()) {
if (!(adaptor instanceof BaseFeed)) {
continue;
}
// if first matching adaptor or a narrower subtype of the current one,
// then use it.
if (adaptedFeed == null ||
adaptedFeed.getClass().isAssignableFrom(adaptor.getClass())) {
adaptedFeed = (BaseFeed<?, ?>) adaptor;
}
}
// If an adapted feed was found, then also synchronize the current set
// of entries into it, adapting them as well.
if (adaptedFeed != null) {
List<E> sourceEntries;
if (adaptedFeed != this) {
sourceEntries = entries;
} else {
// Copy before clearing
sourceEntries = new ArrayList<E>();
sourceEntries.addAll(entries);
}
adaptedFeed.getEntries().clear();
for (E entry : sourceEntries) {
adaptedFeed.getEntries().add(entry.getAdaptedEntry());
}
}
return adaptedFeed;
}
/**
* Gets a list of entries of a particular kind.
*/
public <T extends BaseEntry> List<T> getEntries(Class<T> returnClass) {
List<T> adaptedEntries = new ArrayList<T>();
for (BaseEntry<?> entry : getEntries()) {
T adapted = entry.getAdaptor(returnClass);
if (adapted != null) {
adaptedEntries.add(adapted);
}
}
return adaptedEntries;
}
}