am 9c0798e0: issue 28: fix Sanitizer.STYLES by changing PolicyFactory to store globals and apply its globals to the other factory when combining PolicyFactories via PolicyFactory.and

* commit '9c0798e090ee7db347657ed2b8604ce26fbe74d1':
  issue 28: fix Sanitizer.STYLES by changing PolicyFactory to store globals and apply its globals to the other factory when combining PolicyFactories via PolicyFactory.and
diff --git a/src/main/org/owasp/html/AttributePolicy.java b/src/main/org/owasp/html/AttributePolicy.java
index ee57fe0..7a1f2d9 100644
--- a/src/main/org/owasp/html/AttributePolicy.java
+++ b/src/main/org/owasp/html/AttributePolicy.java
@@ -28,6 +28,11 @@
 
 package org.owasp.html;
 
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.Set;
+import java.util.LinkedHashSet;
+import javax.annotation.CheckReturnValue;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
@@ -60,38 +65,26 @@
      * An attribute policy equivalent to applying all the given policies in
      * order, failing early if any of them fails.
      */
+    @CheckReturnValue
     public static final AttributePolicy join(AttributePolicy... policies) {
-
-      class PolicyJoiner {
-        AttributePolicy last = null;
-        AttributePolicy out = null;
-
-        void join(AttributePolicy p) {
-          if (REJECT_ALL_ATTRIBUTE_POLICY.equals(p)) {
-            out = p;
-          } else if (!REJECT_ALL_ATTRIBUTE_POLICY.equals(out)) {
-            if (p instanceof JoinedAttributePolicy) {
-              JoinedAttributePolicy jap = (JoinedAttributePolicy) p;
-              join(jap.first);
-              join(jap.second);
-            } else if (p != last) {
-              last = p;
-              if (out == null || IDENTITY_ATTRIBUTE_POLICY.equals(out)) {
-                out = p;
-              } else if (!IDENTITY_ATTRIBUTE_POLICY.equals(p)) {
-                out = new JoinedAttributePolicy(out, p);
-              }
-            }
-          }
+      Set<AttributePolicy> uniq = new LinkedHashSet<AttributePolicy>();
+      for (AttributePolicy p : policies) {
+        if (p instanceof JoinedAttributePolicy) {
+          uniq.addAll(((JoinedAttributePolicy) p).policies);
+        } else if (p != null) {
+          uniq.add(p);
         }
       }
 
-      PolicyJoiner pu = new PolicyJoiner();
-      for (AttributePolicy policy : policies) {
-        if (policy == null) { continue; }
-        pu.join(policy);
+      if (uniq.contains(REJECT_ALL_ATTRIBUTE_POLICY)) {
+        return REJECT_ALL_ATTRIBUTE_POLICY;
       }
-      return pu.out != null ? pu.out : IDENTITY_ATTRIBUTE_POLICY;
+      uniq.remove(IDENTITY_ATTRIBUTE_POLICY);
+      switch (uniq.size()) {
+        case 0:  return IDENTITY_ATTRIBUTE_POLICY;
+        case 1:  return uniq.iterator().next();
+        default: return new JoinedAttributePolicy(uniq);
+      }
     }
   }
 
@@ -116,17 +109,29 @@
 
 @Immutable
 final class JoinedAttributePolicy implements AttributePolicy {
-  final AttributePolicy first, second;
+  final ImmutableList<AttributePolicy> policies;
 
-  JoinedAttributePolicy(AttributePolicy first, AttributePolicy second) {
-    this.first = first;
-    this.second = second;
+  JoinedAttributePolicy(Collection<? extends AttributePolicy> policies) {
+    this.policies = ImmutableList.copyOf(policies);
   }
 
   public @Nullable String apply(
-      String elementName, String attributeName, String value) {
-    value = first.apply(elementName, attributeName, value);
-    return value != null
-        ? second.apply(elementName, attributeName, value) : null;
+      String elementName, String attributeName, @Nullable String value) {
+    for (AttributePolicy p : policies) {
+      if (value == null) { break; }
+      value = p.apply(elementName, attributeName, value);
+    }
+    return value;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return o != null && this.getClass() == o.getClass()
+        && policies.equals(((JoinedAttributePolicy) o).policies);
+  }
+
+  @Override
+  public int hashCode() {
+    return policies.hashCode();
   }
 }
diff --git a/src/main/org/owasp/html/ElementAndAttributePolicies.java b/src/main/org/owasp/html/ElementAndAttributePolicies.java
index 3cc841e..343f2ac 100644
--- a/src/main/org/owasp/html/ElementAndAttributePolicies.java
+++ b/src/main/org/owasp/html/ElementAndAttributePolicies.java
@@ -30,6 +30,7 @@
 
 import java.util.Map;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
 import javax.annotation.concurrent.Immutable;
 
 /**
@@ -98,4 +99,40 @@
         joinedAttrPolicies.build(),
         combinedSkipIfEmpty);
   }
+
+  ElementAndAttributePolicies andGlobals(
+      Map<String, AttributePolicy> globalAttrPolicies) {
+    if (globalAttrPolicies.isEmpty()) { return this; }
+    Map<String, AttributePolicy> anded = null;
+    for (Map.Entry<String, AttributePolicy> e : this.attrPolicies.entrySet()) {
+      String attrName = e.getKey();
+      AttributePolicy globalAttrPolicy = globalAttrPolicies.get(attrName);
+      if (globalAttrPolicy != null) {
+        AttributePolicy attrPolicy = e.getValue();
+        AttributePolicy joined = AttributePolicy.Util.join(
+            attrPolicy, globalAttrPolicy);
+        if (!joined.equals(attrPolicy)) {
+          if (anded == null) {
+            anded = Maps.newLinkedHashMap();
+            anded.putAll(this.attrPolicies);
+          }
+          anded.put(attrName, joined);
+        }
+      }
+    }
+    for (Map.Entry<String, AttributePolicy> e : globalAttrPolicies.entrySet()) {
+      String attrName = e.getKey();
+      if (!this.attrPolicies.containsKey(attrName)) {
+        if (anded == null) {
+          anded = Maps.newLinkedHashMap();
+          anded.putAll(this.attrPolicies);
+        }
+        anded.put(attrName, e.getValue());
+      }
+    }
+    if (anded == null) { return this; }
+    return new ElementAndAttributePolicies(
+        elementName, elPolicy, anded, skipIfEmpty);
+  }
+
 }
diff --git a/src/main/org/owasp/html/FilterUrlByProtocolAttributePolicy.java b/src/main/org/owasp/html/FilterUrlByProtocolAttributePolicy.java
index 48c9723..6435d61 100644
--- a/src/main/org/owasp/html/FilterUrlByProtocolAttributePolicy.java
+++ b/src/main/org/owasp/html/FilterUrlByProtocolAttributePolicy.java
@@ -148,4 +148,15 @@
     return s;
   }
 
-}
\ No newline at end of file
+  @Override
+  public boolean equals(Object o) {
+    return o != null && this.getClass() == o.getClass()
+        && protocols.equals(((FilterUrlByProtocolAttributePolicy) o).protocols);
+  }
+
+  @Override
+  public int hashCode() {
+    return protocols.hashCode();
+  }
+
+}
diff --git a/src/main/org/owasp/html/HtmlPolicyBuilder.java b/src/main/org/owasp/html/HtmlPolicyBuilder.java
index fc47c64..a113cdf 100644
--- a/src/main/org/owasp/html/HtmlPolicyBuilder.java
+++ b/src/main/org/owasp/html/HtmlPolicyBuilder.java
@@ -486,9 +486,10 @@
          : this.textContainers.entrySet()) {
       if (Boolean.TRUE.equals(textContainer.getValue())) {
         textContainers.add(textContainer.getKey());
-  }
+      }
     }
-    return new PolicyFactory(compilePolicies(), textContainers.build());
+    return new PolicyFactory(compilePolicies(), textContainers.build(),
+                             ImmutableMap.copyOf(globalAttrPolicies));
   }
 
   // Speed up subsequent builds by caching the compiled policies.
@@ -597,6 +598,8 @@
           = ImmutableMap.builder();
       for (Map.Entry<String, AttributePolicy> ape : elAttrPolicies.entrySet()) {
         String attributeName = ape.getKey();
+        // Handle below so we don't end up putting the same key into the map
+        // twice.  ImmutableMap.Builder hates that.
         if (globalAttrPolicies.containsKey(attributeName)) { continue; }
         AttributePolicy policy = ape.getValue();
         if (!AttributePolicy.REJECT_ALL_ATTRIBUTE_POLICY.equals(policy)) {
diff --git a/src/main/org/owasp/html/PolicyFactory.java b/src/main/org/owasp/html/PolicyFactory.java
index c4bb86e..ace5ede 100644
--- a/src/main/org/owasp/html/PolicyFactory.java
+++ b/src/main/org/owasp/html/PolicyFactory.java
@@ -54,12 +54,16 @@
     implements Function<HtmlStreamEventReceiver, HtmlSanitizer.Policy> {
 
   private final ImmutableMap<String, ElementAndAttributePolicies> policies;
+  private final ImmutableMap<String, AttributePolicy> globalAttrPolicies;
   private final ImmutableSet<String> textContainers;
 
-  PolicyFactory(ImmutableMap<String, ElementAndAttributePolicies> policies,
-                ImmutableSet<String> textContainers) {
+  PolicyFactory(
+      ImmutableMap<String, ElementAndAttributePolicies> policies,
+      ImmutableSet<String> textContainers,
+      ImmutableMap<String, AttributePolicy> globalAttrPolicies) {
     this.policies = policies;
     this.textContainers = textContainers;
+    this.globalAttrPolicies = globalAttrPolicies;
   }
 
   /** Produces a sanitizer that emits tokens to {@code out}. */
@@ -129,6 +133,7 @@
   public PolicyFactory and(PolicyFactory f) {
     ImmutableMap.Builder<String, ElementAndAttributePolicies> b
         = ImmutableMap.builder();
+    // Merge this and f into a map of element names to attribute policies.
     for (Map.Entry<String, ElementAndAttributePolicies> e
         : policies.entrySet()) {
       String elName = e.getKey();
@@ -136,14 +141,21 @@
       ElementAndAttributePolicies q = f.policies.get(elName);
       if (q != null) {
         p = p.and(q);
+      } else {
+        // Mix in any globals that are not already taken into account in this.
+        p = p.andGlobals(f.globalAttrPolicies);
       }
       b.put(elName, p);
     }
+    // Handle keys that are in f but not in this.
     for (Map.Entry<String, ElementAndAttributePolicies> e
         : f.policies.entrySet()) {
       String elName = e.getKey();
       if (!policies.containsKey(elName)) {
-        b.put(elName, e.getValue());
+        ElementAndAttributePolicies p = e.getValue();
+        // Mix in any globals that are not already taken into account in this.
+        p = p.andGlobals(f.globalAttrPolicies);
+        b.put(elName, p);
       }
     }
     ImmutableSet<String> textContainers;
@@ -157,6 +169,30 @@
         .addAll(f.textContainers)
         .build();
     }
-    return new PolicyFactory(b.build(), textContainers);
+    ImmutableMap<String, AttributePolicy> allGlobalAttrPolicies;
+    if (f.globalAttrPolicies.isEmpty()) {
+      allGlobalAttrPolicies = this.globalAttrPolicies;
+    } else if (this.globalAttrPolicies.isEmpty()) {
+      allGlobalAttrPolicies = f.globalAttrPolicies;
+    } else {
+      ImmutableMap.Builder<String, AttributePolicy> ab = ImmutableMap.builder();
+      for (Map.Entry<String, AttributePolicy> e
+          : this.globalAttrPolicies.entrySet()) {
+        String attrName = e.getKey();
+        ab.put(
+            attrName,
+            AttributePolicy.Util.join(
+                e.getValue(), f.globalAttrPolicies.get(attrName)));
+      }
+      for (Map.Entry<String, AttributePolicy> e
+          : f.globalAttrPolicies.entrySet()) {
+        String attrName = e.getKey();
+        if (!this.globalAttrPolicies.containsKey(attrName)) {
+          ab.put(attrName, e.getValue());
+        }
+      }
+      allGlobalAttrPolicies = ab.build();
+    }
+    return new PolicyFactory(b.build(), textContainers, allGlobalAttrPolicies);
   }
 }
diff --git a/src/main/org/owasp/html/StylingPolicy.java b/src/main/org/owasp/html/StylingPolicy.java
index 1c448e7..95f9eda 100644
--- a/src/main/org/owasp/html/StylingPolicy.java
+++ b/src/main/org/owasp/html/StylingPolicy.java
@@ -216,4 +216,16 @@
     }
     return true;
   }
+
+  @Override
+  public boolean equals(Object o) {
+    return o != null && getClass() == o.getClass()
+        && cssSchema.equals(((StylingPolicy) o).cssSchema);
+  }
+
+  @Override
+  public int hashCode() {
+    return cssSchema.hashCode();
+  }
+
 }
diff --git a/src/tests/org/owasp/html/SanitizersTest.java b/src/tests/org/owasp/html/SanitizersTest.java
index 68f621c..f16e047 100644
--- a/src/tests/org/owasp/html/SanitizersTest.java
+++ b/src/tests/org/owasp/html/SanitizersTest.java
@@ -83,6 +83,18 @@
   }
 
   @Test
+  public static final void testStylesAndFormatting() {
+    PolicyFactory sanitizer = Sanitizers.FORMATTING
+      .and(Sanitizers.BLOCKS).and(Sanitizers.STYLES).and(Sanitizers.LINKS);
+    String input = "<span style=\"font-weight:bold;"
+      + "text-decoration:underline;background-color:yellow\""
+      + ">aaaaaaaaaaaaaaaaaaaaaaa</span>";
+    String got = sanitizer.sanitize(input);
+    String want = input;
+    assertEquals(want, got);
+  }
+
+  @Test
   public static final void testAndIntersects() {
     PolicyFactory restrictedLink = new HtmlPolicyBuilder()
        .allowElements("a")