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 if they link offsite (as indicated by the specified base URI). 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") // has special handling for external links, in Cleaner 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 * 416 * @param preserve {@code true} to allow relative links, {@code false} (default) to deny 417 * @return this Safelist, for chaining. 418 * @see #addProtocols 419 */ 420 public Safelist preserveRelativeLinks(boolean preserve) { 421 preserveRelativeLinks = preserve; 422 return this; 423 } 424 425 /** 426 * Get the current setting for preserving relative links. 427 * @return {@code true} if relative links are preserved, {@code false} if they are converted to absolute. 428 */ 429 public boolean preserveRelativeLinks() { 430 return preserveRelativeLinks; 431 } 432 433 /** 434 Add allowed URL protocols for an element's URL attribute. This restricts the possible values of the attribute to 435 URLs with the defined protocol. 436 <p> 437 E.g.: <code>addProtocols("a", "href", "ftp", "http", "https")</code> 438 </p> 439 <p> 440 To allow a link to an in-page URL anchor (i.e. <code><a href="#anchor"></code>, add a <code>#</code>:<br> 441 E.g.: <code>addProtocols("a", "href", "#")</code> 442 </p> 443 444 @param tag Tag the URL protocol is for 445 @param attribute Attribute name 446 @param protocols List of valid protocols 447 @return this, for chaining 448 */ 449 public Safelist addProtocols(String tag, String attribute, String... protocols) { 450 Validate.notEmpty(tag); 451 Validate.notEmpty(attribute); 452 Validate.notNull(protocols); 453 454 TagName tagName = TagName.valueOf(tag); 455 AttributeKey attrKey = AttributeKey.valueOf(attribute); 456 Map<AttributeKey, Set<Protocol>> attrMap = this.protocols.computeIfAbsent(tagName, Functions.mapFunction()); 457 Set<Protocol> protSet = attrMap.computeIfAbsent(attrKey, Functions.setFunction()); 458 459 for (String protocol : protocols) { 460 Validate.notEmpty(protocol); 461 Protocol prot = Protocol.valueOf(protocol); 462 protSet.add(prot); 463 } 464 return this; 465 } 466 467 /** 468 Remove allowed URL protocols for an element's URL attribute. If you remove all protocols for an attribute, that 469 attribute will allow any protocol. 470 <p> 471 E.g.: <code>removeProtocols("a", "href", "ftp")</code> 472 </p> 473 474 @param tag Tag the URL protocol is for 475 @param attribute Attribute name 476 @param removeProtocols List of invalid protocols 477 @return this, for chaining 478 */ 479 public Safelist removeProtocols(String tag, String attribute, String... removeProtocols) { 480 Validate.notEmpty(tag); 481 Validate.notEmpty(attribute); 482 Validate.notNull(removeProtocols); 483 484 TagName tagName = TagName.valueOf(tag); 485 AttributeKey attr = AttributeKey.valueOf(attribute); 486 487 // make sure that what we're removing actually exists; otherwise can open the tag to any data and that can 488 // be surprising 489 Validate.isTrue(protocols.containsKey(tagName), "Cannot remove a protocol that is not set."); 490 Map<AttributeKey, Set<Protocol>> tagProtocols = protocols.get(tagName); 491 Validate.isTrue(tagProtocols.containsKey(attr), "Cannot remove a protocol that is not set."); 492 493 Set<Protocol> attrProtocols = tagProtocols.get(attr); 494 for (String protocol : removeProtocols) { 495 Validate.notEmpty(protocol); 496 attrProtocols.remove(Protocol.valueOf(protocol)); 497 } 498 499 if (attrProtocols.isEmpty()) { // Remove protocol set if empty 500 tagProtocols.remove(attr); 501 if (tagProtocols.isEmpty()) // Remove entry for tag if empty 502 protocols.remove(tagName); 503 } 504 return this; 505 } 506 507 /** 508 * Test if the supplied tag is allowed by this safelist. 509 * @param tag test tag 510 * @return true if allowed 511 */ 512 public boolean isSafeTag(String tag) { 513 return tagNames.contains(TagName.valueOf(tag)); 514 } 515 516 /** 517 * Test if the supplied attribute is allowed by this safelist for this tag. 518 * @param tagName tag to consider allowing the attribute in 519 * @param el element under test, to confirm protocol 520 * @param attr attribute under test 521 * @return true if allowed 522 */ 523 public boolean isSafeAttribute(String tagName, Element el, Attribute attr) { 524 TagName tag = TagName.valueOf(tagName); 525 AttributeKey key = AttributeKey.valueOf(attr.getKey()); 526 527 Set<AttributeKey> okSet = attributes.get(tag); 528 if (okSet != null && okSet.contains(key)) { 529 if (protocols.containsKey(tag)) { 530 Map<AttributeKey, Set<Protocol>> attrProts = protocols.get(tag); 531 // ok if not defined protocol; otherwise test 532 return !attrProts.containsKey(key) || testValidProtocol(el, attr, attrProts.get(key)); 533 } else { // attribute found, no protocols defined, so OK 534 return true; 535 } 536 } 537 // might be an enforced attribute? 538 Map<AttributeKey, AttributeValue> enforcedSet = enforcedAttributes.get(tag); 539 if (enforcedSet != null) { 540 Attributes expect = getEnforcedAttributes(tagName); 541 String attrKey = attr.getKey(); 542 if (expect.hasKeyIgnoreCase(attrKey)) { 543 return expect.getIgnoreCase(attrKey).equals(attr.getValue()); 544 } 545 } 546 // no attributes defined for tag, try :all tag 547 return !tagName.equals(All) && isSafeAttribute(All, el, attr); 548 } 549 550 private boolean testValidProtocol(Element el, Attribute attr, Set<Protocol> protocols) { 551 // try to resolve relative urls to abs, and optionally update the attribute so output html has abs. 552 // rels without a baseuri get removed 553 String value = el.absUrl(attr.getKey()); 554 if (value.length() == 0) 555 value = attr.getValue(); // if it could not be made abs, run as-is to allow custom unknown protocols 556 if (!preserveRelativeLinks) 557 attr.setValue(value); 558 559 for (Protocol protocol : protocols) { 560 String prot = protocol.toString(); 561 562 if (prot.equals("#")) { // allows anchor links 563 if (isValidAnchor(value)) { 564 return true; 565 } else { 566 continue; 567 } 568 } 569 570 prot += ":"; 571 572 if (lowerCase(value).startsWith(prot)) { 573 return true; 574 } 575 } 576 return false; 577 } 578 579 private static boolean isValidAnchor(String value) { 580 return value.startsWith("#") && !value.matches(".*\\s.*"); 581 } 582 583 /** 584 Gets the Attributes that should be enforced for a given tag 585 * @param tagName the tag 586 * @return the attributes that will be enforced; empty if none are set for the given tag 587 */ 588 public Attributes getEnforcedAttributes(String tagName) { 589 Attributes attrs = new Attributes(); 590 TagName tag = TagName.valueOf(tagName); 591 if (enforcedAttributes.containsKey(tag)) { 592 Map<AttributeKey, AttributeValue> keyVals = enforcedAttributes.get(tag); 593 for (Map.Entry<AttributeKey, AttributeValue> entry : keyVals.entrySet()) { 594 attrs.put(entry.getKey().toString(), entry.getValue().toString()); 595 } 596 } 597 return attrs; 598 } 599 600 // named types for config. All just hold strings, but here for my sanity. 601 602 static class TagName extends TypedValue { 603 TagName(String value) { 604 super(value); 605 } 606 607 static TagName valueOf(String value) { 608 return new TagName(Normalizer.lowerCase(value)); 609 } 610 } 611 612 static class AttributeKey extends TypedValue { 613 AttributeKey(String value) { 614 super(value); 615 } 616 617 static AttributeKey valueOf(String value) { 618 return new AttributeKey(Normalizer.lowerCase(value)); 619 } 620 } 621 622 static class AttributeValue extends TypedValue { 623 AttributeValue(String value) { 624 super(value); 625 } 626 627 static AttributeValue valueOf(String value) { 628 return new AttributeValue(value); 629 } 630 } 631 632 static class Protocol extends TypedValue { 633 Protocol(String value) { 634 super(value); 635 } 636 637 static Protocol valueOf(String value) { 638 return new Protocol(value); 639 } 640 } 641 642 abstract static class TypedValue { 643 private final String value; 644 645 TypedValue(String value) { 646 Validate.notNull(value); 647 this.value = value; 648 } 649 650 @Override 651 public int hashCode() { 652 return value.hashCode(); 653 } 654 655 @Override 656 public boolean equals(Object obj) { 657 if (this == obj) return true; 658 if (obj == null || getClass() != obj.getClass()) return false; 659 TypedValue other = (TypedValue) obj; 660 return Objects.equals(value, other.value); 661 } 662 663 @Override 664 public String toString() { 665 return value; 666 } 667 } 668}