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