/* 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.XmlWriter; import com.google.gdata.client.CoreErrorDomain; import com.google.gdata.util.Namespaces; import com.google.gdata.util.ParseException; import com.google.gdata.util.XmlParser; import org.xml.sax.Attributes; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.ArrayList; import java.util.Iterator; /** * The Source class represents an Atom feed source object * primarily on the data model for an {@code <atom:source>} * element. * * Here is the Relax-NG schema that represents an Atom 1.0 * Source: * <pre> * atomSource = * element atom:source { * atomCommonAttributes, * (atomAuthor* * & atomCategory* * & atomContributor* * & atomGenerator? * & atomIcon? * & atomId? * & atomLink* * & atomLogo? * & atomRights? * & atomSubtitle? * & atomTitle? * & atomUpdated? * & extensionElement*) * } * </pre> * * * */ public class Source extends ExtensionPoint { /** * The SourceState class provides a simple structure that encapsulates * the attributes of an Atom source that should be shared with a shallow * copy if the entry is adapted to a more specific Source * {@link Kind.Adaptor} subtypes. * * @see Source#Source(Source) */ protected static class SourceState { /** Feed ID. */ public String id; /** Last updated timestamp. */ public DateTime updated; /** Categories. */ public HashSet<Category> categories = new HashSet<Category>(); /** Title. */ public TextConstruct title; /** Subtitle. */ public TextConstruct subtitle; /** Rights. */ public TextConstruct rights; /** Icon URI. */ public String icon; /** Logo image URI. */ public String logo; /** Links. */ public LinkedList<Link> links = new LinkedList<Link>(); /** Authors. */ public LinkedList<Person> authors = new LinkedList<Person>(); /** Contributors. */ public LinkedList<Person> contributors = new LinkedList<Person>(); /** Generator. */ public Generator generator; } /** * Basic state for this source. May be shared across multiple adapted * instances associated with the same logical source. */ protected SourceState srcState; /** * Constructs a new {@link Source} instance with no initial state. */ public Source() { srcState = new SourceState(); } /** * Copy constructor that initializes a new Source instance to have * identical contents to another instance, using a shared reference to * the same {@link SourceState}. */ protected Source(Source sourceSource) { super(sourceSource); srcState = sourceSource.srcState; } public String getId() { return srcState.id; } public void setId(String v) { srcState.id = v; } public DateTime getUpdated() { return srcState.updated; } public void setUpdated(DateTime v) { srcState.updated = v; } public Set<Category> getCategories() { return srcState.categories; } public TextConstruct getTitle() { return srcState.title; } public void setTitle(TextConstruct v) { srcState.title = v; } public TextConstruct getSubtitle() { return srcState.subtitle; } public void setSubtitle(TextConstruct v) { srcState.subtitle = v; } public TextConstruct getRights() { return srcState.rights; } public void setRights(TextConstruct v) { srcState.rights = v; } public String getIcon() { return srcState.icon; } public void setIcon(String v) { srcState.icon = v; } public String getLogo() { return srcState.logo; } public void setLogo(String v) { srcState.logo = v; } public List<Link> getLinks() { return srcState.links; } public List<Person> getAuthors() { return srcState.authors; } public List<Person> getContributors() { return srcState.contributors; } public Generator getGenerator() { return srcState.generator; } public void setGenerator(Generator v) { srcState.generator = v; } public Generator setGenerator(String version, String uri, String name) { Generator gen = new Generator(); gen.setVersion(version); gen.setUri(uri); gen.setName(name); setGenerator(gen); return gen; } /** * 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: srcState.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 : srcState.links) { if (link.matches(relToMatch, typeToMatch)) { result.add(link); } } return result; } public void addLink(Link link) { srcState.links.add(link); } public Link addLink(String rel, String type, String href) { Link link = new Link(rel, type, href); addLink(link); return link; } /** * 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 = srcState.links.iterator(); iterator.hasNext();) { Link link = iterator.next(); if (link.matches(relToMatch, typeToMatch)) { iterator.remove(); } } } /** * Removes all links. */ public void removeLinks() { srcState.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); } srcState.links.add(link); } /** * Retrieves the first HTML link. * * @return the link */ public Link getHtmlLink() { Link htmlLink = getLink(Link.Rel.ALTERNATE, Link.Type.HTML); return htmlLink; } /** * 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 { generateStartElement(w, Namespaces.atomNs, "source", null, null); // Generate inner content generateInnerAtom(w, extProfile); // Invoke ExtensionPoint. generateExtensions(w, extProfile); w.endElement(Namespaces.atomNs, "source"); } /** * Generates inner XML content in the Atom format. * * @param w * output writer * * @param extProfile * extension profile * * @throws IOException */ protected void generateInnerAtom(XmlWriter w, ExtensionProfile extProfile) throws IOException { if (srcState.id != null) { w.simpleElement(Namespaces.atomNs, "id", null, srcState.id); } if (srcState.updated != null) { w.simpleElement(Namespaces.atomNs, "updated", null, srcState.updated.toString()); } w.startRepeatingElement(); for (Category cat: srcState.categories) { cat.generateAtom(w); } w.endRepeatingElement(); if (srcState.title != null) { srcState.title.generateAtom(w, "title"); } if (srcState.subtitle != null) { srcState.subtitle.generateAtom(w, "subtitle"); } if (srcState.rights != null) { srcState.rights.generateAtom(w, "rights"); } if (srcState.icon != null) { w.simpleElement(Namespaces.atomNs, "icon", null, srcState.icon); } if (srcState.logo != null) { w.simpleElement(Namespaces.atomNs, "logo", null, srcState.logo); } w.startRepeatingElement(); for (Link link: srcState.links) { link.generateAtom(w, extProfile); } w.endRepeatingElement(); w.startRepeatingElement(); for (Person author: srcState.authors) { author.generateAtom(extProfile, w, "author"); } w.endRepeatingElement(); w.startRepeatingElement(); for (Person contributor: srcState.contributors) { contributor.generateAtom(extProfile, w, "contributor"); } w.endRepeatingElement(); if (srcState.generator != null) { srcState.generator.generateAtom(w); } } /** * Parses XML in the Atom format. * * @param extProfile * extension profile * * @param stream * XML input stream * * @throws IOException * * @throws ParseException */ public void parseAtom(ExtensionProfile extProfile, InputStream stream) throws IOException, ParseException { SourceHandler handler = new SourceHandler(extProfile); new XmlParser().parse(stream, handler, Namespaces.atom, "source"); } /** * 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 { SourceHandler handler = new SourceHandler(extProfile); new XmlParser().parse(reader, handler, Namespaces.atom, "source"); } /** {@code <atom:source>} parser. */ public class SourceHandler extends ExtensionPoint.ExtensionHandler { public SourceHandler(ExtensionProfile extProfile) { super(extProfile, Source.class); } protected SourceHandler(ExtensionProfile extProfile, Class<? extends ExtensionPoint> extClass) { super(extProfile, extClass); } @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("updated")) { return new UpdatedHandler(); } else if (localName.equals("category")) { Category cat = new Category(); Kind.Adaptable adaptable; if (Source.this instanceof Kind.Adaptable) { adaptable = (Kind.Adaptable)Source.this; } else { adaptable = null; } return cat.new AtomHandler(extProfile, srcState.categories, adaptable); } else if (localName.equals("title")) { TextConstruct.ChildHandlerInfo chi = TextConstruct.getChildHandler(attrs); if (srcState.title != null) { throw new ParseException( CoreErrorDomain.ERR.duplicateTitle); } srcState.title = chi.textConstruct; return chi.handler; } else if (localName.equals("subtitle")) { TextConstruct.ChildHandlerInfo chi = TextConstruct.getChildHandler(attrs); if (srcState.subtitle != null) { throw new ParseException( CoreErrorDomain.ERR.duplicateSubtitle); } srcState.subtitle = chi.textConstruct; return chi.handler; } else if (localName.equals("rights")) { TextConstruct.ChildHandlerInfo chi = TextConstruct.getChildHandler(attrs); if (srcState.rights != null) { throw new ParseException( CoreErrorDomain.ERR.duplicateRights); } srcState.rights = chi.textConstruct; return chi.handler; } else if (localName.equals("icon")) { return new IconHandler(); } else if (localName.equals("logo")) { return new LogoHandler(); } else if (localName.equals("link")) { Link link = new Link(); srcState.links.add(link); return link.new AtomHandler(extProfile); } else if (localName.equals("author")) { Person author = new Person(); srcState.authors.add(author); return author.new AtomHandler(extProfile); } else if (localName.equals("contributor")) { Person contributor = new Person(); srcState.contributors.add(contributor); return contributor.new AtomHandler(extProfile); } else if (localName.equals("generator")) { if (srcState.generator != null) { throw new ParseException( CoreErrorDomain.ERR.duplicateGenerator); } srcState.generator = new Generator(); return srcState.generator.new AtomHandler(); } } else { return super.getChildHandler(namespace, localName, attrs); } return null; } /** <atom:id> parser. */ private class IdHandler extends XmlParser.ElementHandler { @Override public void processEndElement() throws ParseException { if (srcState.id != null) { throw new ParseException( CoreErrorDomain.ERR.duplicateFeedId); } if (value == null) { throw new ParseException( CoreErrorDomain.ERR.idValueRequired); } srcState.id = value; } } /** <atom:updated> parser. */ class UpdatedHandler extends Rfc3339Handler { @Override public void processEndElement() throws ParseException { super.processEndElement(); srcState.updated = getDateTime(); } } /** <atom:icon> parser. */ private class IconHandler extends XmlParser.ElementHandler { @Override public void processEndElement() throws ParseException { if (srcState.icon != null) { throw new ParseException( CoreErrorDomain.ERR.duplicateIcon); } if (value == null) { throw new ParseException( CoreErrorDomain.ERR.iconValueRequired); } srcState.icon = value; } } /** <atom:logo> parser. */ private class LogoHandler extends XmlParser.ElementHandler { @Override public void processEndElement() throws ParseException { if (srcState.logo != null) { throw new ParseException( CoreErrorDomain.ERR.duplicateLogo); } if (value == null) { throw new ParseException( CoreErrorDomain.ERR.logoValueRequired); } srcState.logo = value; } } } }