001package org.jsoup.safety; 002 003/* 004 Thank you to Ryan Grove (wonko.com) for the Ruby HTML cleaner http://github.com/rgrove/sanitize/, which inspired 005 this safe-list configuration, and the initial defaults. 006 */ 007 008import org.jsoup.helper.Validate; 009import org.jsoup.internal.Normalizer; 010import org.jsoup.nodes.Attribute; 011import org.jsoup.nodes.Attributes; 012import org.jsoup.nodes.Element; 013 014import java.util.HashMap; 015import java.util.HashSet; 016import java.util.Iterator; 017import java.util.Map; 018import java.util.Set; 019 020import static org.jsoup.internal.Normalizer.lowerCase; 021 022 023/** 024 Safe-lists define what HTML (elements and attributes) to allow through the cleaner. Everything else is removed. 025 <p> 026 Start with one of the defaults: 027 </p> 028 <ul> 029 <li>{@link #none} 030 <li>{@link #simpleText} 031 <li>{@link #basic} 032 <li>{@link #basicWithImages} 033 <li>{@link #relaxed} 034 </ul> 035 <p> 036 If you need to allow more through (please be careful!), tweak a base safelist with: 037 </p> 038 <ul> 039 <li>{@link #addTags(String... tagNames)} 040 <li>{@link #addAttributes(String tagName, String... attributes)} 041 <li>{@link #addEnforcedAttribute(String tagName, String attribute, String value)} 042 <li>{@link #addProtocols(String tagName, String attribute, String... protocols)} 043 </ul> 044 <p> 045 You can remove any setting from an existing safelist with: 046 </p> 047 <ul> 048 <li>{@link #removeTags(String... tagNames)} 049 <li>{@link #removeAttributes(String tagName, String... attributes)} 050 <li>{@link #removeEnforcedAttribute(String tagName, String attribute)} 051 <li>{@link #removeProtocols(String tagName, String attribute, String... removeProtocols)} 052 </ul> 053 054 <p> 055 The cleaner and these safelists assume that you want to clean a <code>body</code> fragment of HTML (to add user 056 supplied HTML into a templated page), and not to clean a full HTML document. If the latter is the case, you could wrap 057 the templated document HTML around the cleaned body HTML. 058 </p> 059 <p> 060 If you are going to extend a safelist, please be very careful. Make sure you understand what attributes may lead to 061 XSS attack vectors. URL attributes are particularly vulnerable and require careful validation. See 062 the <a href="https://owasp.org/www-community/xss-filter-evasion-cheatsheet">XSS Filter Evasion Cheat Sheet</a> for some 063 XSS attack examples (that jsoup will safegaurd against the default Cleaner and Safelist configuration). 064 </p> 065 */ 066public class Safelist { 067 private static final String All = ":all"; 068 private final Set<TagName> tagNames; // tags allowed, lower case. e.g. [p, br, span] 069 private final Map<TagName, Set<AttributeKey>> attributes; // tag -> attribute[]. allowed attributes [href] for a tag. 070 private final Map<TagName, Map<AttributeKey, AttributeValue>> enforcedAttributes; // always set these attribute values 071 private final Map<TagName, Map<AttributeKey, Set<Protocol>>> protocols; // allowed URL protocols for attributes 072 private boolean preserveRelativeLinks; // option to preserve relative links 073 074 /** 075 This safelist allows only text nodes: any HTML Element or any Node other than a TextNode will be removed. 076 <p> 077 Note that the output of {@link org.jsoup.Jsoup#clean(String, Safelist)} is still <b>HTML</b> even when using 078 this Safelist, and so any HTML entities in the output will be appropriately escaped. If you want plain text, not 079 HTML, you should use a text method such as {@link Element#text()} instead, after cleaning the document. 080 </p> 081 <p>Example:</p> 082 <pre>{@code 083 String sourceBodyHtml = "<p>5 is < 6.</p>"; 084 String html = Jsoup.clean(sourceBodyHtml, Safelist.none()); 085 086 Cleaner cleaner = new Cleaner(Safelist.none()); 087 String text = cleaner.clean(Jsoup.parse(sourceBodyHtml)).text(); 088 089 // html is: 5 is < 6. 090 // text is: 5 is < 6. 091 }</pre> 092 093 @return safelist 094 */ 095 public static Safelist none() { 096 return new Safelist(); 097 } 098 099 /** 100 This safelist allows only simple text formatting: <code>b, em, i, strong, u</code>. All other HTML (tags and 101 attributes) will be removed. 102 103 @return safelist 104 */ 105 public static Safelist simpleText() { 106 return new Safelist() 107 .addTags("b", "em", "i", "strong", "u") 108 ; 109 } 110 111 /** 112 <p> 113 This safelist allows a fuller range of text nodes: <code>a, b, blockquote, br, cite, code, dd, dl, dt, em, i, li, 114 ol, p, pre, q, small, span, strike, strong, sub, sup, u, ul</code>, and appropriate attributes. 115 </p> 116 <p> 117 Links (<code>a</code> elements) can point to <code>http, https, ftp, mailto</code>, and have an enforced 118 <code>rel=nofollow</code> attribute. 119 </p> 120 <p> 121 Does not allow images. 122 </p> 123 124 @return safelist 125 */ 126 public static Safelist basic() { 127 return new Safelist() 128 .addTags( 129 "a", "b", "blockquote", "br", "cite", "code", "dd", "dl", "dt", "em", 130 "i", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong", "sub", 131 "sup", "u", "ul") 132 133 .addAttributes("a", "href") 134 .addAttributes("blockquote", "cite") 135 .addAttributes("q", "cite") 136 137 .addProtocols("a", "href", "ftp", "http", "https", "mailto") 138 .addProtocols("blockquote", "cite", "http", "https") 139 .addProtocols("cite", "cite", "http", "https") 140 141 .addEnforcedAttribute("a", "rel", "nofollow") 142 ; 143 144 } 145 146 /** 147 This safelist allows the same text tags as {@link #basic}, and also allows <code>img</code> tags, with appropriate 148 attributes, with <code>src</code> pointing to <code>http</code> or <code>https</code>. 149 150 @return safelist 151 */ 152 public static Safelist basicWithImages() { 153 return basic() 154 .addTags("img") 155 .addAttributes("img", "align", "alt", "height", "src", "title", "width") 156 .addProtocols("img", "src", "http", "https") 157 ; 158 } 159 160 /** 161 This safelist allows a full range of text and structural body HTML: <code>a, b, blockquote, br, caption, cite, 162 code, col, colgroup, dd, div, dl, dt, em, h1, h2, h3, h4, h5, h6, i, img, li, ol, p, pre, q, small, span, strike, strong, sub, 163 sup, table, tbody, td, tfoot, th, thead, tr, u, ul</code> 164 <p> 165 Links do not have an enforced <code>rel=nofollow</code> attribute, but you can add that if desired. 166 </p> 167 168 @return safelist 169 */ 170 public static Safelist relaxed() { 171 return new Safelist() 172 .addTags( 173 "a", "b", "blockquote", "br", "caption", "cite", "code", "col", 174 "colgroup", "dd", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6", 175 "i", "img", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong", 176 "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", 177 "ul") 178 179 .addAttributes("a", "href", "title") 180 .addAttributes("blockquote", "cite") 181 .addAttributes("col", "span", "width") 182 .addAttributes("colgroup", "span", "width") 183 .addAttributes("img", "align", "alt", "height", "src", "title", "width") 184 .addAttributes("ol", "start", "type") 185 .addAttributes("q", "cite") 186 .addAttributes("table", "summary", "width") 187 .addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width") 188 .addAttributes( 189 "th", "abbr", "axis", "colspan", "rowspan", "scope", 190 "width") 191 .addAttributes("ul", "type") 192 193 .addProtocols("a", "href", "ftp", "http", "https", "mailto") 194 .addProtocols("blockquote", "cite", "http", "https") 195 .addProtocols("cite", "cite", "http", "https") 196 .addProtocols("img", "src", "http", "https") 197 .addProtocols("q", "cite", "http", "https") 198 ; 199 } 200 201 /** 202 Create a new, empty safelist. Generally it will be better to start with a default prepared safelist instead. 203 204 @see #basic() 205 @see #basicWithImages() 206 @see #simpleText() 207 @see #relaxed() 208 */ 209 public Safelist() { 210 tagNames = new HashSet<>(); 211 attributes = new HashMap<>(); 212 enforcedAttributes = new HashMap<>(); 213 protocols = new HashMap<>(); 214 preserveRelativeLinks = false; 215 } 216 217 /** 218 Deep copy an existing Safelist to a new Safelist. 219 @param copy the Safelist to copy 220 */ 221 public Safelist(Safelist copy) { 222 this(); 223 tagNames.addAll(copy.tagNames); 224 for (Map.Entry<TagName, Set<AttributeKey>> copyTagAttributes : copy.attributes.entrySet()) { 225 attributes.put(copyTagAttributes.getKey(), new HashSet<>(copyTagAttributes.getValue())); 226 } 227 for (Map.Entry<TagName, Map<AttributeKey, AttributeValue>> enforcedEntry : copy.enforcedAttributes.entrySet()) { 228 enforcedAttributes.put(enforcedEntry.getKey(), new HashMap<>(enforcedEntry.getValue())); 229 } 230 for (Map.Entry<TagName, Map<AttributeKey, Set<Protocol>>> protocolsEntry : copy.protocols.entrySet()) { 231 Map<AttributeKey, Set<Protocol>> attributeProtocolsCopy = new HashMap<>(); 232 for (Map.Entry<AttributeKey, Set<Protocol>> attributeProtocols : protocolsEntry.getValue().entrySet()) { 233 attributeProtocolsCopy.put(attributeProtocols.getKey(), new HashSet<>(attributeProtocols.getValue())); 234 } 235 protocols.put(protocolsEntry.getKey(), attributeProtocolsCopy); 236 } 237 preserveRelativeLinks = copy.preserveRelativeLinks; 238 } 239 240 /** 241 Add a list of allowed elements to a safelist. (If a tag is not allowed, it will be removed from the HTML.) 242 243 @param tags tag names to allow 244 @return this (for chaining) 245 */ 246 public Safelist addTags(String... tags) { 247 Validate.notNull(tags); 248 249 for (String tagName : tags) { 250 Validate.notEmpty(tagName); 251 Validate.isFalse(tagName.equalsIgnoreCase("noscript"), 252 "noscript is unsupported in Safelists, due to incompatibilities between parsers with and without script-mode enabled"); 253 tagNames.add(TagName.valueOf(tagName)); 254 } 255 return this; 256 } 257 258 /** 259 Remove a list of allowed elements from a safelist. (If a tag is not allowed, it will be removed from the HTML.) 260 261 @param tags tag names to disallow 262 @return this (for chaining) 263 */ 264 public Safelist removeTags(String... tags) { 265 Validate.notNull(tags); 266 267 for(String tag: tags) { 268 Validate.notEmpty(tag); 269 TagName tagName = TagName.valueOf(tag); 270 271 if(tagNames.remove(tagName)) { // Only look in sub-maps if tag was allowed 272 attributes.remove(tagName); 273 enforcedAttributes.remove(tagName); 274 protocols.remove(tagName); 275 } 276 } 277 return this; 278 } 279 280 /** 281 Add a list of allowed attributes to a tag. (If an attribute is not allowed on an element, it will be removed.) 282 <p> 283 E.g.: <code>addAttributes("a", "href", "class")</code> allows <code>href</code> and <code>class</code> attributes 284 on <code>a</code> tags. 285 </p> 286 <p> 287 To make an attribute valid for <b>all tags</b>, use the pseudo tag <code>:all</code>, e.g. 288 <code>addAttributes(":all", "class")</code>. 289 </p> 290 291 @param tag The tag the attributes are for. The tag will be added to the allowed tag list if necessary. 292 @param attributes List of valid attributes for the tag 293 @return this (for chaining) 294 */ 295 public Safelist addAttributes(String tag, String... attributes) { 296 Validate.notEmpty(tag); 297 Validate.notNull(attributes); 298 Validate.isTrue(attributes.length > 0, "No attribute names supplied."); 299 300 addTags(tag); 301 TagName tagName = TagName.valueOf(tag); 302 Set<AttributeKey> attributeSet = new HashSet<>(); 303 for (String key : attributes) { 304 Validate.notEmpty(key); 305 attributeSet.add(AttributeKey.valueOf(key)); 306 } 307 if (this.attributes.containsKey(tagName)) { 308 Set<AttributeKey> currentSet = this.attributes.get(tagName); 309 currentSet.addAll(attributeSet); 310 } else { 311 this.attributes.put(tagName, attributeSet); 312 } 313 return this; 314 } 315 316 /** 317 Remove a list of allowed attributes from a tag. (If an attribute is not allowed on an element, it will be removed.) 318 <p> 319 E.g.: <code>removeAttributes("a", "href", "class")</code> disallows <code>href</code> and <code>class</code> 320 attributes on <code>a</code> tags. 321 </p> 322 <p> 323 To make an attribute invalid for <b>all tags</b>, use the pseudo tag <code>:all</code>, e.g. 324 <code>removeAttributes(":all", "class")</code>. 325 </p> 326 327 @param tag The tag the attributes are for. 328 @param attributes List of invalid attributes for the tag 329 @return this (for chaining) 330 */ 331 public Safelist removeAttributes(String tag, String... attributes) { 332 Validate.notEmpty(tag); 333 Validate.notNull(attributes); 334 Validate.isTrue(attributes.length > 0, "No attribute names supplied."); 335 336 TagName tagName = TagName.valueOf(tag); 337 Set<AttributeKey> attributeSet = new HashSet<>(); 338 for (String key : attributes) { 339 Validate.notEmpty(key); 340 attributeSet.add(AttributeKey.valueOf(key)); 341 } 342 if(tagNames.contains(tagName) && this.attributes.containsKey(tagName)) { // Only look in sub-maps if tag was allowed 343 Set<AttributeKey> currentSet = this.attributes.get(tagName); 344 currentSet.removeAll(attributeSet); 345 346 if(currentSet.isEmpty()) // Remove tag from attribute map if no attributes are allowed for tag 347 this.attributes.remove(tagName); 348 } 349 if(tag.equals(All)) { // Attribute needs to be removed from all individually set tags 350 Iterator<Map.Entry<TagName, Set<AttributeKey>>> it = this.attributes.entrySet().iterator(); 351 while (it.hasNext()) { 352 Map.Entry<TagName, Set<AttributeKey>> entry = it.next(); 353 Set<AttributeKey> currentSet = entry.getValue(); 354 currentSet.removeAll(attributeSet); 355 if(currentSet.isEmpty()) // Remove tag from attribute map if no attributes are allowed for tag 356 it.remove(); 357 } 358 } 359 return this; 360 } 361 362 /** 363 Add an enforced attribute to a tag. An enforced attribute will always be added to the element. If the element 364 already has the attribute set, it will be overridden with this value. 365 <p> 366 E.g.: <code>addEnforcedAttribute("a", "rel", "nofollow")</code> will make all <code>a</code> tags output as 367 <code><a href="..." rel="nofollow"></code> 368 </p> 369 370 @param tag The tag the enforced attribute is for. The tag will be added to the allowed tag list if necessary. 371 @param attribute The attribute name 372 @param value The enforced attribute value 373 @return this (for chaining) 374 */ 375 public Safelist addEnforcedAttribute(String tag, String attribute, String value) { 376 Validate.notEmpty(tag); 377 Validate.notEmpty(attribute); 378 Validate.notEmpty(value); 379 380 TagName tagName = TagName.valueOf(tag); 381 tagNames.add(tagName); 382 AttributeKey attrKey = AttributeKey.valueOf(attribute); 383 AttributeValue attrVal = AttributeValue.valueOf(value); 384 385 if (enforcedAttributes.containsKey(tagName)) { 386 enforcedAttributes.get(tagName).put(attrKey, attrVal); 387 } else { 388 Map<AttributeKey, AttributeValue> attrMap = new HashMap<>(); 389 attrMap.put(attrKey, attrVal); 390 enforcedAttributes.put(tagName, attrMap); 391 } 392 return this; 393 } 394 395 /** 396 Remove a previously configured enforced attribute from a tag. 397 398 @param tag The tag the enforced attribute is for. 399 @param attribute The attribute name 400 @return this (for chaining) 401 */ 402 public Safelist removeEnforcedAttribute(String tag, String attribute) { 403 Validate.notEmpty(tag); 404 Validate.notEmpty(attribute); 405 406 TagName tagName = TagName.valueOf(tag); 407 if(tagNames.contains(tagName) && enforcedAttributes.containsKey(tagName)) { 408 AttributeKey attrKey = AttributeKey.valueOf(attribute); 409 Map<AttributeKey, AttributeValue> attrMap = enforcedAttributes.get(tagName); 410 attrMap.remove(attrKey); 411 412 if(attrMap.isEmpty()) // Remove tag from enforced attribute map if no enforced attributes are present 413 enforcedAttributes.remove(tagName); 414 } 415 return this; 416 } 417 418 /** 419 * Configure this Safelist to preserve relative links in an element's URL attribute, or convert them to absolute 420 * links. By default, this is <b>false</b>: URLs will be made absolute (e.g. start with an allowed protocol, like 421 * e.g. {@code http://}. 422 * <p> 423 * Note that when handling relative links, the input document must have an appropriate {@code base URI} set when 424 * parsing, so that the link's protocol can be confirmed. Regardless of the setting of the {@code preserve relative 425 * links} option, the link must be resolvable against the base URI to an allowed protocol; otherwise the attribute 426 * will be removed. 427 * </p> 428 * 429 * @param preserve {@code true} to allow relative links, {@code false} (default) to deny 430 * @return this Safelist, for chaining. 431 * @see #addProtocols 432 */ 433 public Safelist preserveRelativeLinks(boolean preserve) { 434 preserveRelativeLinks = preserve; 435 return this; 436 } 437 438 /** 439 Add allowed URL protocols for an element's URL attribute. This restricts the possible values of the attribute to 440 URLs with the defined protocol. 441 <p> 442 E.g.: <code>addProtocols("a", "href", "ftp", "http", "https")</code> 443 </p> 444 <p> 445 To allow a link to an in-page URL anchor (i.e. <code><a href="#anchor"></code>, add a <code>#</code>:<br> 446 E.g.: <code>addProtocols("a", "href", "#")</code> 447 </p> 448 449 @param tag Tag the URL protocol is for 450 @param attribute Attribute name 451 @param protocols List of valid protocols 452 @return this, for chaining 453 */ 454 public Safelist addProtocols(String tag, String attribute, String... protocols) { 455 Validate.notEmpty(tag); 456 Validate.notEmpty(attribute); 457 Validate.notNull(protocols); 458 459 TagName tagName = TagName.valueOf(tag); 460 AttributeKey attrKey = AttributeKey.valueOf(attribute); 461 Map<AttributeKey, Set<Protocol>> attrMap; 462 Set<Protocol> protSet; 463 464 if (this.protocols.containsKey(tagName)) { 465 attrMap = this.protocols.get(tagName); 466 } else { 467 attrMap = new HashMap<>(); 468 this.protocols.put(tagName, attrMap); 469 } 470 if (attrMap.containsKey(attrKey)) { 471 protSet = attrMap.get(attrKey); 472 } else { 473 protSet = new HashSet<>(); 474 attrMap.put(attrKey, protSet); 475 } 476 for (String protocol : protocols) { 477 Validate.notEmpty(protocol); 478 Protocol prot = Protocol.valueOf(protocol); 479 protSet.add(prot); 480 } 481 return this; 482 } 483 484 /** 485 Remove allowed URL protocols for an element's URL attribute. If you remove all protocols for an attribute, that 486 attribute will allow any protocol. 487 <p> 488 E.g.: <code>removeProtocols("a", "href", "ftp")</code> 489 </p> 490 491 @param tag Tag the URL protocol is for 492 @param attribute Attribute name 493 @param removeProtocols List of invalid protocols 494 @return this, for chaining 495 */ 496 public Safelist removeProtocols(String tag, String attribute, String... removeProtocols) { 497 Validate.notEmpty(tag); 498 Validate.notEmpty(attribute); 499 Validate.notNull(removeProtocols); 500 501 TagName tagName = TagName.valueOf(tag); 502 AttributeKey attr = AttributeKey.valueOf(attribute); 503 504 // make sure that what we're removing actually exists; otherwise can open the tag to any data and that can 505 // be surprising 506 Validate.isTrue(protocols.containsKey(tagName), "Cannot remove a protocol that is not set."); 507 Map<AttributeKey, Set<Protocol>> tagProtocols = protocols.get(tagName); 508 Validate.isTrue(tagProtocols.containsKey(attr), "Cannot remove a protocol that is not set."); 509 510 Set<Protocol> attrProtocols = tagProtocols.get(attr); 511 for (String protocol : removeProtocols) { 512 Validate.notEmpty(protocol); 513 attrProtocols.remove(Protocol.valueOf(protocol)); 514 } 515 516 if (attrProtocols.isEmpty()) { // Remove protocol set if empty 517 tagProtocols.remove(attr); 518 if (tagProtocols.isEmpty()) // Remove entry for tag if empty 519 protocols.remove(tagName); 520 } 521 return this; 522 } 523 524 /** 525 * Test if the supplied tag is allowed by this safelist. 526 * @param tag test tag 527 * @return true if allowed 528 */ 529 public boolean isSafeTag(String tag) { 530 return tagNames.contains(TagName.valueOf(tag)); 531 } 532 533 /** 534 * Test if the supplied attribute is allowed by this safelist for this tag. 535 * @param tagName tag to consider allowing the attribute in 536 * @param el element under test, to confirm protocol 537 * @param attr attribute under test 538 * @return true if allowed 539 */ 540 public boolean isSafeAttribute(String tagName, Element el, Attribute attr) { 541 TagName tag = TagName.valueOf(tagName); 542 AttributeKey key = AttributeKey.valueOf(attr.getKey()); 543 544 Set<AttributeKey> okSet = attributes.get(tag); 545 if (okSet != null && okSet.contains(key)) { 546 if (protocols.containsKey(tag)) { 547 Map<AttributeKey, Set<Protocol>> attrProts = protocols.get(tag); 548 // ok if not defined protocol; otherwise test 549 return !attrProts.containsKey(key) || testValidProtocol(el, attr, attrProts.get(key)); 550 } else { // attribute found, no protocols defined, so OK 551 return true; 552 } 553 } 554 // might be an enforced attribute? 555 Map<AttributeKey, AttributeValue> enforcedSet = enforcedAttributes.get(tag); 556 if (enforcedSet != null) { 557 Attributes expect = getEnforcedAttributes(tagName); 558 String attrKey = attr.getKey(); 559 if (expect.hasKeyIgnoreCase(attrKey)) { 560 return expect.getIgnoreCase(attrKey).equals(attr.getValue()); 561 } 562 } 563 // no attributes defined for tag, try :all tag 564 return !tagName.equals(All) && isSafeAttribute(All, el, attr); 565 } 566 567 private boolean testValidProtocol(Element el, Attribute attr, Set<Protocol> protocols) { 568 // try to resolve relative urls to abs, and optionally update the attribute so output html has abs. 569 // rels without a baseuri get removed 570 String value = el.absUrl(attr.getKey()); 571 if (value.length() == 0) 572 value = attr.getValue(); // if it could not be made abs, run as-is to allow custom unknown protocols 573 if (!preserveRelativeLinks) 574 attr.setValue(value); 575 576 for (Protocol protocol : protocols) { 577 String prot = protocol.toString(); 578 579 if (prot.equals("#")) { // allows anchor links 580 if (isValidAnchor(value)) { 581 return true; 582 } else { 583 continue; 584 } 585 } 586 587 prot += ":"; 588 589 if (lowerCase(value).startsWith(prot)) { 590 return true; 591 } 592 } 593 return false; 594 } 595 596 private boolean isValidAnchor(String value) { 597 return value.startsWith("#") && !value.matches(".*\\s.*"); 598 } 599 600 /** 601 Gets the Attributes that should be enforced for a given tag 602 * @param tagName the tag 603 * @return the attributes that will be enforced; empty if none are set for the given tag 604 */ 605 public Attributes getEnforcedAttributes(String tagName) { 606 Attributes attrs = new Attributes(); 607 TagName tag = TagName.valueOf(tagName); 608 if (enforcedAttributes.containsKey(tag)) { 609 Map<AttributeKey, AttributeValue> keyVals = enforcedAttributes.get(tag); 610 for (Map.Entry<AttributeKey, AttributeValue> entry : keyVals.entrySet()) { 611 attrs.put(entry.getKey().toString(), entry.getValue().toString()); 612 } 613 } 614 return attrs; 615 } 616 617 // named types for config. All just hold strings, but here for my sanity. 618 619 static class TagName extends TypedValue { 620 TagName(String value) { 621 super(value); 622 } 623 624 static TagName valueOf(String value) { 625 return new TagName(Normalizer.lowerCase(value)); 626 } 627 } 628 629 static class AttributeKey extends TypedValue { 630 AttributeKey(String value) { 631 super(value); 632 } 633 634 static AttributeKey valueOf(String value) { 635 return new AttributeKey(Normalizer.lowerCase(value)); 636 } 637 } 638 639 static class AttributeValue extends TypedValue { 640 AttributeValue(String value) { 641 super(value); 642 } 643 644 static AttributeValue valueOf(String value) { 645 return new AttributeValue(value); 646 } 647 } 648 649 static class Protocol extends TypedValue { 650 Protocol(String value) { 651 super(value); 652 } 653 654 static Protocol valueOf(String value) { 655 return new Protocol(value); 656 } 657 } 658 659 abstract static class TypedValue { 660 private final String value; 661 662 TypedValue(String value) { 663 Validate.notNull(value); 664 this.value = value; 665 } 666 667 @Override 668 public int hashCode() { 669 final int prime = 31; 670 int result = 1; 671 result = prime * result + ((value == null) ? 0 : value.hashCode()); 672 return result; 673 } 674 675 @Override 676 public boolean equals(Object obj) { 677 if (this == obj) return true; 678 if (obj == null) return false; 679 if (getClass() != obj.getClass()) return false; 680 TypedValue other = (TypedValue) obj; 681 if (value == null) { 682 return other.value == null; 683 } else return value.equals(other.value); 684 } 685 686 @Override 687 public String toString() { 688 return value; 689 } 690 } 691}