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