/*! * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2002-2013 Pentaho Corporation.. All rights reserved. */ package org.pentaho.reporting.libraries.css.resolver.impl; import org.pentaho.reporting.libraries.base.util.ObjectUtilities; import org.pentaho.reporting.libraries.css.PseudoPage; import org.pentaho.reporting.libraries.css.dom.DocumentContext; import org.pentaho.reporting.libraries.css.dom.LayoutElement; import org.pentaho.reporting.libraries.css.dom.StyleReference; import org.pentaho.reporting.libraries.css.model.CSSCounterRule; import org.pentaho.reporting.libraries.css.model.CSSPageRule; import org.pentaho.reporting.libraries.css.model.CSSStyleRule; import org.pentaho.reporting.libraries.css.model.StyleRule; import org.pentaho.reporting.libraries.css.model.StyleSheet; import org.pentaho.reporting.libraries.css.namespace.NamespaceCollection; import org.pentaho.reporting.libraries.css.namespace.NamespaceDefinition; import org.pentaho.reporting.libraries.css.namespace.Namespaces; import org.pentaho.reporting.libraries.css.resolver.StyleRuleMatcher; import org.pentaho.reporting.libraries.css.selectors.CSSSelector; import org.pentaho.reporting.libraries.css.values.CSSValue; import org.pentaho.reporting.libraries.resourceloader.Resource; import org.pentaho.reporting.libraries.resourceloader.ResourceException; import org.pentaho.reporting.libraries.resourceloader.ResourceKey; import org.pentaho.reporting.libraries.resourceloader.ResourceKeyCreationException; import org.pentaho.reporting.libraries.resourceloader.ResourceManager; import org.w3c.css.sac.AttributeCondition; import org.w3c.css.sac.CombinatorCondition; import org.w3c.css.sac.Condition; import org.w3c.css.sac.ConditionalSelector; import org.w3c.css.sac.DescendantSelector; import org.w3c.css.sac.ElementSelector; import org.w3c.css.sac.NegativeCondition; import org.w3c.css.sac.NegativeSelector; import org.w3c.css.sac.Selector; import org.w3c.css.sac.SiblingSelector; import org.w3c.css.sac.SimpleSelector; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Locale; import java.util.StringTokenizer; /** * A stateless implementation of the style rule matching. This implementation is stateless within the current layout * process. * * @author Thomas Morgner */ public class SimpleStyleRuleMatcher implements StyleRuleMatcher { private DocumentContext layoutProcess; private ResourceManager resourceManager; private CSSStyleRule[] activeStyleRules; private CSSStyleRule[] activePseudoStyleRules; private CSSPageRule[] pageRules; private CSSCounterRule[] counterRules; private NamespaceCollection namespaces; public SimpleStyleRuleMatcher() { } public void initialize( final DocumentContext layoutProcess ) { if ( layoutProcess == null ) { throw new NullPointerException(); } this.layoutProcess = layoutProcess; this.resourceManager = layoutProcess.getResourceManager(); final ArrayList pageRules = new ArrayList(); final ArrayList counterRules = new ArrayList(); final ArrayList styleRules = new ArrayList(); final DocumentContext dc = this.layoutProcess; namespaces = dc.getNamespaces(); final String[] nsUri = namespaces.getNamespaces(); for ( int i = 0; i < nsUri.length; i++ ) { final String uri = nsUri[ i ]; final NamespaceDefinition nsDef = namespaces.getDefinition( uri ); final ResourceKey rawKey = nsDef.getDefaultStyleSheetLocation(); if ( rawKey == null ) { // there is no default stylesheet for that namespace. continue; } final ResourceKey baseKey = layoutProcess.getContextKey(); final StyleSheet styleSheet = parseStyleSheet( rawKey, baseKey ); if ( styleSheet == null ) { continue; } // Log.debug("Loaded stylesheet from " + rawKey + " for namespace " + nsDef.getURI()); addStyleRules( styleSheet, styleRules ); addPageRules( styleSheet, pageRules ); addCounterRules( styleSheet, counterRules ); } final StyleReference[] refs = dc.getStyleReferences(); for ( int i = 0; i < refs.length; i++ ) { final StyleReference ref = refs[ i ]; if ( ref.getType() == StyleReference.LINK ) { handleLinkNode( dc, ref, styleRules, pageRules, counterRules ); } else { handleStyleNode( dc, ref, styleRules, pageRules, counterRules ); } } activeStyleRules = (CSSStyleRule[]) styleRules.toArray( new CSSStyleRule[ styleRules.size() ] ); this.pageRules = (CSSPageRule[]) pageRules.toArray( new CSSPageRule[ pageRules.size() ] ); this.counterRules = (CSSCounterRule[]) counterRules.toArray( new CSSCounterRule[ counterRules.size() ] ); styleRules.clear(); for ( int i = 0; i < activeStyleRules.length; i++ ) { final CSSStyleRule activeStyleRule = activeStyleRules[ i ]; if ( isPseudoElementRule( activeStyleRule ) == false ) { continue; } styleRules.add( activeStyleRule ); } activePseudoStyleRules = (CSSStyleRule[]) styleRules.toArray( new CSSStyleRule[ styleRules.size() ] ); } private void handleLinkNode( final DocumentContext context, final StyleReference node, final ArrayList styleRules, final ArrayList pageRules, final ArrayList counterRules ) { // do some external parsing // (Same as the <link> element of HTML) try { final String href = node.getStyleContent(); final ResourceKey baseKey = context.getContextKey(); final ResourceKey derivedKey; if ( baseKey == null ) { derivedKey = resourceManager.createKey( href ); } else { derivedKey = resourceManager.deriveKey( baseKey, String.valueOf( href ) ); } final StyleSheet styleSheet = parseStyleSheet( derivedKey, null ); if ( styleSheet == null ) { return; } addStyleRules( styleSheet, styleRules ); addPageRules( styleSheet, pageRules ); addCounterRules( styleSheet, counterRules ); } catch ( ResourceKeyCreationException e ) { e.printStackTrace(); } } private void handleStyleNode( final DocumentContext context, final StyleReference node, final ArrayList styleRules, final ArrayList pageRules, final ArrayList counterRules ) { // do some inline parsing // (Same as the <style> element of HTML) // we also accept preparsed content ... final String styleText = node.getStyleContent(); try { final byte[] bytes = styleText.getBytes( "UTF-8" ); final ResourceKey rawKey = resourceManager.createKey( bytes ); final ResourceKey baseKey = context.getContextKey(); final StyleSheet styleSheet = parseStyleSheet( rawKey, baseKey ); if ( styleSheet == null ) { return; } addStyleRules( styleSheet, styleRules ); addPageRules( styleSheet, pageRules ); addCounterRules( styleSheet, counterRules ); } catch ( UnsupportedEncodingException e ) { e.printStackTrace(); } catch ( ResourceKeyCreationException e ) { e.printStackTrace(); } } private void addCounterRules( final StyleSheet styleSheet, final ArrayList rules ) { final int sc = styleSheet.getStyleSheetCount(); for ( int i = 0; i < sc; i++ ) { addCounterRules( styleSheet.getStyleSheet( i ), rules ); } final int rc = styleSheet.getRuleCount(); for ( int i = 0; i < rc; i++ ) { final StyleRule rule = styleSheet.getRule( i ); if ( rule instanceof CSSCounterRule ) { final CSSCounterRule drule = (CSSCounterRule) rule; rules.add( drule ); } } } private void addPageRules( final StyleSheet styleSheet, final ArrayList rules ) { final int sc = styleSheet.getStyleSheetCount(); for ( int i = 0; i < sc; i++ ) { addPageRules( styleSheet.getStyleSheet( i ), rules ); } final int rc = styleSheet.getRuleCount(); for ( int i = 0; i < rc; i++ ) { final StyleRule rule = styleSheet.getRule( i ); if ( rule instanceof CSSPageRule ) { final CSSPageRule drule = (CSSPageRule) rule; rules.add( drule ); } } } private void addStyleRules( final StyleSheet styleSheet, final ArrayList activeStyleRules ) { final int sc = styleSheet.getStyleSheetCount(); for ( int i = 0; i < sc; i++ ) { addStyleRules( styleSheet.getStyleSheet( i ), activeStyleRules ); } final int rc = styleSheet.getRuleCount(); for ( int i = 0; i < rc; i++ ) { final StyleRule rule = styleSheet.getRule( i ); if ( rule instanceof CSSStyleRule ) { final CSSStyleRule drule = (CSSStyleRule) rule; activeStyleRules.add( drule ); } } } private StyleSheet parseStyleSheet( final ResourceKey key, final ResourceKey context ) { try { final Resource resource = resourceManager.create ( key, context, StyleSheet.class ); return (StyleSheet) resource.getResource(); } catch ( ResourceException e ) { // Log.info("Unable to parse StyleSheet: " + e.getLocalizedMessage()); } return null; } private boolean isPseudoElementRule( final CSSStyleRule rule ) { final CSSSelector selector = rule.getSelector(); if ( selector == null ) { return false; } if ( selector.getSelectorType() != Selector.SAC_CONDITIONAL_SELECTOR ) { return false; } final ConditionalSelector cs = (ConditionalSelector) selector; final Condition condition = cs.getCondition(); if ( condition.getConditionType() != Condition.SAC_PSEUDO_CLASS_CONDITION ) { return false; } return true; } public boolean isMatchingPseudoElement( final LayoutElement element, final String pseudo ) { for ( int i = 0; i < activePseudoStyleRules.length; i++ ) { final CSSStyleRule activeStyleRule = activePseudoStyleRules[ i ]; final CSSSelector selector = activeStyleRule.getSelector(); final ConditionalSelector cs = (ConditionalSelector) selector; final Condition condition = cs.getCondition(); final AttributeCondition ac = (AttributeCondition) condition; if ( ObjectUtilities.equal( ac.getValue(), pseudo ) == false ) { continue; } final SimpleSelector simpleSelector = cs.getSimpleSelector(); if ( isMatch( element, simpleSelector ) ) { return true; } } return false; } /** * Creates an independent copy of this style rule matcher. * * @return this instance, as this implementation is stateless */ public StyleRuleMatcher deriveInstance() { return this; } public CSSStyleRule[] getMatchingRules( final LayoutElement element ) { final ArrayList retvals = new ArrayList(); for ( int i = 0; i < activeStyleRules.length; i++ ) { final CSSStyleRule activeStyleRule = activeStyleRules[ i ]; final CSSSelector selector = activeStyleRule.getSelector(); if ( selector == null ) { continue; } if ( isMatch( element, selector ) ) { retvals.add( activeStyleRule ); } } // Log.debug ("Got " + retvals.size() + " matching rules for " + // layoutContext.getTagName() + ":" + // layoutContext.getPseudoElement()); return (CSSStyleRule[]) retvals.toArray ( new CSSStyleRule[ retvals.size() ] ); } private boolean isMatch( final LayoutElement node, final Selector selector ) { final short selectorType = selector.getSelectorType(); switch( selectorType ) { case Selector.SAC_ANY_NODE_SELECTOR: return true; case Selector.SAC_ROOT_NODE_SELECTOR: return node.getParentLayoutElement() == null; case Selector.SAC_NEGATIVE_SELECTOR: { final NegativeSelector negativeSelector = (NegativeSelector) selector; return isMatch( node, negativeSelector ) == false; } case Selector.SAC_DIRECT_ADJACENT_SELECTOR: { final SiblingSelector silbSelect = (SiblingSelector) selector; return isSilblingMatch( node, silbSelect ); } case Selector.SAC_PSEUDO_ELEMENT_SELECTOR: { return node.isPseudoElement(); } case Selector.SAC_ELEMENT_NODE_SELECTOR: { final ElementSelector es = (ElementSelector) selector; final String localName = es.getLocalName(); if ( localName != null ) { if ( localName.equals( node.getTagName() ) == false ) { return false; } } final String namespaceURI = es.getNamespaceURI(); if ( namespaceURI != null ) { return containsNamespace( namespaceURI, layoutProcess.getNamespaces() ); // if (namespaceURI.equals(layoutProcess.getNamespaces()) == false) // { // return false; // } } return true; } case Selector.SAC_CHILD_SELECTOR: { final DescendantSelector ds = (DescendantSelector) selector; if ( isMatch( node, ds.getSimpleSelector() ) == false ) { return false; } final LayoutElement parent = node.getParentLayoutElement(); return ( isMatch( parent, ds.getAncestorSelector() ) ); } case Selector.SAC_DESCENDANT_SELECTOR: { final DescendantSelector ds = (DescendantSelector) selector; if ( isMatch( node, ds.getSimpleSelector() ) == false ) { return false; } return ( isDescendantMatch( node, ds.getAncestorSelector() ) ); } case Selector.SAC_CONDITIONAL_SELECTOR: { final ConditionalSelector cs = (ConditionalSelector) selector; if ( evaluateCondition( node, cs.getCondition() ) == false ) { return false; } if ( isMatch( node, cs.getSimpleSelector() ) == false ) { return false; } return true; } default: return false; } } /** * Searches the namespace collection and indicates if the supplied namespace can be found within. * * @param namespaceURI the namespace used in the search * @param namespaces the collection of namespaces being searched * @return <code>true</code> if the supplied namespace is contained in the namespace collection, <code>false</code> * otherwise. If either the supplied namespace or the collection is <code>null</code>, this method will return * <code>false</code> */ private boolean containsNamespace( String namespaceURI, NamespaceCollection namespaces ) { if ( namespaces == null || namespaceURI == null ) { return false; } String namespaceStrings[] = namespaces.getNamespaces(); for ( int i = 0; i < namespaceStrings.length; ++i ) { if ( namespaceURI.equals( namespaceStrings[ i ] ) ) { // FOUND! return true; } } // None found return false; } private boolean evaluateCondition( final LayoutElement node, final Condition condition ) { switch( condition.getConditionType() ) { case Condition.SAC_AND_CONDITION: { final CombinatorCondition cc = (CombinatorCondition) condition; return ( evaluateCondition( node, cc.getFirstCondition() ) && evaluateCondition( node, cc.getSecondCondition() ) ); } case Condition.SAC_OR_CONDITION: { final CombinatorCondition cc = (CombinatorCondition) condition; return ( evaluateCondition( node, cc.getFirstCondition() ) || evaluateCondition( node, cc.getSecondCondition() ) ); } case Condition.SAC_ATTRIBUTE_CONDITION: { final AttributeCondition ac = (AttributeCondition) condition; String namespaceURI = ac.getNamespaceURI(); if ( namespaceURI == null ) { namespaceURI = node.getNamespace(); } final Object attr = node.getAttribute ( namespaceURI, ac.getLocalName() ); if ( ac.getValue() == null ) { // dont care what's inside, as long as there is a value .. return attr != null; } else { return ObjectUtilities.equal( attr, ac.getValue() ); } } case Condition.SAC_CLASS_CONDITION: { final AttributeCondition ac = (AttributeCondition) condition; final String namespace = node.getNamespace(); if ( namespace == null ) { return false; } final NamespaceDefinition ndef = namespaces.getDefinition( namespace ); if ( ndef == null ) { return false; } final String[] classAttribute = ndef.getClassAttribute( node.getTagName() ); for ( int i = 0; i < classAttribute.length; i++ ) { final String attr = classAttribute[ i ]; final String htmlAttr = (String) node.getAttribute( namespace, attr ); if ( isOneOfAttributes( htmlAttr, ac.getValue() ) ) { return true; } } return false; } case Condition.SAC_ID_CONDITION: { final AttributeCondition ac = (AttributeCondition) condition; final Object id = node.getAttribute( Namespaces.XML_NAMESPACE, "id" ); return ObjectUtilities.equal( ac.getValue(), id ); } case Condition.SAC_LANG_CONDITION: { final AttributeCondition ac = (AttributeCondition) condition; final Locale locale = node.getLanguage(); final String lang = locale.getLanguage(); return isBeginHyphenAttribute( lang, ac.getValue() ); } case Condition.SAC_NEGATIVE_CONDITION: { final NegativeCondition nc = (NegativeCondition) condition; return evaluateCondition( node, nc.getCondition() ) == false; } case Condition.SAC_ONE_OF_ATTRIBUTE_CONDITION: { final AttributeCondition ac = (AttributeCondition) condition; final String attr = (String) node.getAttribute ( ac.getNamespaceURI(), ac.getLocalName() ); return isOneOfAttributes( attr, ac.getValue() ); } case Condition.SAC_PSEUDO_CLASS_CONDITION: { final AttributeCondition ac = (AttributeCondition) condition; final String pseudoClass = node.getPseudoElement(); if ( pseudoClass == null ) { return false; } if ( pseudoClass.equals( ac.getValue() ) ) { return true; } return false; } case Condition.SAC_ONLY_CHILD_CONDITION: case Condition.SAC_ONLY_TYPE_CONDITION: case Condition.SAC_POSITIONAL_CONDITION: case Condition.SAC_CONTENT_CONDITION: default: { // todo return false; } } } private boolean isOneOfAttributes( final String attrValue, final String value ) { if ( attrValue == null ) { return false; } if ( attrValue.equals( value ) ) { return true; } final StringTokenizer strTok = new StringTokenizer( attrValue ); while ( strTok.hasMoreTokens() ) { final String token = strTok.nextToken(); if ( token.equals( value ) ) { return true; } } return false; } private boolean isBeginHyphenAttribute( final String attrValue, final String value ) { if ( attrValue == null ) { return false; } if ( value == null ) { return false; } return ( attrValue.startsWith( value ) ); } private boolean isDescendantMatch( final LayoutElement node, final Selector selector ) { LayoutElement parent = node.getParentLayoutElement(); while ( parent != null ) { if ( isMatch( parent, selector ) ) { return true; } parent = parent.getParentLayoutElement(); } return false; } private boolean isSilblingMatch( final LayoutElement node, final SiblingSelector select ) { LayoutElement pred = node.getPreviousLayoutElement(); while ( pred != null ) { if ( isMatch( pred, select ) ) { return true; } pred = pred.getPreviousLayoutElement(); } return false; } public CSSPageRule[] getPageRule( final CSSValue pageName, final PseudoPage[] pseudoPages ) { final CSSPageRule[] pageRules = this.pageRules; final ArrayList rules = new ArrayList(); for ( int i = 0; i < pageRules.length; i++ ) { final CSSPageRule rule = pageRules[ i ]; final String rulePageName = rule.getName(); // Check the page name. if ( rulePageName != null ) { if ( rulePageName.equals( pageName ) == false ) { continue; } } // And the pseudo page .. final String rulePseudoPage = rule.getPseudoPage(); if ( rulePseudoPage != null ) { for ( int j = 0; j < pseudoPages.length; j++ ) { final PseudoPage pseudoPage = pseudoPages[ j ]; if ( pseudoPage.toString().equalsIgnoreCase( rulePseudoPage ) ) { rules.add( rule ); } } continue; } rules.add( rule ); } return (CSSPageRule[]) rules.toArray( new CSSPageRule[ rules.size() ] ); } }