src/share/classes/com/sun/jndi/ldap/LdapName.java

Print this page




  61  * meaning would be ambiguous.
  62  *<p>
  63  * <code>LdapName</code> will properly parse all valid names, but
  64  * does not attempt to detect all possible violations when parsing
  65  * invalid names.  It's "generous".
  66  *<p>
  67  * When names are tested for equality, attribute types and binary
  68  * values are case-insensitive, and string values are by default
  69  * case-insensitive.
  70  * String values with different but equivalent usage of quoting,
  71  * escaping, or UTF8-hex-encoding are considered equal.  The order of
  72  * components in multi-valued RDNs (such as "ou=Sales+cn=Bob") is not
  73  * significant.
  74  *
  75  * @author Scott Seligman
  76  */
  77 
  78 public final class LdapName implements Name {
  79 
  80     private transient String unparsed;  // if non-null, the DN in unparsed form
  81     private transient Vector rdns;      // parsed name components
  82     private transient boolean valuesCaseSensitive = false;
  83 
  84     /**
  85      * Constructs an LDAP name from the given DN.
  86      *
  87      * @param name      An LDAP DN.  To JNDI, a compound name.
  88      *
  89      * @throws InvalidNameException if a syntax violation is detected.
  90      */
  91     public LdapName(String name) throws InvalidNameException {
  92         unparsed = name;
  93         parse();
  94     }
  95 
  96     /*
  97      * Constructs an LDAP name given its parsed components and, optionally
  98      * (if "name" is not null), the unparsed DN.
  99      */
 100     private LdapName(String name, Vector rdns) {

 101         unparsed = name;
 102         this.rdns = (Vector)rdns.clone();
 103     }
 104 
 105     /*
 106      * Constructs an LDAP name given its parsed components (the elements
 107      * of "rdns" in the range [beg,end)) and, optionally
 108      * (if "name" is not null), the unparsed DN.
 109      */
 110     private LdapName(String name, Vector rdns, int beg, int end) {
 111         unparsed = name;
 112         this.rdns = new Vector();
 113         for (int i = beg; i < end; i++) {
 114             this.rdns.addElement(rdns.elementAt(i));
 115         }
 116     }
 117 
 118 
 119     public Object clone() {
 120         return new LdapName(unparsed, rdns);
 121     }
 122 
 123     public String toString() {
 124         if (unparsed != null) {
 125             return unparsed;
 126         }
 127 
 128         StringBuffer buf = new StringBuffer();
 129         for (int i = rdns.size() - 1; i >= 0; i--) {
 130             if (i < rdns.size() - 1) {
 131                 buf.append(',');
 132             }
 133             Rdn rdn = (Rdn)rdns.elementAt(i);
 134             buf.append(rdn);
 135         }
 136 
 137         unparsed = new String(buf);
 138         return unparsed;
 139     }
 140 
 141     public boolean equals(Object obj) {
 142         return ((obj instanceof LdapName) &&
 143                 (compareTo(obj) == 0));
 144     }
 145 
 146     public int compareTo(Object obj) {
 147         LdapName that = (LdapName)obj;
 148 
 149         if ((obj == this) ||                    // check possible shortcuts
 150             (unparsed != null && unparsed.equals(that.unparsed))) {
 151             return 0;
 152         }
 153 
 154         // Compare RDNs one by one, lexicographically.
 155         int minSize = Math.min(rdns.size(), that.rdns.size());
 156         for (int i = 0 ; i < minSize; i++) {
 157             // Compare a single pair of RDNs.
 158             Rdn rdn1 = (Rdn)rdns.elementAt(i);
 159             Rdn rdn2 = (Rdn)that.rdns.elementAt(i);
 160 
 161             int diff = rdn1.compareTo(rdn2);
 162             if (diff != 0) {
 163                 return diff;
 164             }
 165         }
 166         return (rdns.size() - that.rdns.size());        // longer DN wins
 167     }
 168 
 169     public int hashCode() {
 170         // Sum up the hash codes of the components.
 171         int hash = 0;
 172 
 173         // For each RDN...
 174         for (int i = 0; i < rdns.size(); i++) {
 175             Rdn rdn = (Rdn)rdns.elementAt(i);
 176             hash += rdn.hashCode();
 177         }
 178         return hash;
 179     }
 180 
 181     public int size() {
 182         return rdns.size();
 183     }
 184 
 185     public boolean isEmpty() {
 186         return rdns.isEmpty();
 187     }
 188 
 189     public Enumeration getAll() {
 190         final Enumeration enum_ = rdns.elements();
 191 
 192         return new Enumeration () {
 193             public boolean hasMoreElements() {
 194                 return enum_.hasMoreElements();
 195             }
 196             public Object nextElement() {
 197                 return enum_.nextElement().toString();
 198             }
 199         };
 200     }
 201 
 202     public String get(int pos) {
 203         return rdns.elementAt(pos).toString();
 204     }
 205 
 206     public Name getPrefix(int pos) {
 207         return new LdapName(null, rdns, 0, pos);
 208     }
 209 
 210     public Name getSuffix(int pos) {
 211         return new LdapName(null, rdns, pos, rdns.size());
 212     }
 213 
 214     public boolean startsWith(Name n) {
 215         int len1 = rdns.size();
 216         int len2 = n.size();


 237              parse();
 238          } catch (InvalidNameException e) {
 239              // shouldn't happen
 240              throw new IllegalStateException("Cannot parse name: " + unparsed);
 241          }
 242          valuesCaseSensitive = caseSensitive;
 243      }
 244 
 245     /*
 246      * Helper method for startsWith() and endsWith().
 247      * Returns true if components [beg,end) match the components of "n".
 248      * If "n" is not an LdapName, each of its components is parsed as
 249      * the string form of an RDN.
 250      * The following must hold:  end - beg == n.size().
 251      */
 252     private boolean matches(int beg, int end, Name n) {
 253         for (int i = beg; i < end; i++) {
 254             Rdn rdn;
 255             if (n instanceof LdapName) {
 256                 LdapName ln = (LdapName)n;
 257                 rdn = (Rdn)ln.rdns.elementAt(i - beg);
 258             } else {
 259                 String rdnString = n.get(i - beg);
 260                 try {
 261                     rdn = (new DnParser(rdnString, valuesCaseSensitive)).getRdn();
 262                 } catch (InvalidNameException e) {
 263                     return false;
 264                 }
 265             }
 266 
 267             if (!rdn.equals(rdns.elementAt(i))) {
 268                 return false;
 269             }
 270         }
 271         return true;
 272     }
 273 
 274     public Name addAll(Name suffix) throws InvalidNameException {
 275         return addAll(size(), suffix);
 276     }
 277 
 278     /*
 279      * If "suffix" is not an LdapName, each of its components is parsed as
 280      * the string form of an RDN.
 281      */
 282     public Name addAll(int pos, Name suffix) throws InvalidNameException {
 283         if (suffix instanceof LdapName) {
 284             LdapName s = (LdapName)suffix;
 285             for (int i = 0; i < s.rdns.size(); i++) {
 286                 rdns.insertElementAt(s.rdns.elementAt(i), pos++);
 287             }
 288         } else {
 289             Enumeration comps = suffix.getAll();
 290             while (comps.hasMoreElements()) {
 291                 DnParser p = new DnParser((String)comps.nextElement(),
 292                     valuesCaseSensitive);
 293                 rdns.insertElementAt(p.getRdn(), pos++);
 294             }
 295         }
 296         unparsed = null;                                // no longer valid
 297         return this;
 298     }
 299 
 300     public Name add(String comp) throws InvalidNameException {
 301         return add(size(), comp);
 302     }
 303 
 304     public Name add(int pos, String comp) throws InvalidNameException {
 305         Rdn rdn = (new DnParser(comp, valuesCaseSensitive)).getRdn();
 306         rdns.insertElementAt(rdn, pos);
 307         unparsed = null;                                // no longer valid
 308         return this;
 309     }
 310 
 311     public Object remove(int pos) throws InvalidNameException {


 389         private final String name;      // DN being parsed
 390         private final char[] chars;     // characters in LDAP name being parsed
 391         private final int len;          // length of "chars"
 392         private int cur = 0;            // index of first unconsumed char in "chars"
 393         private boolean valuesCaseSensitive;
 394 
 395         /*
 396          * Given an LDAP DN in string form, returns a parser for it.
 397          */
 398         DnParser(String name, boolean valuesCaseSensitive)
 399             throws InvalidNameException {
 400             this.name = name;
 401             len = name.length();
 402             chars = name.toCharArray();
 403             this.valuesCaseSensitive = valuesCaseSensitive;
 404         }
 405 
 406         /*
 407          * Parses the DN, returning a Vector of its RDNs.
 408          */
 409         Vector getDn() throws InvalidNameException {
 410             cur = 0;
 411             Vector rdns = new Vector(len / 3 + 10);  // leave room for growth
 412 
 413             if (len == 0) {
 414                 return rdns;
 415             }
 416 
 417             rdns.addElement(parseRdn());
 418             while (cur < len) {
 419                 if (chars[cur] == ',' || chars[cur] == ';') {
 420                     ++cur;
 421                     rdns.insertElementAt(parseRdn(), 0);
 422                 } else {
 423                     throw new InvalidNameException("Invalid name: " + name);
 424                 }
 425             }
 426             return rdns;
 427         }
 428 
 429         /*
 430          * Parses the DN, if it is known to contain a single RDN.
 431          */


 578          * a string attribute value.
 579          */
 580         private boolean atTerminator() {
 581             return (cur < len &&
 582                     (chars[cur] == ',' ||
 583                      chars[cur] == ';' ||
 584                      chars[cur] == '+'));
 585         }
 586     }
 587 
 588 
 589     /*
 590      * Class Rdn represents a set of TypeAndValue.
 591      */
 592     static class Rdn {
 593 
 594         /*
 595          * A vector of the TypeAndValue elements of this Rdn.
 596          * It is sorted to facilitate set operations.
 597          */
 598         private final Vector tvs = new Vector();
 599 
 600         void add(TypeAndValue tv) {
 601 
 602             // Set i to index of first element greater than tv, or to
 603             // tvs.size() if there is none.
 604             int i;
 605             for (i = 0; i < tvs.size(); i++) {
 606                 int diff = tv.compareTo(tvs.elementAt(i));
 607                 if (diff == 0) {
 608                     return;             // tv is a duplicate:  ignore it
 609                 } else if (diff < 0) {
 610                     break;
 611                 }
 612             }
 613 
 614             tvs.insertElementAt(tv, i);
 615         }
 616 
 617         public String toString() {
 618             StringBuffer buf = new StringBuffer();
 619             for (int i = 0; i < tvs.size(); i++) {
 620                 if (i > 0) {
 621                     buf.append('+');
 622                 }
 623                 buf.append(tvs.elementAt(i));
 624             }
 625             return new String(buf);
 626         }
 627 
 628         public boolean equals(Object obj) {
 629             return ((obj instanceof Rdn) &&
 630                     (compareTo(obj) == 0));
 631         }
 632 
 633         // Compare TypeAndValue components one by one, lexicographically.
 634         public int compareTo(Object obj) {
 635             Rdn that = (Rdn)obj;
 636             int minSize = Math.min(tvs.size(), that.tvs.size());
 637             for (int i = 0; i < minSize; i++) {
 638                 // Compare a single pair of type/value pairs.
 639                 TypeAndValue tv = (TypeAndValue)tvs.elementAt(i);
 640                 int diff = tv.compareTo(that.tvs.elementAt(i));
 641                 if (diff != 0) {
 642                     return diff;
 643                 }
 644             }
 645             return (tvs.size() - that.tvs.size());      // longer RDN wins
 646         }
 647 
 648         public int hashCode() {
 649             // Sum up the hash codes of the components.
 650             int hash = 0;
 651 
 652             // For each type/value pair...
 653             for (int i = 0; i < tvs.size(); i++) {
 654                 hash += tvs.elementAt(i).hashCode();
 655             }
 656             return hash;
 657         }
 658 
 659         Attributes toAttributes() {
 660             Attributes attrs = new BasicAttributes(true);
 661             TypeAndValue tv;
 662             Attribute attr;
 663 
 664             for (int i = 0; i < tvs.size(); i++) {
 665                 tv = (TypeAndValue) tvs.elementAt(i);
 666                 if ((attr = attrs.get(tv.getType())) == null) {
 667                     attrs.put(tv.getType(), tv.getUnescapedValue());
 668                 } else {
 669                     attr.add(tv.getUnescapedValue());
 670                 }
 671             }
 672             return attrs;
 673         }
 674     }
 675 
 676 
 677     /*
 678      * Class TypeAndValue represents an attribute type and its
 679      * corresponding value.
 680      */
 681     static class TypeAndValue {
 682 
 683         private final String type;
 684         private final String value;             // value, escaped or quoted
 685         private final boolean binary;




  61  * meaning would be ambiguous.
  62  *<p>
  63  * <code>LdapName</code> will properly parse all valid names, but
  64  * does not attempt to detect all possible violations when parsing
  65  * invalid names.  It's "generous".
  66  *<p>
  67  * When names are tested for equality, attribute types and binary
  68  * values are case-insensitive, and string values are by default
  69  * case-insensitive.
  70  * String values with different but equivalent usage of quoting,
  71  * escaping, or UTF8-hex-encoding are considered equal.  The order of
  72  * components in multi-valued RDNs (such as "ou=Sales+cn=Bob") is not
  73  * significant.
  74  *
  75  * @author Scott Seligman
  76  */
  77 
  78 public final class LdapName implements Name {
  79 
  80     private transient String unparsed;  // if non-null, the DN in unparsed form
  81     private transient Vector<Rdn> rdns;      // parsed name components
  82     private transient boolean valuesCaseSensitive = false;
  83 
  84     /**
  85      * Constructs an LDAP name from the given DN.
  86      *
  87      * @param name      An LDAP DN.  To JNDI, a compound name.
  88      *
  89      * @throws InvalidNameException if a syntax violation is detected.
  90      */
  91     public LdapName(String name) throws InvalidNameException {
  92         unparsed = name;
  93         parse();
  94     }
  95 
  96     /*
  97      * Constructs an LDAP name given its parsed components and, optionally
  98      * (if "name" is not null), the unparsed DN.
  99      */
 100     @SuppressWarnings("unchecked")
 101     private LdapName(String name, Vector<Rdn> rdns) {
 102         unparsed = name;
 103         this.rdns = (Vector<Rdn>)rdns.clone();
 104     }
 105 
 106     /*
 107      * Constructs an LDAP name given its parsed components (the elements
 108      * of "rdns" in the range [beg,end)) and, optionally
 109      * (if "name" is not null), the unparsed DN.
 110      */
 111     private LdapName(String name, Vector<Rdn> rdns, int beg, int end) {
 112         unparsed = name;
 113         this.rdns = new Vector<>();
 114         for (int i = beg; i < end; i++) {
 115             this.rdns.addElement(rdns.elementAt(i));
 116         }
 117     }
 118 
 119 
 120     public Object clone() {
 121         return new LdapName(unparsed, rdns);
 122     }
 123 
 124     public String toString() {
 125         if (unparsed != null) {
 126             return unparsed;
 127         }
 128 
 129         StringBuffer buf = new StringBuffer();
 130         for (int i = rdns.size() - 1; i >= 0; i--) {
 131             if (i < rdns.size() - 1) {
 132                 buf.append(',');
 133             }
 134             Rdn rdn = rdns.elementAt(i);
 135             buf.append(rdn);
 136         }
 137 
 138         unparsed = new String(buf);
 139         return unparsed;
 140     }
 141 
 142     public boolean equals(Object obj) {
 143         return ((obj instanceof LdapName) &&
 144                 (compareTo(obj) == 0));
 145     }
 146 
 147     public int compareTo(Object obj) {
 148         LdapName that = (LdapName)obj;
 149 
 150         if ((obj == this) ||                    // check possible shortcuts
 151             (unparsed != null && unparsed.equals(that.unparsed))) {
 152             return 0;
 153         }
 154 
 155         // Compare RDNs one by one, lexicographically.
 156         int minSize = Math.min(rdns.size(), that.rdns.size());
 157         for (int i = 0 ; i < minSize; i++) {
 158             // Compare a single pair of RDNs.
 159             Rdn rdn1 = rdns.elementAt(i);
 160             Rdn rdn2 = that.rdns.elementAt(i);
 161 
 162             int diff = rdn1.compareTo(rdn2);
 163             if (diff != 0) {
 164                 return diff;
 165             }
 166         }
 167         return (rdns.size() - that.rdns.size());        // longer DN wins
 168     }
 169 
 170     public int hashCode() {
 171         // Sum up the hash codes of the components.
 172         int hash = 0;
 173 
 174         // For each RDN...
 175         for (int i = 0; i < rdns.size(); i++) {
 176             Rdn rdn = rdns.elementAt(i);
 177             hash += rdn.hashCode();
 178         }
 179         return hash;
 180     }
 181 
 182     public int size() {
 183         return rdns.size();
 184     }
 185 
 186     public boolean isEmpty() {
 187         return rdns.isEmpty();
 188     }
 189 
 190     public Enumeration<String> getAll() {
 191         final Enumeration<Rdn> enum_ = rdns.elements();
 192 
 193         return new Enumeration<String>() {
 194             public boolean hasMoreElements() {
 195                 return enum_.hasMoreElements();
 196             }
 197             public String nextElement() {
 198                 return enum_.nextElement().toString();
 199             }
 200         };
 201     }
 202 
 203     public String get(int pos) {
 204         return rdns.elementAt(pos).toString();
 205     }
 206 
 207     public Name getPrefix(int pos) {
 208         return new LdapName(null, rdns, 0, pos);
 209     }
 210 
 211     public Name getSuffix(int pos) {
 212         return new LdapName(null, rdns, pos, rdns.size());
 213     }
 214 
 215     public boolean startsWith(Name n) {
 216         int len1 = rdns.size();
 217         int len2 = n.size();


 238              parse();
 239          } catch (InvalidNameException e) {
 240              // shouldn't happen
 241              throw new IllegalStateException("Cannot parse name: " + unparsed);
 242          }
 243          valuesCaseSensitive = caseSensitive;
 244      }
 245 
 246     /*
 247      * Helper method for startsWith() and endsWith().
 248      * Returns true if components [beg,end) match the components of "n".
 249      * If "n" is not an LdapName, each of its components is parsed as
 250      * the string form of an RDN.
 251      * The following must hold:  end - beg == n.size().
 252      */
 253     private boolean matches(int beg, int end, Name n) {
 254         for (int i = beg; i < end; i++) {
 255             Rdn rdn;
 256             if (n instanceof LdapName) {
 257                 LdapName ln = (LdapName)n;
 258                 rdn = ln.rdns.elementAt(i - beg);
 259             } else {
 260                 String rdnString = n.get(i - beg);
 261                 try {
 262                     rdn = (new DnParser(rdnString, valuesCaseSensitive)).getRdn();
 263                 } catch (InvalidNameException e) {
 264                     return false;
 265                 }
 266             }
 267 
 268             if (!rdn.equals(rdns.elementAt(i))) {
 269                 return false;
 270             }
 271         }
 272         return true;
 273     }
 274 
 275     public Name addAll(Name suffix) throws InvalidNameException {
 276         return addAll(size(), suffix);
 277     }
 278 
 279     /*
 280      * If "suffix" is not an LdapName, each of its components is parsed as
 281      * the string form of an RDN.
 282      */
 283     public Name addAll(int pos, Name suffix) throws InvalidNameException {
 284         if (suffix instanceof LdapName) {
 285             LdapName s = (LdapName)suffix;
 286             for (int i = 0; i < s.rdns.size(); i++) {
 287                 rdns.insertElementAt(s.rdns.elementAt(i), pos++);
 288             }
 289         } else {
 290             Enumeration<String> comps = suffix.getAll();
 291             while (comps.hasMoreElements()) {
 292                 DnParser p = new DnParser(comps.nextElement(),
 293                     valuesCaseSensitive);
 294                 rdns.insertElementAt(p.getRdn(), pos++);
 295             }
 296         }
 297         unparsed = null;                                // no longer valid
 298         return this;
 299     }
 300 
 301     public Name add(String comp) throws InvalidNameException {
 302         return add(size(), comp);
 303     }
 304 
 305     public Name add(int pos, String comp) throws InvalidNameException {
 306         Rdn rdn = (new DnParser(comp, valuesCaseSensitive)).getRdn();
 307         rdns.insertElementAt(rdn, pos);
 308         unparsed = null;                                // no longer valid
 309         return this;
 310     }
 311 
 312     public Object remove(int pos) throws InvalidNameException {


 390         private final String name;      // DN being parsed
 391         private final char[] chars;     // characters in LDAP name being parsed
 392         private final int len;          // length of "chars"
 393         private int cur = 0;            // index of first unconsumed char in "chars"
 394         private boolean valuesCaseSensitive;
 395 
 396         /*
 397          * Given an LDAP DN in string form, returns a parser for it.
 398          */
 399         DnParser(String name, boolean valuesCaseSensitive)
 400             throws InvalidNameException {
 401             this.name = name;
 402             len = name.length();
 403             chars = name.toCharArray();
 404             this.valuesCaseSensitive = valuesCaseSensitive;
 405         }
 406 
 407         /*
 408          * Parses the DN, returning a Vector of its RDNs.
 409          */
 410         Vector<Rdn> getDn() throws InvalidNameException {
 411             cur = 0;
 412             Vector<Rdn> rdns = new Vector<>(len / 3 + 10);  // leave room for growth
 413 
 414             if (len == 0) {
 415                 return rdns;
 416             }
 417 
 418             rdns.addElement(parseRdn());
 419             while (cur < len) {
 420                 if (chars[cur] == ',' || chars[cur] == ';') {
 421                     ++cur;
 422                     rdns.insertElementAt(parseRdn(), 0);
 423                 } else {
 424                     throw new InvalidNameException("Invalid name: " + name);
 425                 }
 426             }
 427             return rdns;
 428         }
 429 
 430         /*
 431          * Parses the DN, if it is known to contain a single RDN.
 432          */


 579          * a string attribute value.
 580          */
 581         private boolean atTerminator() {
 582             return (cur < len &&
 583                     (chars[cur] == ',' ||
 584                      chars[cur] == ';' ||
 585                      chars[cur] == '+'));
 586         }
 587     }
 588 
 589 
 590     /*
 591      * Class Rdn represents a set of TypeAndValue.
 592      */
 593     static class Rdn {
 594 
 595         /*
 596          * A vector of the TypeAndValue elements of this Rdn.
 597          * It is sorted to facilitate set operations.
 598          */
 599         private final Vector<TypeAndValue> tvs = new Vector<>();
 600 
 601         void add(TypeAndValue tv) {
 602 
 603             // Set i to index of first element greater than tv, or to
 604             // tvs.size() if there is none.
 605             int i;
 606             for (i = 0; i < tvs.size(); i++) {
 607                 int diff = tv.compareTo(tvs.elementAt(i));
 608                 if (diff == 0) {
 609                     return;             // tv is a duplicate:  ignore it
 610                 } else if (diff < 0) {
 611                     break;
 612                 }
 613             }
 614 
 615             tvs.insertElementAt(tv, i);
 616         }
 617 
 618         public String toString() {
 619             StringBuffer buf = new StringBuffer();
 620             for (int i = 0; i < tvs.size(); i++) {
 621                 if (i > 0) {
 622                     buf.append('+');
 623                 }
 624                 buf.append(tvs.elementAt(i));
 625             }
 626             return new String(buf);
 627         }
 628 
 629         public boolean equals(Object obj) {
 630             return ((obj instanceof Rdn) &&
 631                     (compareTo(obj) == 0));
 632         }
 633 
 634         // Compare TypeAndValue components one by one, lexicographically.
 635         public int compareTo(Object obj) {
 636             Rdn that = (Rdn)obj;
 637             int minSize = Math.min(tvs.size(), that.tvs.size());
 638             for (int i = 0; i < minSize; i++) {
 639                 // Compare a single pair of type/value pairs.
 640                 TypeAndValue tv = tvs.elementAt(i);
 641                 int diff = tv.compareTo(that.tvs.elementAt(i));
 642                 if (diff != 0) {
 643                     return diff;
 644                 }
 645             }
 646             return (tvs.size() - that.tvs.size());      // longer RDN wins
 647         }
 648 
 649         public int hashCode() {
 650             // Sum up the hash codes of the components.
 651             int hash = 0;
 652 
 653             // For each type/value pair...
 654             for (int i = 0; i < tvs.size(); i++) {
 655                 hash += tvs.elementAt(i).hashCode();
 656             }
 657             return hash;
 658         }
 659 
 660         Attributes toAttributes() {
 661             Attributes attrs = new BasicAttributes(true);
 662             TypeAndValue tv;
 663             Attribute attr;
 664 
 665             for (int i = 0; i < tvs.size(); i++) {
 666                 tv = tvs.elementAt(i);
 667                 if ((attr = attrs.get(tv.getType())) == null) {
 668                     attrs.put(tv.getType(), tv.getUnescapedValue());
 669                 } else {
 670                     attr.add(tv.getUnescapedValue());
 671                 }
 672             }
 673             return attrs;
 674         }
 675     }
 676 
 677 
 678     /*
 679      * Class TypeAndValue represents an attribute type and its
 680      * corresponding value.
 681      */
 682     static class TypeAndValue {
 683 
 684         private final String type;
 685         private final String value;             // value, escaped or quoted
 686         private final boolean binary;