/* 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.util.ArrayList; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Category type. * <p> * For the purposes of comparison, two Category instances are considered * to be identical if they have matching schemes and terms. The label * attributes <em>are not</em> used for the purpose of testing equality. * * */ public class Category implements ICategory { /** * The character used to prefix any (optional) scheme in the compound * scheme+term Category format. */ public static final char SCHEME_PREFIX = '{'; /** * The character used to suffix any (optional) scheme in the compound * scheme+term Category format. */ public static final char SCHEME_SUFFIX = '}'; public Category() {} // A simple pattern matcher for the "{scheme}term" syntax private static final Pattern categoryPattern = Pattern.compile("(\\{([^\\}]+)\\})?(.+)"); /** * Constructs a new category from a Category string. The format of * the String is the same as the one used to represent a category in * a GData query: an optional scheme surrounded by braces, followed * by a term. * * @param category the category string */ public Category(String category) { Matcher m = categoryPattern.matcher(category); if (!m.matches()) { throw new IllegalArgumentException("Invalid category: " + category); } if (m.group(2) != null) { scheme = m.group(2); } term = m.group(3); } /** * Constructs a new category. */ public Category(String scheme, String term) { this(scheme, term, null); } /** * Constructs a new category. */ public Category(String scheme, String term, String label) { this.scheme = scheme; if (term == null) { throw new NullPointerException("Invalid term. Cannot be null"); } this.term = term; this.label = label; } /** Scheme (domain). */ protected String scheme; public String getScheme() { return scheme; } public void setScheme(String v) { scheme = v; } /** Term. */ protected String term; public String getTerm() { return term; } public void setTerm(String v) { term = v; } /** Human-readable label. */ protected String label; public String getLabel() { return label; } public void setLabel(String v) { label = v; } /** Language. */ protected String labelLang; public String getLabelLang() { return labelLang; } public void setLabelLang(String v) { labelLang = v; } @Override public String toString() { StringBuilder sb = new StringBuilder(); if (scheme != null) { sb.append(SCHEME_PREFIX); sb.append(scheme); sb.append(SCHEME_SUFFIX); } // Label syntax is not in the query model, so no need to define // public constants for the delimiters. sb.append(term); if (label != null) { sb.append("("); sb.append(label); sb.append(")"); } return sb.toString(); } // identical scheme/term values for all user-defined labels. The label // attribute is being used for the user label. This seems somewhat counter // to Atom semantics, where the scheme is a namespace and the term // identifies membership in the categories in that scheme. Needs review, // but until this is done the label must be taken into account for // equals()/hashCode() @Override public boolean equals(Object obj) { if (! (obj instanceof Category)) { return false; } return toString().equals(obj.toString()); } @Override public int hashCode() { int result = 17; result = 37 * result + ((scheme != null) ? scheme.hashCode() : 0); result = 37 * result + term.hashCode(); result = 37 * result + ((label != null) ? label.hashCode() : 0); return result; } /** * Generates XML in the Atom format. * * @param w * Output writer. * * @throws IOException */ public void generateAtom(XmlWriter w) throws IOException { ArrayList<XmlWriter.Attribute> attrs = new ArrayList<XmlWriter.Attribute>(3); if (scheme != null) { attrs.add(new XmlWriter.Attribute("scheme", scheme)); } if (term != null) { attrs.add(new XmlWriter.Attribute("term", term)); } if (label != null) { attrs.add(new XmlWriter.Attribute("label", label)); } if (labelLang != null) { attrs.add(new XmlWriter.Attribute("xml:lang", labelLang)); } w.simpleElement(Namespaces.atomNs, "category", attrs, null); } /** * Generates XML in the RSS format. * * @param w * Output writer. * * @throws IOException */ public void generateRss(XmlWriter w) throws IOException { ArrayList<XmlWriter.Attribute> attrs = new ArrayList<XmlWriter.Attribute>(3); if (scheme != null) { attrs.add(new XmlWriter.Attribute("domain", scheme)); } if (labelLang != null) { attrs.add(new XmlWriter.Attribute("xml:lang", labelLang)); } String value = term; if (term == null) { value = label; } w.simpleElement(Namespaces.rssNs, "category", attrs, value); } /** {@code <atom:category>} parser. */ public class AtomHandler extends XmlParser.ElementHandler { Set<Category> categorySet; ExtensionProfile extProfile; Kind.Adaptable adaptable; public AtomHandler() {} /** Constructor used when parsing a Category for a source or entry */ public AtomHandler(ExtensionProfile extProfile, Set<Category> categorySet, Kind.Adaptable adaptable) { this.extProfile = extProfile; this.categorySet = categorySet; this.adaptable = adaptable; } @Override public void processAttribute(String namespace, String localName, String value) { if (namespace.equals("") && localName.equals("scheme")) { scheme = value; } else if (namespace.equals("") && localName.equals("term")) { term = value; } else if (namespace.equals("") && localName.equals("label")) { label = value; } } @Override public XmlParser.ElementHandler getChildHandler(String namespace, String localName, Attributes attrs) { // Allow undefined extensions. return null; } @Override public void processEndElement() throws ParseException { if (term == null) { throw new ParseException( CoreErrorDomain.ERR.missingTermAttribute); } labelLang = xmlLang; // Allow undefined content. // Because categories are stored in a hashed collection, they can't // be stored until fully initialized. if (categorySet != null) { categorySet.add(Category.this); } // If parsing an Adaptable type and the parsing extension profile // supports autoextension and a Kind category is being parsed, then do // kind-based autoextension. if (adaptable != null && extProfile.isAutoExtending() && Kind.isKindCategory(Category.this)) { try { Kind.Adaptor adaptor = Kind.getAdaptor(term, adaptable); if (adaptor != null) { extProfile.addDeclarations(adaptor); } } catch (Kind.AdaptorException ae) { throw new ParseException( CoreErrorDomain.ERR.cantLoadKindAdaptor, ae); } } } } }