package fr.openwide.core.wicket.more.notification.service.impl; import java.text.Collator; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Map; import org.apache.commons.lang3.tuple.Pair; import org.apache.wicket.markup.ComponentTag; import org.jsoup.nodes.Node; import org.springframework.util.Assert; import com.google.common.base.Function; import com.google.common.collect.Collections2; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Ordering; import com.helger.css.CCSS; import com.helger.css.ECSSVersion; import com.helger.css.ICSSWriterSettings; import com.helger.css.decl.CSSDeclaration; import com.helger.css.decl.CSSSelector; import com.helger.css.decl.CSSSelectorSimpleMember; import com.helger.css.decl.CSSStyleRule; import com.helger.css.decl.CascadingStyleSheet; import com.helger.css.decl.ICSSSelectorMember; import com.helger.css.writer.CSSWriterSettings; import fr.openwide.core.commons.util.ordering.SerializableCollator; import fr.openwide.core.wicket.more.notification.service.IHtmlNotificationCssService.IHtmlNotificationCssRegistry; /** * A simple IHtmlNotificationCssRegistry using Phloc CSS. * <p>This registry does <strong>not</strong> optimize selector matching at all : each time getStyle() is called, the component * tags are compared to every single selector in the stylesheet. */ public class SimplePhlocCssHtmlNotificationCssRegistry implements IHtmlNotificationCssRegistry { private static final Comparator<String> CSS_PROPERTY_NAME_COLLATOR; static { SerializableCollator collator = new SerializableCollator(Locale.ROOT); collator.setStrength(Collator.SECONDARY); CSS_PROPERTY_NAME_COLLATOR = collator; } private static final ICSSWriterSettings STYLE_ATTRIBUTE_WRITER_SETTINGS; static { CSSWriterSettings settings = new CSSWriterSettings(ECSSVersion.CSS30, true); STYLE_ATTRIBUTE_WRITER_SETTINGS = settings; } private final CascadingStyleSheet styleSheet; public SimplePhlocCssHtmlNotificationCssRegistry(CascadingStyleSheet styleSheet) { super(); Assert.notNull(styleSheet); this.styleSheet = styleSheet; } @Override public String getStyle(ComponentTag tag) { return getStyle(new PhlocCssMatchableHtmlTag(tag.getName(), tag.getId(), tag.getAttribute("class"))); } @Override public String getStyle(Node node) { return getStyle(new PhlocCssMatchableHtmlTag(node.nodeName(), node.attr("id"), node.attr("class"))); } private String getStyle(PhlocCssMatchableHtmlTag matchableTag) { Map<CSSStyleRule, CssSelectorSpecificity> matchedRules = getMatchedRules(matchableTag); Collection<CSSDeclaration> declarations = mergeRules(matchedRules); return buildString(declarations); } private Map<CSSStyleRule, CssSelectorSpecificity> getMatchedRules(PhlocCssMatchableHtmlTag matchableTag) { Map<CSSStyleRule, CssSelectorSpecificity> matchedRules = Maps.newHashMap(); for (CSSStyleRule rule : styleSheet.getAllStyleRules()) { Collection<CSSSelector> matchedSelectors = getMatchedSelectors(matchableTag, rule); Collection<CssSelectorSpecificity> matchedSelectorsSpecificities = Collections2.transform(matchedSelectors, new Function<CSSSelector, CssSelectorSpecificity>() { @Override public CssSelectorSpecificity apply(CSSSelector input) { return computeSpecificity(input); } }); if (!matchedSelectorsSpecificities.isEmpty()) { matchedRules.put(rule, Ordering.natural().max(matchedSelectorsSpecificities)); } } return matchedRules; } private Collection<CSSSelector> getMatchedSelectors(PhlocCssMatchableHtmlTag matchableTag, CSSStyleRule rule) { List<CSSSelector> matchedSelectors = Lists.newArrayList(); for (CSSSelector selector : rule.getAllSelectors()) { if (matchableTag.matches(selector)) { matchedSelectors.add(selector); } } return matchedSelectors; } private Collection<CSSDeclaration> mergeRules(Map<CSSStyleRule, CssSelectorSpecificity> matchedRules) { // Maps property names to the elected CSS declaration and its precedence Map<String, Pair<CSSDeclaration, CssDeclarationPrecedence>> mergedDeclarations = Maps.newTreeMap(CSS_PROPERTY_NAME_COLLATOR); for (Map.Entry<CSSStyleRule, CssSelectorSpecificity> entry : matchedRules.entrySet()) { final CssSelectorSpecificity specificity = entry.getValue(); // Merge declarations if the score is high enough for (CSSDeclaration currentDeclaration : entry.getKey().getAllDeclarations()) { String propertyName = currentDeclaration.getProperty(); CssDeclarationPrecedence currentDeclarationPrecedence = new CssDeclarationPrecedence(currentDeclaration.isImportant(), specificity); Pair<CSSDeclaration, CssDeclarationPrecedence> electedDeclaration = mergedDeclarations.get(propertyName); if (electedDeclaration == null || electedDeclaration.getRight().compareTo(currentDeclarationPrecedence) <= 0) { mergedDeclarations.put(propertyName, Pair.of(currentDeclaration, currentDeclarationPrecedence)); } } } return Collections2.transform(mergedDeclarations.values(), new Function<Pair<CSSDeclaration, CssDeclarationPrecedence>, CSSDeclaration>() { @Override public CSSDeclaration apply(Pair<CSSDeclaration, CssDeclarationPrecedence> input) { return input.getLeft(); } }); } private CssSelectorSpecificity computeSpecificity(CSSSelector selector) { int idSelectors = 0; int classAndPseudoClassSelectors = 0; int typeSelectorsAndPseudoElements = 0; for (ICSSSelectorMember member : selector.getAllMembers()) { if (member instanceof CSSSelectorSimpleMember && !((CSSSelectorSimpleMember) member).isPseudo()) { CSSSelectorSimpleMember simpleMember = (CSSSelectorSimpleMember) member; if (simpleMember.isClass()) { ++classAndPseudoClassSelectors; } else if (simpleMember.isElementName()) { ++typeSelectorsAndPseudoElements; } else if (simpleMember.isHash()) { ++idSelectors; } else { throw new IllegalStateException("Unexpected type for simple selector member " + simpleMember); } } else { throw new IllegalStateException("Unexpected type for selector member " + member); } } return new CssSelectorSpecificity(idSelectors, classAndPseudoClassSelectors, typeSelectorsAndPseudoElements); } private String buildString(Collection<CSSDeclaration> declarations) { StringBuilder builder = new StringBuilder(); for (CSSDeclaration declaration : declarations) { builder .append(declaration.getAsCSSString(STYLE_ATTRIBUTE_WRITER_SETTINGS, 0)) .append(CCSS.DEFINITION_END); } return builder.toString(); } }