1
/* Copyright 2002-2005 Elliotte Rusty Harold
3
This library is free software; you can redistribute it and/or modify
4
it under the terms of version 2.1 of the GNU Lesser General Public
5
License as published by the Free Software Foundation.
7
This library is distributed in the hope that it will be useful,
8
but WITHOUT ANY WARRANTY; without even the implied warranty of
9
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
GNU Lesser General Public License for more details.
12
You should have received a copy of the GNU Lesser General Public
13
License along with this library; if not, write to the
14
Free Software Foundation, Inc., 59 Temple Place, Suite 330,
15
Boston, MA 02111-1307 USA
17
You can contact Elliotte Rusty Harold by sending e-mail to
18
elharo@metalab.unc.edu. Please include the word "XOM" in the
19
subject line. The XOM home page is located at http://www.xom.nu/
26
* This class represents an attribute such as
27
* <code>type="empty"</code> or
28
* <code>xlink:href="http://www.example.com"</code>.
32
* Attributes that declare namespaces such as
33
* <code>xmlns="http://www.w3.org/TR/1999/xhtml"</code>
34
* or <code>xmlns:xlink="http://www.w3.org/TR/1999/xlink"</code>
35
* are stored separately on the elements where they
36
* appear. They are never represented as <code>Attribute</code>
40
* @author Elliotte Rusty Harold
44
public class Attribute extends Node {
46
private String localName;
47
private String prefix;
49
private String value = "";
55
* Creates a new attribute in no namespace with the
56
* specified name and value and undeclared type.
59
* @param localName the unprefixed attribute name
60
* @param value the attribute value
62
* @throws IllegalNameException if the local name is not
63
* a namespace well-formed, non-colonized name
64
* @throws IllegalDataException if the value contains characters
65
* which are not legal in XML such as vertical tab or a null.
66
* Characters such as " and & are legal, but will be
67
* automatically escaped when the attribute is serialized.
69
public Attribute(String localName, String value) {
70
this(localName, "", value, Type.UNDECLARED);
76
* Creates a new attribute in no namespace with the
77
* specified name, value, and type.
80
* @param localName the unprefixed attribute name
81
* @param value the attribute value
82
* @param type the attribute type
84
* @throws IllegalNameException if the local name is
85
* not a namespace well-formed non-colonized name
86
* @throws IllegalDataException if the value contains
87
* characters which are not legal in
88
* XML such as vertical tab or a null. Note that
89
* characters such as " and & are legal,
90
* but will be automatically escaped when the
91
* attribute is serialized.
93
public Attribute(String localName, String value, Type type) {
94
this(localName, "", value, type);
100
* Creates a new attribute in the specified namespace with the
101
* specified name and value and undeclared type.
104
* @param name the prefixed attribute name
105
* @param URI the namespace URI
106
* @param value the attribute value
108
* @throws IllegalNameException if the name is not a namespace
110
* @throws IllegalDataException if the value contains characters
111
* which are not legal in XML such as vertical tab or a null.
112
* Note that characters such as " and & are legal, but will
113
* be automatically escaped when the attribute is serialized.
114
* @throws MalformedURIException if <code>URI</code> is not
115
* an RFC 3986 URI reference
116
* @throws NamespaceConflictException if there's no prefix,
117
* but the URI is not the empty string, or the prefix is
118
* <code>xml</code> and the URI is not
119
* http://www.w3.org/XML/1998/namespace
121
public Attribute(String name, String URI, String value) {
122
this(name, URI, value, Type.UNDECLARED);
128
* Creates a new attribute in the specified namespace with the
129
* specified name, value, and type.
132
* @param name the prefixed attribute name
133
* @param URI the namespace URI
134
* @param value the attribute value
135
* @param type the attribute type
137
* @throws IllegalNameException if the name is not a namespace
138
* well-formed prefixed name
139
* @throws IllegalDataException if the value contains
140
* characters which are not legal in XML such as
141
* vertical tab or a null. Note that characters such as
142
* " and & are legal, but will be automatically escaped
143
* when the attribute is serialized.
144
* @throws MalformedURIException if <code>URI</code> is not
145
* an RFC 3986 absolute URI reference
148
String name, String URI, String value, Type type) {
151
String localName = name;
152
// ???? Warning: do not cache the value returned by indexOf here
154
// without profiling. My current tests show doing this slows
155
// down the parse by about 7%. I can't explain it.
156
if (name.indexOf(':') > 0) {
157
prefix = name.substring(0, name.indexOf(':'));
158
localName = name.substring(name.indexOf(':') + 1);
162
_setLocalName(localName);
164
catch (IllegalNameException ex) {
168
_setNamespace(prefix, URI);
170
if ("xml".equals(this.prefix) && "id".equals(this.localName)) {
171
_setType(Attribute.Type.ID);
182
* Creates a copy of the specified attribute.
185
* @param attribute the attribute to copy
188
public Attribute(Attribute attribute) {
190
// These are all immutable types
191
this.localName = attribute.localName;
192
this.prefix = attribute.prefix;
193
this.URI = attribute.URI;
194
this.value = attribute.value;
195
this.type = attribute.type;
200
private Attribute() {}
202
static Attribute build(
203
String qualifiedName, String URI, String value, Type type, String localName) {
205
Attribute result = new Attribute();
208
if (qualifiedName.indexOf(':') >= 0) {
209
prefix = qualifiedName.substring(0, qualifiedName.indexOf(':'));
210
if ("xml:id".equals(qualifiedName)) {
211
type = Attribute.Type.ID;
212
value = normalize(value);
213
// ???? should I only do this if validating?
214
// Verifier.checkNCName(value);
218
result.localName = localName;
219
result.prefix = prefix;
222
result.value = value;
229
private static String normalize(String s) {
231
int length = s.length();
233
while (pos < length && s.charAt(pos) == ' ') pos++;
234
s = s.substring(pos);
235
int end = s.length()-1;
236
while (end > 0 && s.charAt(end) == ' ') end--;
237
s = s.substring(0, end+1);
240
StringBuffer sb = new StringBuffer(length);
241
boolean wasSpace = false;
242
for (int i = 0; i < length; i++) {
243
char c = s.charAt(i);
245
if (wasSpace) continue;
254
return sb.toString();
261
* Returns the DTD type of this attribute.
262
* If this attribute does not have a type, then
263
* <code>Type.UNDECLARED</code> is returned.
266
* @return the DTD type of this attribute
268
public final Type getType() {
275
* Sets the type of this attribute to one of the ten
276
* DTD types or <code>Type.UNDECLARED</code>.
279
* @param type the DTD type of this attribute
281
public void setType(Type type) {
283
if (isXMLID() && ! Type.ID.equals(type)) {
284
throw new IllegalDataException(
285
"Can't change type of xml:id attribute to " + type);
292
private boolean isXMLID() {
293
return "xml".equals(this.prefix) && "id".equals(this.localName);
297
private void _setType(Type type) {
304
* Returns the attribute value. If the attribute was
305
* originally created by a parser, it will have been
306
* normalized according to its type.
307
* However, attributes created in memory are not normalized.
310
* @return the value of the attribute
313
public final String getValue() {
320
* Sets the attribute's value to the specified string,
321
* replacing any previous value. The value is not normalized
325
* @param value the value assigned to the attribute
326
* @throws IllegalDataException if the value contains characters
327
* which are not legal in XML such as vertical tab or a null.
328
* Characters such as " and & are legal, but will be
329
* automatically escaped when the attribute is serialized.
330
* @throws MalformedURIException if this is an
331
* <code>xml:base</code> attribute, and the value is not a
334
public void setValue(String value) {
339
private void _setValue(String value) {
341
if ("xml".equals(this.prefix) && "id".equals(this.localName)) {
342
// ???? do I really want to do this. XML ID test case
344
Verifier.checkNCName(value);
347
Verifier.checkPCDATA(value);
356
* Returns the local name of this attribute,
357
* not including the prefix.
360
* @return the attribute's local name
362
public final String getLocalName() {
369
* Sets the local name of the attribute.
372
* @param localName the new local name
374
* @throws IllegalNameException if <code>localName</code>
375
* is not a namespace well-formed, non-colonized name
378
public void setLocalName(String localName) {
380
if ("id".equals(localName) &&
381
"http://www.w3.org/XML/1998/namespace".equals(this.URI)) {
382
Verifier.checkNCName(this.value);
384
_setLocalName(localName);
386
this.setType(Attribute.Type.ID);
392
private void _setLocalName(String localName) {
393
Verifier.checkNCName(localName);
394
if (localName.equals("xmlns")) {
395
throw new IllegalNameException("The Attribute class is not"
396
+ " used for namespace declaration attributes.");
398
this.localName = localName;
404
* Returns the qualified name of this attribute,
405
* including the prefix if this attribute is in a namespace.
408
* @return the attribute's qualified name
410
public final String getQualifiedName() {
411
if (prefix.length() == 0) return localName;
412
else return prefix + ":" + localName;
418
* Returns the namespace URI of this attribute, or the empty string
419
* if this attribute is not in a namespace.
422
* @return the attribute's namespace URI
424
public final String getNamespaceURI() {
431
* Returns the prefix of this attribute,
432
* or the empty string if this attribute
433
* is not in a namespace.
436
* @return the attribute's prefix
438
public final String getNamespacePrefix() {
445
* Sets the attribute's namespace prefix and URI.
446
* Because attributes must be prefixed in order to have a
447
* namespace URI (and vice versa) this must be done
451
* @param prefix the new namespace prefix
452
* @param URI the new namespace URI
454
* @throws MalformedURIException if <code>URI</code> is
455
* not an RFC 3986 URI reference
456
* @throws IllegalNameException if
458
* <li>The prefix is <code>xmlns</code>.</li>
459
* <li>The prefix is null or the empty string.</li>
460
* <li>The URI is null or the empty string.</li>
462
* @throws NamespaceConflictException if
464
* <li>The prefix is <code>xml</code> and the namespace URI is
465
* not <code>http://www.w3.org/XML/1998/namespace</code>.</li>
466
* <li>The prefix conflicts with an existing declaration
467
* on the attribute's parent element.</li>
470
public void setNamespace(String prefix, String URI) {
472
if ("xml".equals(prefix) && "id".equals(this.localName)) {
473
Verifier.checkNCName(this.value);
474
this.setType(Attribute.Type.ID);
477
_setNamespace(prefix, URI);
482
private void _setNamespace(String prefix, String URI) {
484
if (URI == null) URI = "";
485
if (prefix == null) prefix = "";
487
// ???? use == to compare?
488
if (prefix.equals("xmlns")) {
489
throw new IllegalNameException(
490
"Attribute objects are not used to represent "
491
+ " namespace declarations");
493
else if (prefix.equals("xml")
494
&& !(URI.equals("http://www.w3.org/XML/1998/namespace"))) {
495
throw new NamespaceConflictException(
496
"Wrong namespace URI for xml prefix: " + URI);
498
else if (URI.equals("http://www.w3.org/XML/1998/namespace")
499
&& !prefix.equals("xml")) {
500
throw new NamespaceConflictException(
501
"Wrong prefix for the XML namespace: " + prefix);
503
else if (prefix.length() == 0) {
504
if (URI.length() == 0) {
510
throw new NamespaceConflictException(
511
"Unprefixed attribute " + this.localName
512
+ " cannot be in default namespace " + URI);
515
else if (URI.length() == 0) {
516
throw new NamespaceConflictException(
517
"Attribute prefixes must be declared.");
520
ParentNode parent = this.getParent();
521
if (parent != null) {
522
// test for namespace conflicts
523
Element element = (Element) parent;
524
String currentURI = element.getLocalNamespaceURI(prefix);
525
if (currentURI != null && !currentURI.equals(URI)) {
526
throw new NamespaceConflictException(
527
"New prefix " + prefix
528
+ "conflicts with existing namespace declaration"
534
Verifier.checkAbsoluteURIReference(URI);
535
Verifier.checkNCName(prefix);
538
this.prefix = prefix;
545
* Throws <code>IndexOutOfBoundsException</code>
546
* because attributes do not have children.
549
* @param position the child to return
551
* @return nothing. This method always throws an exception.
553
* @throws IndexOutOfBoundsException because attributes do
556
public final Node getChild(int position) {
557
throw new IndexOutOfBoundsException(
558
"Attributes do not have children"
565
* Returns 0 because attributes do not have children.
570
public final int getChildCount() {
577
* Creates a deep copy of this attribute that
578
* is not attached to an element.
581
* @return a copy of this attribute
585
return new Attribute(this);
591
* Returns a string representation of the attribute
592
* that is a well-formed XML attribute.
595
* @return a string containing the XML form of this attribute
597
public final String toXML() {
598
// It's a common belief that methods like this one should be
599
// implemented using StringBuffers rather than String
600
// concatenation for maximum performance. However,
601
// disassembling the code shows that today's compilers are
602
// smart enough to figure this out for themselves. The compiled
603
// version of this class only uses a single StringBuffer. No
604
// benefit would be gained by making the code more opaque here.
605
return getQualifiedName() + "=\"" + escapeText(value) + "\"";
611
* Returns a string representation of the attribute suitable for
612
* debugging and diagnosis. However, this is not necessarily
613
* a well-formed XML attribute.
616
* @return a non-XML string representation of this attribute
618
* @see java.lang.Object#toString()
620
public final String toString() {
621
return "[" + getClass().getName() + ": "
622
+ getQualifiedName() + "=\""
623
+ Text.escapeLineBreaksAndTruncate(getValue()) + "\"]";
627
private static String escapeText(String s) {
629
int length = s.length();
630
// Give the string buffer enough room for a couple of escaped characters
631
StringBuffer result = new StringBuffer(length+12);
632
for (int i = 0; i < length; i++) {
633
char c = s.charAt(i);
636
result.append("	");
639
result.append("
");
648
result.append("
");
711
result.append(""");
723
result.append("&");
789
result.append("<");
795
result.append(">");
801
return result.toString();
806
boolean isAttribute() {
813
* Uses the type-safe enumeration
814
* design pattern to represent attribute types,
815
* as specified by XML DTDs.
819
* XOM enforces well-formedness, but it does not enforce
820
* validity. Thus it is possible for a single element to have
821
* multiple ID type attributes, or ID type attributes
822
* on different elements to have the same value,
823
* or NMTOKEN type attributes that don't contain legal
824
* XML name tokens, and so forth.
827
* @author Elliotte Rusty Harold
831
public static final class Type {
835
* The type of attributes declared to have type CDATA
836
* in the DTD. The most general attribute type.
837
* All well-formed attribute values are valid for
838
* attributes of type CDATA.
841
public static final Type CDATA = new Type(1);
845
* The type of attributes declared to have type ID
846
* in the DTD. In order to be valid, an ID type attribute
847
* must contain an XML name which is unique among other
848
* ID type attributes in the document.
849
* Furthermore, each element may contain no more than one
850
* ID type attribute. However, XOM does not enforce
851
* such validity constraints.
854
public static final Type ID = new Type(2);
858
* The type of attributes declared to have type IDREF
859
* in the DTD. In order to be valid, an IDREF type attribute
860
* must contain an XML name which is also the value of
861
* ID type attribute of some element in the document.
862
* However, XOM does not enforce such validity constraints.
866
public static final Type IDREF = new Type(3);
870
* The type of attributes declared to have type IDREFS
871
* in the DTD. In order to be valid, an IDREFS type attribute
872
* must contain a white space separated list of
873
* XML names, each of which is also the value of
874
* ID type attribute of some element in the document.
875
* However, XOM does not enforce such validity constraints.
879
public static final Type IDREFS = new Type(4);
883
* The type of attributes declared to have type NMTOKEN
884
* in the DTD. In order to be valid, a NMTOKEN type
885
* attribute must contain a single XML name token. However,
886
* XOM does not enforce such validity constraints.
890
public static final Type NMTOKEN = new Type(5);
894
* The type of attributes declared to have type NMTOKENS
895
* in the DTD. In order to be valid, a NMTOKENS type attribute
896
* must contain a white space separated list of XML name
897
* tokens. However, XOM does not enforce such validity
902
public static final Type NMTOKENS = new Type(6);
907
* The type of attributes declared to have type NOTATION
908
* in the DTD. In order to be valid, a NOTATION type
909
* attribute must contain the name of a notation declared
910
* in the DTD. However, XOM does not enforce such
911
* validity constraints.
915
public static final Type NOTATION = new Type(7);
919
* The type of attributes declared to have type ENTITY
920
* in the DTD. In order to be valid, a ENTITY type attribute
921
* must contain the name of an unparsed entity declared in
922
* the DTD. However, XOM does not enforce such
923
* validity constraints.
927
public static final Type ENTITY = new Type(8);
931
* The type of attributes declared to have type ENTITIES
932
* in the DTD. In order to be valid, an ENTITIES type
933
* attribute must contain a white space separated list of
934
* names of unparsed entities declared in the DTD.
935
* However, XOM does not enforce such validity constraints.
939
public static final Type ENTITIES = new Type(9);
943
* The type of attributes declared by an enumeration
944
* in the DTD. In order to be valid, a enumeration type
945
* attribute must contain exactly one of the names given
946
* in the enumeration in the DTD. However, XOM does not
947
* enforce such validity constraints.
951
* Most parsers report attributes of type enumeration as
952
* having type NMTOKEN. In this case, XOM will not
953
* distinguish NMTOKEN and enumerated attributes.
957
public static final Type ENUMERATION = new Type(10);
961
* The type of attributes not declared in the DTD.
962
* This type only appears in invalid documents.
963
* This is the default type for all attributes in
964
* documents without DTDs.
968
* Most parsers report attributes of undeclared
969
* type as having type CDATA. In this case, XOM
970
* will not distinguish CDATA and undeclared attributes.
973
public static final Type UNDECLARED = new Type(0);
978
* Returns the string name of this type as might
979
* be used in a DTD; for example, "ID", "CDATA", etc.
982
* @return an XML string representation of this type
984
public String getName() {
1008
return "ENUMERATION";
1010
throw new RuntimeException(
1011
"Bug in XOM: unexpected attribute type: " + type);
1017
private final int type;
1019
private Type(int type) {
1026
* Returns a unique identifier for this type.
1029
* @return a unique identifier for this type
1031
* @see java.lang.Object#hashCode()
1033
public int hashCode() {
1040
* Tests for type equality. This is only necessary,
1041
* to handle the case where two <code>Type</code> objects
1042
* are loaded by different class loaders.
1045
* @param o the object compared for equality to this type
1047
* @return true if and only if <code>o</code> represents
1050
* @see java.lang.Object#equals(Object)
1052
public boolean equals(Object o) {
1054
if (o == this) return true;
1055
if (o == null) return false;
1056
if (this.hashCode() != o.hashCode()) return false;
1057
if (!o.getClass().getName().equals("nu.xom.Attribute.Type")) {
1067
* Returns a string representation of the type
1068
* suitable for debugging and diagnosis.
1071
* @return a non-XML string representation of this type
1073
* @see java.lang.Object#toString()
1075
public String toString() {
1078
= new StringBuffer("[Attribute.Type: ");
1079
result.append(getName());
1081
return result.toString();