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