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