/*!
* 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.engine.classic.core.style.css;
import org.pentaho.reporting.engine.classic.core.AttributeNames;
import org.pentaho.reporting.engine.classic.core.Element;
import org.pentaho.reporting.engine.classic.core.ReportElement;
import org.pentaho.reporting.engine.classic.core.Section;
import org.pentaho.reporting.engine.classic.core.style.ElementStyleSheet;
import org.pentaho.reporting.engine.classic.core.style.css.namespaces.NamespaceCollection;
import org.pentaho.reporting.engine.classic.core.style.css.namespaces.NamespaceDefinition;
import org.pentaho.reporting.engine.classic.core.style.css.selector.CSSSelector;
import org.pentaho.reporting.engine.classic.core.style.css.selector.SelectorWeight;
import org.pentaho.reporting.engine.classic.core.util.beans.BeanException;
import org.pentaho.reporting.engine.classic.core.util.beans.ConverterRegistry;
import org.pentaho.reporting.libraries.base.util.ObjectUtilities;
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.util.ArrayList;
import java.util.List;
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 ResourceManager resourceManager;
private ElementStyleRule[] activeStyleRules;
private ElementStyleRule[] activePseudoStyleRules;
private NamespaceCollection namespaces;
private DocumentContext context;
public SimpleStyleRuleMatcher() {
}
public void initialize( final DocumentContext layoutProcess ) {
if ( layoutProcess == null ) {
throw new NullPointerException();
}
this.context = layoutProcess;
this.resourceManager = layoutProcess.getResourceManager();
final ArrayList<CSSCounterRule> counterRules = new ArrayList<CSSCounterRule>();
final ArrayList<ElementStyleRule> styleRules = new ArrayList<ElementStyleRule>();
final DocumentContext dc = this.context;
namespaces = dc.getNamespaces();
if ( dc.getStyleResource() != null ) {
handleLinkNode( dc.getStyleResource(), styleRules, counterRules );
}
if ( dc.getStyleDefinition() != null ) {
handleStyleNode( dc.getStyleDefinition(), styleRules, counterRules );
}
activeStyleRules = styleRules.toArray( new ElementStyleRule[styleRules.size()] );
styleRules.clear();
for ( int i = 0; i < activeStyleRules.length; i++ ) {
final ElementStyleRule activeStyleRule = activeStyleRules[i];
if ( isPseudoElementRule( activeStyleRule ) == false ) {
continue;
}
styleRules.add( activeStyleRule );
}
activePseudoStyleRules = styleRules.toArray( new ElementStyleRule[styleRules.size()] );
}
private void handleLinkNode( final Object styleResource, final ArrayList<ElementStyleRule> styleRules,
final ArrayList<CSSCounterRule> counterRules ) {
// do some external parsing
// (Same as the <link> element of HTML)
try {
final String href = (String) styleResource;
final ResourceKey baseKey = context.getContextKey();
final ResourceKey derivedKey;
if ( baseKey == null ) {
derivedKey = resourceManager.createKey( href );
} else {
derivedKey = resourceManager.deriveKey( baseKey, String.valueOf( href ) );
}
final ElementStyleDefinition styleSheet = parseStyleSheet( derivedKey, null );
if ( styleSheet == null ) {
return;
}
addStyleRules( styleSheet, styleRules );
addCounterRules( styleSheet, counterRules );
} catch ( ResourceKeyCreationException e ) {
e.printStackTrace();
}
}
private void handleStyleNode( final ElementStyleDefinition node, final ArrayList<ElementStyleRule> styleRules,
final ArrayList<CSSCounterRule> counterRules ) {
addStyleRules( node, styleRules );
addCounterRules( node, counterRules );
}
private void addCounterRules( final ElementStyleDefinition styleSheet, final ArrayList<CSSCounterRule> 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 ElementStyleSheet rule = styleSheet.getRule( i );
if ( rule instanceof CSSCounterRule ) {
final CSSCounterRule drule = (CSSCounterRule) rule;
rules.add( drule );
}
}
}
private void addStyleRules( final ElementStyleDefinition styleSheet,
final ArrayList<ElementStyleRule> 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 ElementStyleSheet rule = styleSheet.getRule( i );
if ( rule instanceof ElementStyleRule ) {
final ElementStyleRule drule = (ElementStyleRule) rule;
activeStyleRules.add( drule );
}
}
}
private ElementStyleDefinition parseStyleSheet( final ResourceKey key, final ResourceKey context ) {
try {
final Resource resource = resourceManager.create( key, context, ElementStyleDefinition.class );
return (ElementStyleDefinition) resource.getResource();
} catch ( ResourceException e ) {
// Log.info("Unable to parse StyleSheet: " + e.getLocalizedMessage());
}
return null;
}
private boolean isPseudoElementRule( final ElementStyleRule rule ) {
final List<CSSSelector> selectorList = rule.getSelectorList();
for ( int i = 0; i < selectorList.size(); i += 1 ) {
final CSSSelector selector = selectorList.get( i );
if ( selector == null ) {
continue;
}
if ( selector.getSelectorType() != Selector.SAC_CONDITIONAL_SELECTOR ) {
continue;
}
final ConditionalSelector cs = (ConditionalSelector) selector;
final Condition condition = cs.getCondition();
if ( condition.getConditionType() != Condition.SAC_PSEUDO_CLASS_CONDITION ) {
continue;
}
return true;
}
return false;
}
public boolean isMatchingPseudoElement( final ReportElement element, final String pseudo ) {
for ( int i = 0; i < activePseudoStyleRules.length; i++ ) {
final ElementStyleRule activeStyleRule = activePseudoStyleRules[i];
final List<CSSSelector> selectorList = activeStyleRule.getSelectorList();
for ( int x = 0; x < selectorList.size(); x += 1 ) {
final CSSSelector selector = selectorList.get( x );
if ( selector instanceof ConditionalSelector == false ) {
continue;
}
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;
}
/**
* Returns all matching rules for the given element. Each matched rule must carry the weight of the matching selector.
*
* @param element
* @return
*/
public MatcherResult[] getMatchingRules( final ReportElement element ) {
final ArrayList<MatcherResult> retvals = new ArrayList<MatcherResult>();
for ( int i = 0; i < activeStyleRules.length; i++ ) {
final ElementStyleRule activeStyleRule = activeStyleRules[i];
final List<CSSSelector> selectorList = activeStyleRule.getSelectorList();
SelectorWeight weight = null;
for ( int x = 0; x < selectorList.size(); x += 1 ) {
final CSSSelector selector = selectorList.get( x );
if ( selector == null ) {
continue;
}
if ( isMatch( element, selector ) ) {
if ( weight == null ) {
weight = selector.getWeight();
} else {
if ( weight.compareTo( selector.getWeight() ) < 0 ) {
weight = selector.getWeight();
}
}
}
}
if ( weight != null ) {
retvals.add( new MatcherResult( weight, activeStyleRule ) );
}
}
// Log.debug ("Got " + retvals.size() + " matching rules for " +
// layoutContext.getTagName() + ":" +
// layoutContext.getPseudoElement());
return retvals.toArray( new MatcherResult[retvals.size()] );
}
private boolean isMatch( final ReportElement 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.getParentSection() == 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 false;
}
case Selector.SAC_ELEMENT_NODE_SELECTOR: {
final ElementSelector es = (ElementSelector) selector;
final String localName = es.getLocalName();
if ( localName != null ) {
if ( localName.equals( getTagName( node ) ) == false ) {
return false;
}
}
final String namespaceURI = es.getNamespaceURI();
if ( namespaceURI != null ) {
final String namespace = getNamespace( node );
if ( namespaceURI.equals( namespace ) == false ) {
return false;
}
}
return true;
}
case Selector.SAC_CHILD_SELECTOR: {
final DescendantSelector ds = (DescendantSelector) selector;
if ( isMatch( node, ds.getSimpleSelector() ) == false ) {
return false;
}
final ReportElement parent = node.getParentSection();
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;
}
}
private boolean evaluateCondition( final ReportElement 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;
final Object attr = queryAttribute( node, ac );
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;
String namespace = getNamespace( node );
if ( namespace == null ) {
namespace = namespaces.getDefaultNamespaceURI();
}
if ( namespace == null ) {
return false;
}
final NamespaceDefinition ndef = namespaces.getDefinition( namespace );
if ( ndef == null ) {
return false;
}
final String[] classAttribute = ndef.getClassAttribute( getTagName( node ) );
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( AttributeNames.Xml.NAMESPACE, AttributeNames.Xml.ID );
return ObjectUtilities.equal( ac.getValue(), id );
}
case Condition.SAC_LANG_CONDITION: {
final AttributeCondition ac = (AttributeCondition) condition;
final Locale locale = getLanguage( node );
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 Object o = queryAttribute( node, ac );
if ( o == null ) {
return false;
}
try {
final String attr = ConverterRegistry.toAttributeValue( o );
return isOneOfAttributes( attr, ac.getValue() );
} catch ( BeanException e ) {
return false;
}
}
case Condition.SAC_PSEUDO_CLASS_CONDITION: {
final AttributeCondition ac = (AttributeCondition) condition;
final String pseudoClass = getPseudoElement( node );
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: {
// any of these conditionals are not yet implemented. They are defined as part of the CSS standard.
return false;
}
}
}
private Object queryAttribute( final ReportElement node, final AttributeCondition ac ) {
final String namespaceURI = ac.getNamespaceURI();
final Object attr;
if ( namespaceURI == null ) {
attr = node.getFirstAttribute( ac.getLocalName() );
} else {
attr = node.getAttribute( namespaceURI, ac.getLocalName() );
}
return attr;
}
private String getPseudoElement( final ReportElement node ) {
// at the moment we do not support pseudo-elements.
return null;
}
private String getNamespace( final ReportElement node ) {
return AttributeNames.Core.NAMESPACE;
}
private String getTagName( final ReportElement node ) {
return node.getElementType().getMetaData().getName();
}
private Locale getLanguage( final ReportElement node ) {
return null;
}
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 ReportElement node, final Selector selector ) {
ReportElement parent = node.getParentSection();
while ( parent != null ) {
if ( isMatch( parent, selector ) ) {
return true;
}
parent = parent.getParentSection();
}
return false;
}
private boolean isSilblingMatch( final ReportElement node, final SiblingSelector select ) {
ReportElement pred = getPreviousReportElement( node );
while ( pred != null ) {
if ( isMatch( pred, select ) ) {
return true;
}
pred = getPreviousReportElement( pred );
}
return false;
}
private ReportElement getPreviousReportElement( final ReportElement e ) {
final Section parentSection = e.getParentSection();
if ( parentSection == null ) {
return null;
}
final int count = parentSection.getElementCount();
for ( int i = 0; i < count; i += 1 ) {
final Element element = parentSection.getElement( i );
if ( e == element ) {
if ( i == 0 ) {
return null;
} else {
return parentSection.getElement( i - 1 );
}
}
}
return null;
}
}