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