themeinstaller/source/src/com/nokia/tools/themeinstaller/cssparser/CSSDOMProcessor.java
author Pat Downey <patd@symbian.org>
Wed, 01 Sep 2010 12:32:13 +0100
branchRCL_3
changeset 18 04b7640f6fb5
parent 0 05da4621cfb2
permissions -rw-r--r--
Revert incorrect RCL_3 drop: Revision: 201032 Kit: 201035

/*
* Copyright (c) 2007 Nokia Corporation and/or its subsidiary(-ies). 
* All rights reserved.
* This component and the accompanying materials are made available
* under the terms of "Eclipse Public License v1.0"
* which accompanies this distribution, and is available
* at the URL "http://www.eclipse.org/legal/epl-v10.html".
*
* Initial Contributors:
* Nokia Corporation - initial contribution.
*
* Contributors:
*
* Description:  CSSDOMProcessor applies given CSSRules to DOM Document.
 *
*/


package com.nokia.tools.themeinstaller.cssparser;

import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Vector;

import org.w3c.css.sac.LexicalUnit;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.traversal.DocumentTraversal;
import org.w3c.dom.traversal.NodeFilter;
import org.w3c.dom.traversal.NodeIterator;

/**
 * CSSDOMProcessor applies given CSSRules to DOM Document.
 */
public class CSSDOMProcessor
    {
    /** The Constant STRING_PROPERTY. */
    public static final String STRING_PROPERTY = "styleproperty";

    /** The Constant STRING_PSEUDOCLASS. */
    public static final String STRING_PSEUDOCLASS = "pseudoclass";

    /** The Constant STRING_NAME. */
    public static final String STRING_NAME = "name";

    /** The Constant STRING_VALUE. */
    public static final String STRING_VALUE = "value";

    /** The Constant STRING_INHERIT. */
    public static final String STRING_INHERIT = "inherit";

    /** The Constant CHAR_COLON. */
    private static final char CHAR_COLON = ':';

    /** The Constant STRING_SEPARATOR. */
    private static final String STRING_SEPARATOR = "|";

    /** The Constant PSEUDO_TABLE. */
    private static final String[] UNSTYLABLE_ELEMENTS_TABLE = { "styleproperty" };

    /** The Element type resolver. */
    private static ElementTypeResolver iElementTypeResolver = new ElementTypeResolver();

    /** The Constant INHERITABLE_PROPERTIES. */
    public static final String[] INHERITABLE_PROPERTIES = { "visibility",
            "block-progression", "direction", "color", "font-family",
            "font-size", "font-weight", "font-style", "_s60-tab-style",
            "_s60-tab-color" };

    /** The Match Maker. */
    private CSSMatchMaker iCSSMatchMaker;

    /**
     * Instantiates a new CSSDOM processor.
     */
    public CSSDOMProcessor()
        {
        iCSSMatchMaker = new CSSMatchMaker();
        }

    /**
     * Sort rules.
     *
     * @param rules The rules to be sorted by priority
     */
    private static void sortRules( Vector rules )
        {
        Collections.sort( rules );
        }

    /**
     * Checks if is stylable element.
     *
     * @param aElement The element to be checked
     *
     * @return true, if is stylable element
     */
    private static boolean isStylableElement(Element aElement){

    for ( int i = 0; i < UNSTYLABLE_ELEMENTS_TABLE.length; i++ )
        {
        if ( UNSTYLABLE_ELEMENTS_TABLE[ i ].equals( aElement.getTagName() ))
            {
            return false;
            }
        }

        return true;
    }

    /**
     * Apply style rule to DOM. Walks through the DOM tree elements and finds
     * those that match with the CSSRule Selector. If matching element is found,
     * Rule is applied to DOM Element.
     *
     * @param aDocument The DOM document to apply the rules
     * @param aRuleList List of CSS style rules
     */
    public void applyRulesToDom( Document aDocument,
            Vector aRuleList )
        {
        DocumentTraversal traversal = ( DocumentTraversal ) aDocument;

        NodeIterator iterator = traversal.createNodeIterator( aDocument
                .getDocumentElement(), NodeFilter.SHOW_ELEMENT, null, true );

        for ( Node node = iterator.nextNode(); node != null; node = iterator
                .nextNode() )
            {
            Element element = ( Element ) node;
                if ( isStylableElement(element) )
                {
                Vector rulesMatch = new Vector();

                for ( int i = 0; i < aRuleList.size(); i++ )
                    {
                	
                    CSSRule rule = ( CSSRule ) aRuleList.get( i );
                    
                    if ( iCSSMatchMaker.match( rule, element ) )
                        {
                        rulesMatch.add( rule );
                        }
                    }

                sortRules( rulesMatch );

                for ( int j = 0; j < rulesMatch.size(); j++ )
                    {
                    CSSRule matchingRule = ( CSSRule ) rulesMatch.elementAt( j );
//                    if(matchingRule.getSelector().toString().equals("box:edit")){
//                    	System.out.println("Party");
//                    }
                    
                    applyRuleToElement( element, matchingRule );
                    }
                }
            }
            applyInheritance( aDocument );
        }


    /**
     * Checks if given property is inheritable property.
     *
     * @param aProperty the a property
     *
     * @return true, if is inheritable property
     */
    public boolean isInheritableProperty( String aProperty )
        {
        for ( int i = 0; i < INHERITABLE_PROPERTIES.length; i++ )
            {
            if ( aProperty.equals( INHERITABLE_PROPERTIES[ i ] ) )
                {
                return true;
                }
            }
        return false;
        }

    /**
     * Checks if given element can inherit given property name.
     *
     * @param aElement the a element
     * @param aPropertyName the a value's name
     *
     * @return true, if element can inherit the property
     */
    public boolean canInherit( Element aElement, String aPropertyName )
        {
        return canInherit( aElement, aPropertyName, null );
        }

    /**
     * Checks if given element can inherit given property.
     *
     * If property value equals "inherit", the property
     * is interpreted as inherited.
     *
     * If property is inheritable and element can inherit
     * properties, the element can inherit the property.
     *
     * @param aElement the a element
     * @param aPropertyName the a property's name
     * @param aPropertyValue the a property's value
     *
     * @return true, if element can inherit the property
     */
    public boolean canInherit( Element aElement, String aPropertyName,
            String aPropertyValue )
        {
        if ( aPropertyValue != null && aPropertyValue.equals( STRING_INHERIT ) )
            {
            return true;
            }

        if ( !isInheritableProperty( aPropertyName ) )
            {
            return false;
            }

        if ( iElementTypeResolver.canInherit( aElement.getNodeName() ) )
            {
            return true;
            }

        return false;
        }

    /**
     * Apply inheritance for stylable nodes that can inherit properties from
     * parent elements.
     *
     * @param aDocument The DOM document to be modified
     */
    private void applyInheritance( Document aDocument )
        {
        DocumentTraversal traversal = ( DocumentTraversal ) aDocument;

        NodeIterator iterator = traversal.createNodeIterator( aDocument
                .getDocumentElement(), NodeFilter.SHOW_ELEMENT, null, true );

        for ( Node node = iterator.nextNode(); node != null; node = iterator
                .nextNode() )
            {
            Element element = ( Element ) node;
//            if(node.getLocalName().equals("box"))
//            		System.out.println("Kill me!!!");
            if ( isStylableElement( element ) )
                {
                if ( iElementTypeResolver.canInherit( element.getNodeName() ) )
                    {
                    applyInheritance( element );
                    }
                }
            }
        }

    /**
     * Apply inheritance for DOM Element. Checks if any of the elements parents
     * has properties that can be inherited in the given element. If such
     * property is found, new property to the element is added, unless there
     * already exists a property with the same name.
     *
     * @param aElement The element to be modified
     */
    private void applyInheritance( Element aElement )
        {
        for ( int i = 0; i < INHERITABLE_PROPERTIES.length; i++ )
            {
            String inheritableProperty = INHERITABLE_PROPERTIES[ i ];
            if ( iElementTypeResolver.canInherit( aElement.getNodeName() ) )
                {
                if ( !hasAttribute( aElement, inheritableProperty ) )
                    {
                    if ( hasParentElementWithAttribute( aElement,
                            inheritableProperty ) )
                        {
                        if ( hasChildElementWithAttribute( aElement,
                                STRING_PROPERTY, inheritableProperty ) == null )
                            {
                            addNewChildElement( aElement, STRING_PROPERTY,
                                    inheritableProperty,
                                    LexicalUnit.SAC_IDENT
                                            + STRING_SEPARATOR + STRING_INHERIT );
                            }
                        }
                    }
                }
            }
        }

    /**
     * Checks for parent element with attribute name.
     *
     * @param aElement The element that's parents are checked
     * @param aAttributeName The name of the property
     *
     * @return true, aElement has parent node with given attribute name
     */
    private static boolean hasParentElementWithAttribute( Element aElement,
            String aAttributeName )
        {
        Node n = aElement.getParentNode();
        while ( n != null )
            {
            if ( n.getNodeType() == Node.ELEMENT_NODE )
                {
                if ( hasChildElementWithAttribute( ( Element ) n,
                        STRING_PROPERTY, aAttributeName ) != null )
                    {
                    return true;
                    }
                }
            n = n.getParentNode();
            }
        return false;
        }

    /**
     * Apply rule to DOM Element. Creates child elements for aElement with style
     * data. If the element already has matching child element, the value of it
     * is overwritten.
     *
     * @param aElement The DOM Element
     * @param aRule CSS style rule
     */
    private void applyRuleToElement( Element aElement, CSSRule aRule )
        {
        HashMap styleMap = aRule.getStyleMap();

        Iterator itKeys = styleMap.keySet().iterator();
        while ( itKeys.hasNext() )
            {
            String keyName = ( String ) itKeys.next();

            // Case 1 : Rule has pseudo selector, add or replace Element
            if ( aRule.isPseudo() )
                {
                String selectorString = aRule.getSelector().toString();
                String pseudoPart = selectorString.substring( selectorString
                        .lastIndexOf( CHAR_COLON ) + 1 );

                Vector elementsAttributes = new Vector();
                elementsAttributes.add( new NameValuePair(STRING_NAME, keyName) );
                elementsAttributes.add( new NameValuePair(STRING_PSEUDOCLASS, pseudoPart) );

                Element e = hasChildElementWithAttributes( aElement,
                        STRING_PROPERTY, elementsAttributes );
                if ( e != null && hasAttribute( e, STRING_NAME, keyName ) )
                    {

                    CSSStyleProperty property = ( CSSStyleProperty ) styleMap
                            .get( keyName );
                    Vector values = property.getValues();

                    CSSPropertyValue propertyValue = ( CSSPropertyValue ) values
                            .elementAt( 0 );

                    // 1st value has name "value"
                    e.setAttribute( STRING_VALUE, propertyValue
                            .getValueTypeAndValue() );

                    // if there are more values, their name is set to "value1,
                    // value 2, ..."
                    for ( int i = 1; i < values.size(); i++ )
                        {

                        propertyValue = ( CSSPropertyValue ) values
                                .elementAt( i );

                        e.setAttribute( STRING_VALUE + i, propertyValue
                                .getValueTypeAndValue() );
                        }
                    }
                else
                    {

                    CSSStyleProperty property = ( CSSStyleProperty ) styleMap
                            .get( keyName );
                    Vector values = property.getValues();

                    CSSPropertyValue propertyValue = ( CSSPropertyValue ) values
                            .elementAt( 0 );

                    Element newElement = addNewChildElement( aElement,
                            STRING_PROPERTY, keyName, propertyValue
                                    .getValueTypeAndValue(), pseudoPart );

                    // Rest of the values are set to element we just created
                    for ( int i = 1; i < values.size(); i++ )
                        {
                        propertyValue = ( CSSPropertyValue ) values
                                .elementAt( i );
                        newElement.setAttribute( STRING_VALUE + i,
                                propertyValue.getValueTypeAndValue() );
                        }
                    }
                }
            // Case 2 : Rule don't have pseudo selector, add or replace Element
            else
                {
                Vector elementsAttributes = new Vector();
                elementsAttributes.add( new NameValuePair(STRING_NAME, keyName) );

                Element e = hasChildElementWithAttributes( aElement,
                        STRING_PROPERTY, elementsAttributes );

                if ( e != null && hasAttribute( e, STRING_NAME, keyName ) )
                    {
                    CSSStyleProperty property = ( CSSStyleProperty ) styleMap
                            .get( keyName );
                    Vector values = property.getValues();

                    CSSPropertyValue propertyValue = ( CSSPropertyValue ) values
                            .elementAt( 0 );

                    // 1st value has name "value"
                    e.setAttribute( STRING_VALUE, propertyValue
                            .getValueTypeAndValue() );

                    // if there are more values, their name is set to "value1,
                    // value 2, ..."
                    for ( int i = 1; i < values.size(); i++ )
                        {
                        propertyValue = ( CSSPropertyValue ) values
                                .elementAt( i );
                        e.setAttribute( STRING_VALUE + i, propertyValue
                                .getValueTypeAndValue() );
                        }
                    }
                else
                    {

                    CSSStyleProperty property = ( CSSStyleProperty ) styleMap
                            .get( keyName );
                    Vector values = property.getValues();

                    CSSPropertyValue propertyValue = ( CSSPropertyValue ) values
                            .elementAt( 0 );

                    Element newElement = addNewChildElement( aElement,
                            STRING_PROPERTY, keyName, propertyValue
                                    .getValueTypeAndValue() );

                    // Rest of the values are set to element we just created
                    for ( int i = 1; i < values.size(); i++ )
                        {
                        propertyValue = ( CSSPropertyValue ) values
                                .elementAt( i );
                        newElement.setAttribute( STRING_VALUE + i,
                                propertyValue.getValueTypeAndValue() );
                        }
                    }
                }
            }
        }

    /**
     * Checks if DOM Element has a attribute with given value.
     *
     * @param aElement The DOM element to check
     * @param aValue Attributes value
     *
     * @return true, if successful
     */
    private static boolean hasAttribute( Element aElement, String aValue )
        {
        return hasAttribute( aElement, null, aValue );
        }


    /**
     * Checks if DOM Element has a attribute with given name and value.
     * Attribute's name can be null, when only the attributes value is checked
     *
     * @param aElement The DOM element to check
     * @param aName Attributes name
     * @param aValue Attributes value
     *
     * @return true, if successful
     */
    private static boolean hasAttribute( Element aElement, String aName,
            String aValue )
        {
        if ( aElement == null )
            {
            return false;
            }
        if ( aElement.hasAttributes() )
            {
            NamedNodeMap nnm = aElement.getAttributes();
            for ( int i = 0; i < nnm.getLength(); i++ )
                {
                Node att = nnm.item( i );
                if ( aName != null )
                    {
                    if ( att.getNodeValue().equals( aValue )
                            && att.getNodeName().equals( aName ) )
                        {
                        return true;
                        }
                    }
                else
                    {
                    if ( att.getNodeValue().equals( aValue ) )
                        {
                        return true;
                        }
                    }
                }
            }
        return false;
        }

    /**
     * Checks for elements attributes.
     *
     * @param aElement The element to be checked
     * @param aAttributes Attributes in name-value pairs
     *
     * @return true, if element has attributes given in aAttributes
     */
    private static boolean hasAttributes( Element aElement, Vector aAttributes )
        {
        if ( aElement == null )
            {
            return false;
            }

        if ( aElement.hasAttributes() )
            {

            for ( int i = 0; i < aAttributes.size(); i++ )
                {

                NameValuePair nameValuePair = ( NameValuePair ) aAttributes
                        .get( i );

                String name = nameValuePair.getName();
                String value = nameValuePair.getValue();

                if ( !hasAttribute( aElement, name, value ) )
                    {
                    return false;
                    }
                }
            }
        return true;
        }

    /**
     * Checks for child element with attribute's name.
     *
     * @param aParent The parent element
     * @param aElementName The element tag name to be matched with child nodes
     *            name
     * @param aName Attributes name to be matched
     *
     * @return Child element with attribute, null if none
     */
    private static Element hasChildElementWithAttribute( Element aParent,
            String aElementName, String aName )
        {
        for ( Node n = aParent.getFirstChild(); n != null; n = n
                .getNextSibling() )
            {
            if ( n.getNodeType() == Node.ELEMENT_NODE )
                {
                if ( hasAttribute( ( Element ) n, aName )
                        && n.getNodeName().equals( aElementName ) )
                    {
                    return ( Element ) n;
                    }
                }
            }
        return null;
        }



    /**
     * Checks for child element with attributes.
     *
     * @param aParent The parent element
     * @param aElementName The element tag name to be matched with child nodes
     *            name
     * @param aNameValuePairs The Name-Value pairs for attributes that need to be
     *            found
     *
     * @return Child element with attributes, null if none
     */
    private static Element hasChildElementWithAttributes( Element aParent,
            String aElementName, Vector aNameValuePairs )
        {
        for ( Node n = aParent.getFirstChild(); n != null; n = n
                .getNextSibling() )
            {
            if ( n.getNodeType() == Node.ELEMENT_NODE )
                {
                if ( n.getNodeName().equals( aElementName )
                        && hasAttributes( ( Element ) n, aNameValuePairs ) )
                    {
                    return ( Element ) n;
                    }
                }
            }
        return null;
        }

    /**
     * Adds the new node to DOM.
     *
     * @param aParent The parent of the new node
     * @param aElementName Element name for new DOM node
     * @param aKeyName Attribute name for new DOM node
     * @param aKeyValue Attribute value for new DOM node
     */
    private Element addNewChildElement( Element aParent,
            String aElementName, String aKeyName, String aKeyValue )
        {
        return addNewChildElement( aParent, aElementName, aKeyName, aKeyValue, null );
        }

    /**
     * Adds the new node to DOM.
     *
     * @param aParent The parent of the new node
     * @param aElementName Element name for new DOM node
     * @param aKeyName Attribute name for new DOM node
     * @param aKeyValue Attribute value for new DOM node
     * @param aPseudo Pseudo attribute value for the DOM node
     */
    private Element addNewChildElement( Element aParent, String aElementName,
            String aKeyName, String aKeyValue, String aPseudo )
        {
        Document document = aParent.getOwnerDocument();

        String namespaceUri = aParent.getNamespaceURI();

        Element newElement = document.createElementNS( namespaceUri,
                aElementName );

        if ( aPseudo != null )
            {
            newElement.setAttribute( STRING_PSEUDOCLASS, aPseudo );
            }
        newElement.setAttribute( STRING_NAME, aKeyName );
        newElement.setAttribute( STRING_VALUE, aKeyValue );

        aParent.appendChild( newElement );
        return newElement;
        }


    /**
     * Class for temporarily store Node's attribute name and value.
     */
    private class NameValuePair
        {
        private String iName;

        private String iValue;

        NameValuePair( String aName, String aValue )
            {
            iName = aName;
            iValue = aValue;
            }

        public String getName()
            {
            return iName;
            }

        public String getValue()
            {
            return iValue;
            }

        }

    }