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 &lt; 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 &lt; 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>&lt;a href="..." rel="nofollow"&gt;</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>&lt;a href="#anchor"&gt;</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}