/* 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.client.CoreErrorDomain; import com.google.gdata.util.ParseException; import org.xml.sax.Attributes; import java.math.BigDecimal; import java.math.BigInteger; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * Helps accessing tag attributes. * * The helper only checks attributes in the default namespace ("") and * rejects unknown attributes. * * The idea is to remove (consume) attributes as they are read * from the list and at the end make sure that all attributes have * been read, to detect whether unknown attributes have been * specified. This is done by the method {@link #assertAllConsumed()} usually * called from * {@link com.google.gdata.util.XmlParser.ElementHandler#processEndElement()}. * * */ public class AttributeHelper { /** Maps attribute local name to string value. */ protected final Map<String, String> attrs = new HashMap<String, String>(); /** set of attributes that are duplicated */ private Set<String> dups = new HashSet<String>(); /** if the content has been consumed */ private boolean contentConsumed = false; /** element's text content or {@code null} for no text content */ private String content = null; /** * Creates a helper tied to a specific set of SAX attributes. * * @param attrs the SAX attributes to be processed */ public AttributeHelper(Attributes attrs) { // attributes for (int i = 0; i < attrs.getLength(); i++) { if (attrs.getURI(i).length() != 0) { String attrLocalName = attrs.getLocalName(i); if (this.attrs.put(attrLocalName, attrs.getValue(i)) != null) { dups.add(attrLocalName); } } else { this.attrs.put(attrs.getQName(i), attrs.getValue(i)); } } } /** * Gets the element's text content and removes it from the list. * * @param required indicates attributes is required * @return element's text content or {@code null} for no text content * @exception ParseException if required is set and the text content * is not defined */ public String consumeContent(boolean required) throws ParseException { return consume(null, required); } /** * Gets the value of an attribute and remove it from the list. * * @param name attribute name or <code>null</code> for text content * @param required indicates attributes is required * @return attribute value or null if not available * @exception ParseException if required is set and the attribute * is not defined */ public String consume(String name, boolean required) throws ParseException { if (name == null) { if (content == null && required) { throw new ParseException( CoreErrorDomain.ERR.missingRequiredContent); } contentConsumed = true; return content; } String value = attrs.get(name); if (value == null) { if (required) { ParseException pe = new ParseException( CoreErrorDomain.ERR.missingAttribute); pe.setInternalReason("Missing attribute: '" + name + "'"); throw pe; } return null; } attrs.remove(name); return value; } /** * Gets the value of a byte attribute and remove it from the list. * * @param name attribute name or <code>null</code> for text content * @param required indicates attribute is required * @param defaultValue the default value for an optional attribute (used if * not present) * @return the byte value of this attribute * @exception ParseException if required is set and the attribute * is not defined, or if the attribute value is not a valid byte */ public byte consumeByte(String name, boolean required, byte defaultValue) throws ParseException { String value = consume(name, required); if (value == null) { return defaultValue; } try { return Byte.parseByte(value); } catch (NumberFormatException e) { ParseException pe = new ParseException( CoreErrorDomain.ERR.invalidByteAttribute); pe.setInternalReason("Invalid byte value for attribute: '" + name + "'"); throw pe; } } /** * Gets the value of a byte attribute and remove it from the list. * * @param name attribute name * @param required indicates attribute is required * @return the byte value of this attribute, 0 by default * @exception ParseException if required is set and the attribute is not * defined, or if the attribute value is not a valid byte */ public byte consumeByte(String name, boolean required) throws ParseException { return consumeByte(name, required, (byte) 0); } /** * Gets the value of a short attribute and remove it from the list. * * @param name attribute name or <code>null</code> for text content * @param required indicates attribute is required * @param defaultValue the default value for an optional attribute (used if * not present) * @return the short value of this attribute * @exception ParseException if required is set and the attribute * is not defined, or if the attribute value is not a valid short */ public short consumeShort(String name, boolean required, short defaultValue) throws ParseException { String value = consume(name, required); if (value == null) { return defaultValue; } try { return Short.parseShort(value); } catch (NumberFormatException e) { ParseException pe = new ParseException( CoreErrorDomain.ERR.invalidShortAttribute); pe.setInternalReason("Invalid short value for attribute: '" + name + "'"); throw pe; } } /** * Gets the value of a short attribute and remove it from the list. * * @param name attribute name * @param required indicates attribute is required * @return the short value of this attribute, 0 by default * @exception ParseException if required is set and the attribute * is not defined, or if the attribute value is not a valid short */ public short consumeShort(String name, boolean required) throws ParseException { return consumeShort(name, required, (short) 0); } /** * Gets the value of an integer attribute and remove it from the list. * * @param name attribute name or <code>null</code> for text content * @param required indicates attribute is required * @param defaultValue the default value for an optional attribute (used * if not present) * @return the integer value of this attribute * @exception ParseException if required is set and the attribute * is not defined, or if the attribute value is not a valid integer */ public int consumeInteger(String name, boolean required, int defaultValue) throws ParseException { String value = consume(name, required); if (value == null) { return defaultValue; } try { return Integer.parseInt(value); } catch (NumberFormatException e) { ParseException pe = new ParseException( CoreErrorDomain.ERR.invalidIntegerAttribute); pe.setInternalReason("Invalid integer value for attribute: '" + name + "'"); throw pe; } } /** * Gets the value of an integer attribute and remove it from the list. * * @param name attribute name * @param required indicates attribute is required * @return the integer value of this attribute, 0 by default * @exception ParseException if required is set and the attribute * is not defined, or if the attribute value is not a valid integer */ public int consumeInteger(String name, boolean required) throws ParseException { return consumeInteger(name, required, 0); } /** * Gets the value of a long attribute and remove it from the list. * * @param name attribute name or <code>null</code> for text content * @param required indicates attribute is required * @param defaultValue the default value for an optional attribute (used * if not present) * @return the long value of this attribute * @exception ParseException if required is set and the attribute * is not defined, or if the attribute value is not a valid long */ public long consumeLong(String name, boolean required, long defaultValue) throws ParseException { String value = consume(name, required); if (value == null) { return defaultValue; } try { return Long.parseLong(value); } catch (NumberFormatException e) { ParseException pe = new ParseException( CoreErrorDomain.ERR.invalidLongAttribute, e); pe.setInternalReason("Invalid long value for attribute: '" + name + "'"); throw pe; } } /** * Gets the value of a long attribute and remove it from the list. * * @param name attribute name * @param required indicates attribute is required * @return the long value of this attribute, 0 by default * @exception ParseException if required is set and the attribute * is not defined, or if the attribute value is not a valid long */ public long consumeLong(String name, boolean required) throws ParseException { return consumeLong(name, required, 0); } /** * Gets the value of a big integer attribute and remove it from the list. * * @param name attribute name or <code>null</code> for text content * @param required indicates attribute is required * @param defaultValue the default value for an optional attribute (used if * not present) * @return the big integer value of this attribute * @exception ParseException if required is set and the attribute * is not defined, or if the attribute value is not a valid big integer */ public BigInteger consumeBigInteger(String name, boolean required, BigInteger defaultValue) throws ParseException { String value = consume(name, required); if (value == null) { return defaultValue; } try { return new BigInteger(value); } catch (NumberFormatException e) { ParseException pe = new ParseException( CoreErrorDomain.ERR.invalidBigIntegerAttribute); pe.setInternalReason("Invalid big integer value for attribute: '" + name + "'"); throw pe; } } /** * Gets the value of a big integer attribute and remove it from the list. * * @param name attribute name * @param required indicates attribute is required * @return the big integer value of this attribute, 0 by default * @exception ParseException if required is set and the attribute * is not defined, or if the attribute value is not a valid big integer */ public BigInteger consumeBigInteger(String name, boolean required) throws ParseException { return consumeBigInteger(name, required, BigInteger.ZERO); } /** * Gets the value of a big decimal attribute and remove it from the list. * * @param name attribute name or <code>null</code> for text content * @param required indicates attribute is required * @param defaultValue the default value for an optional attribute (used if * not present) * @return the big decimal value of this attribute * @exception ParseException if required is set and the attribute * is not defined, or if the attribute value is not a valid big decimal */ public BigDecimal consumeBigDecimal(String name, boolean required, BigDecimal defaultValue) throws ParseException { String value = consume(name, required); if (value == null) { return defaultValue; } try { return new BigDecimal(value); } catch (NumberFormatException e) { ParseException pe = new ParseException( CoreErrorDomain.ERR.invalidBigDecimalAttribute); pe.setInternalReason("Invalid big decimal value for attribute: '" + name + "'"); throw pe; } } /** * Gets the value of a big decimal attribute and remove it from the list. * * @param name attribute name * @param required indicates attribute is required * @return the big decimal value of this attribute, 0 by default * @exception ParseException if required is set and the attribute * is not defined, or if the attribute value is not a valid big decimal */ public BigDecimal consumeBigDecimal(String name, boolean required) throws ParseException { return consumeBigDecimal(name, required, BigDecimal.ZERO); } /** * Gets the value of a double attribute and remove it from the list. * * @param name attribute name or <code>null</code> for text content * @param required indicates attribute is required * @param defaultValue the default value for an optional attribute (used if * not present) * @return the double value of this attribute * @throws ParseException if required is set and the attribute is not defined, * or if the attribute value is not a valid double */ public double consumeDouble( String name, boolean required, double defaultValue) throws ParseException { String value = consume(name, required); if (value == null) { return defaultValue; } if ("INF".equals(value)) { return Double.POSITIVE_INFINITY; } else if ("-INF".equals(value)) { return Double.NEGATIVE_INFINITY; } try { return Double.parseDouble(value); } catch (NumberFormatException e) { ParseException pe = new ParseException( CoreErrorDomain.ERR.invalidDoubleAttribute, e); pe.setInternalReason("Invalid double value for attribute: '" + name + "'"); throw pe; } } /** * Gets the value of a double attribute and remove it from the list. * * @param name attribute name * @param required indicates attribute is required * @return the double value of this attribute, 0 by default * @throws ParseException if required is set and the attribute is not defined, * or if the attribute value is not a valid double */ public double consumeDouble(String name, boolean required) throws ParseException { return consumeDouble(name, required, 0); } /** * Gets the value of a float attribute and remove it from the list. * * @param name attribute name or <code>null</code> for text content * @param required indicates attribute is required * @param defaultValue the default value for an optional attribute (used if * not present) * @return the float value of this attribute * @throws ParseException if required is set and the attribute is not defined, * or if the attribute value is not a valid float */ public float consumeFloat( String name, boolean required, float defaultValue) throws ParseException { String value = consume(name, required); if (value == null) { return defaultValue; } if ("INF".equals(value)) { return Float.POSITIVE_INFINITY; } else if ("-INF".equals(value)) { return Float.NEGATIVE_INFINITY; } try { return Float.parseFloat(value); } catch (NumberFormatException e) { ParseException pe = new ParseException( CoreErrorDomain.ERR.invalidFloatAttribute, e); pe.setInternalReason("Invalid float value for attribute: '" + name + "'"); throw pe; } } /** * Gets the value of a float attribute and remove it from the list. * * @param name attribute name * @param required indicates attribute is required * @return the float value of this attribute, 0 by default * @throws ParseException if required is set and the attribute is not defined, * or if the attribute value is not a valid float */ public float consumeFloat(String name, boolean required) throws ParseException { return consumeFloat(name, required, 0); } /** * Gets the value of a boolean attribute and remove it from the list. The * accepted values are based upon xsd:boolean syntax (true, false, 1, 0). * * @param name attribute name or <code>null</code> for text content * @param required indicates attribute is required * @param defaultValue the default value for an optional attribute (used * if not present) * @return the boolean value of this attribute * @exception ParseException if required is set and the attribute * is not defined, or if the attribute value is neither {@code true} * nor {@code false}. */ public boolean consumeBoolean(String name, boolean required, boolean defaultValue) throws ParseException { String value = consume(name, required); if (value == null) { return defaultValue; } if ("true".equals(value) || "1".equals(value)) { return true; } else if ("false".equals(value) || "0".equals(value)) { return false; } else { ParseException pe = new ParseException( CoreErrorDomain.ERR.invalidBooleanAttribute); pe.setInternalReason("Invalid boolean value for attribute: '" + name + "'"); throw pe; } } /** * Gets the value of a boolean attribute and remove it from the list. The * accepted values are based upon xsd:boolean syntax (true, false, 1, 0). * * @param name attribute name * @param required indicates attribute is required * @return the boolean value of this attribute, false by default * @exception ParseException if required is set and the attribute * is not defined, or if the attribute value is neither {@code true} * nor {@code false}. */ public boolean consumeBoolean(String name, boolean required) throws ParseException { return consumeBoolean(name, required, false); } /** * Gets the value of a {@link DateTime} attribute and remove it from the list. * * @param name attribute name or <code>null</code> for text content * @param required indicates attribute is required * @return the date-time value of this attribute, {@code null} by default * @exception ParseException if required is set and the attribute * is not defined, or if the date-time attribute cannot be parsed */ public DateTime consumeDateTime(String name, boolean required) throws ParseException { String value = consume(name, required); if (value == null) { return null; } try { return DateTime.parseDateTimeChoice(value); } catch (NumberFormatException e) { ParseException pe = new ParseException( CoreErrorDomain.ERR.invalidDatetime, e); pe.setInternalReason("Badly formatted datetime in attribute: " + name); throw pe; } } /** * Defines a custom mapping of an enum value to an attribute value (similar to * a closure). */ public static interface EnumToAttributeValue<T extends Enum<T>> { String getAttributeValue(T enumValue); } /** * Implements the most common custom mapping of an enum value to an attribute * value using the lower-case form of the enum name. */ public static class LowerCaseEnumToAttributeValue<T extends Enum<T>> implements EnumToAttributeValue<T> { public String getAttributeValue(T enumValue) { return enumValue.name().toLowerCase(); } } /** * Gets the value of an enumerated attribute and remove it from the list, * using a custom mapping of enum to attribute value. * * @param name attribute name or <code>null</code> for text content * @param required indicates attribute is required * @param enumClass enumeration class * @param defaultValue the default value for an optional attribute * (used if not present) * @param enumToAttributeValue custom mapping of enum to attribute value * @return an enumerated value * @throws ParseException if required is set and the attribute is not defined, * or if the attribute value is not a valid enumerated * value */ public <T extends Enum<T>> T consumeEnum(String name, boolean required, Class<T> enumClass, T defaultValue, EnumToAttributeValue<T> enumToAttributeValue) throws ParseException { String value = consume(name, required); if (value == null) { return defaultValue; } for (T enumValue : enumClass.getEnumConstants()) { if (enumToAttributeValue.getAttributeValue(enumValue).equals(value)) { return enumValue; } } ParseException pe = new ParseException( CoreErrorDomain.ERR.invalidAttributeValue); pe.setInternalReason("Invalid value for attribute : '" + name + "'"); throw pe; } /** * Gets the value of an enumerated attribute and remove it from the list. * * Enumerated values are case-insensitive. * * @param name attribute name or <code>null</code> for text content * @param required indicates attribute is required * @param enumClass enumeration class * @param defaultValue the default value for an optional attribute (used * if not present) * @return an enumerated value * @exception ParseException if required is set and the attribute * is not defined, or if the attribute value is not a valid * enumerated value */ public <T extends Enum<T>> T consumeEnum(String name, boolean required, Class<T> enumClass, T defaultValue) throws ParseException { String value = consume(name, required); if (value == null) { return defaultValue; } try { return Enum.valueOf(enumClass, value.toUpperCase()); } catch (IllegalArgumentException e) { ParseException pe = new ParseException( CoreErrorDomain.ERR.invalidAttributeValue, e); pe.setInternalReason("Invalid value for attribute : '" + name + "'"); throw pe; } } /** * Gets the value of an enumerated attribute and remove it from the list. * * Enumerated values are case-insensitive. * * @param name attribute name * @param required indicates attribute is required * @param enumClass enumeration class * @return an enumerated value or null if not present * @exception ParseException if required is set and the attribute * is not defined, or if the attribute value is not a valid * enumerated value */ public <T extends Enum<T>> T consumeEnum(String name, boolean required, Class<T> enumClass) throws ParseException { return consumeEnum(name, required, enumClass, null); } /** * Makes sure all attributes have been removed from the list. * * To all attribute in the default namespace must correspond exactly * one call to consume*(). * * @exception ParseException if an attribute in the default namespace * hasn't been removed */ public void assertAllConsumed() throws ParseException { StringBuffer message = new StringBuffer(); if (!attrs.isEmpty()) { message.append("Unknown attribute"); if (attrs.size() > 1) { message.append('s'); } message.append(':'); for (String name : attrs.keySet()) { message.append(" '"); message.append(name); message.append("' "); } } if (!dups.isEmpty()) { message.append("Duplicate attribute"); if (dups.size() > 1) { message.append('s'); } message.append(':'); for (String dup : dups) { message.append(" '"); message.append(dup); message.append("' "); } } if (!contentConsumed && content != null && content.length() != 0) { message.append("Unexpected text content "); } if (message.length() != 0) { throw new ParseException(message.toString()); } } /** * Sets the content. * * @param content element's text content */ void setContent(String content) { // text content this.content = content == null ? null : content.trim(); } }