All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cz.vutbr.web.domassign.Analyzer Maven / Gradle / Ivy

Go to download

jStyleParser is a CSS parser written in Java. It has its own application interface that is designed to allow an efficient CSS processing in Java and mapping the values to the Java data types. It parses CSS 2.1 style sheets into structures that can be efficiently assigned to DOM elements. It is intended be the primary CSS parser for the CSSBox library. While handling errors, it is user agent conforming according to the CSS specification.

The newest version!
package cz.vutbr.web.domassign;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.traversal.NodeFilter;
import org.w3c.dom.traversal.TreeWalker;

import cz.vutbr.web.css.CSSFactory;
import cz.vutbr.web.css.CombinedSelector;
import cz.vutbr.web.css.Declaration;
import cz.vutbr.web.css.ElementMatcher;
import cz.vutbr.web.css.MatchCondition;
import cz.vutbr.web.css.MediaSpec;
import cz.vutbr.web.css.NodeData;
import cz.vutbr.web.css.RuleSet;
import cz.vutbr.web.css.Selector;
import cz.vutbr.web.css.Selector.PseudoElementType;
import cz.vutbr.web.css.StyleSheet;

/**
 * Analyzer allows to apply the given style to any document.
 * During the initialization, it divides rules of stylesheet into maps accoring to
 * medias and their type. Afterwards, it is able to return CSS declaration for any
 * DOM tree and media. It allows to use or not to use inheritance.
 * 
 * @author Karel Piwko 2008
 * @author Radek Burget 2008-2014
 * 
 */
public class Analyzer {

	private static final Logger log = LoggerFactory.getLogger(Analyzer.class);

	/** The style sheets to be processed. */
	protected List sheets;
	
	/** Rule order counter */
	protected int currentOrder;
	
	/**
	 * Holds maps of declared rules classified into groups of
	 * HolderItem (ID, CLASS, ELEMENT, OTHER).
	 */
	protected Holder rules;

	private MatchCondition matchCond;
	private ElementMatcher matcher;

	/**
	 * Creates the analyzer for a single style sheet.
	 * @param sheet The stylesheet that will be used as the source of rules.
	 */
	public Analyzer(StyleSheet sheet) {
	    sheets = new ArrayList(1);
	    sheets.add(sheet);
	    matchCond = CSSFactory.getDefaultMatchCondition();
	    matcher = CSSFactory.getElementMatcher();
	}

	/**
	 * Creates the analyzer for multiple style sheets.
	 * @param sheets A list of stylesheets that will be used as the source of rules.
	 */
	public Analyzer(List sheets) {
	    this.sheets = sheets;
        matchCond = CSSFactory.getDefaultMatchCondition();
        matcher = CSSFactory.getElementMatcher();
	}
	
	/**
	 * Registers a new match condition to be used for matching the elements and
	 * selector parts.
	 *
	 * @param matchCond
	 *            the new match condition
	 */
	public final void registerMatchCondition(MatchCondition matchCond) {
		this.matchCond = matchCond;
	}

	/**
	 * Obtains the match condition to be used for matching the elements and
	 * selector parts.
	 *
	 * @return the match condition used by the Analyzer.
	 */
	public final MatchCondition getMatchCondition() {
		return this.matchCond;
	}

    /**
     * Registers a new element matcher to be used for matching the elements and
     * selector parts.
     *
     * @param matcher
     *            the new element matcher
     */
    public final void registerElementMatcher(ElementMatcher matcher) {
        this.matcher = matcher;
    }

    /**
     * Obtains the matcher to be used for matching the elements.
     *
     * @return the matcher used by the Analyzer.
     */
    public final ElementMatcher getElementMatcher() {
        return matcher;
    }

	/**
	 * Evaluates CSS properties of DOM tree
	 * 
	 * @param doc
	 *            Document tree
	 * @param media
	 *            Media
	 * @param inherit
	 *            Use inheritance
	 * @return Map where each element contains its CSS properties
	 */
	public StyleMap evaluateDOM(Document doc, MediaSpec media, final boolean inherit) {

		DeclarationMap declarations = assingDeclarationsToDOM(doc, media, inherit);

		StyleMap nodes = new StyleMap(declarations.size());

		Traversal traversal = new Traversal(
				doc, (Object) declarations, NodeFilter.SHOW_ELEMENT) {
			
			@Override
			protected void processNode(StyleMap result, Node current, Object source) {

			    NodeData main = CSSFactory.createNodeData();
			    
				// for all declarations available in the main list (pseudo=null)
				List declarations = ((DeclarationMap) source).get((Element) current, null);
				if (declarations != null) 
				{
					for (Declaration d : declarations) {
						main.push(d);
					}
					if (inherit)
						main.inheritFrom(result.get((Element) walker.parentNode(), null));
				}
				// concretize values and store them
				result.put((Element) current, null, main.concretize());
				
				//repeat for the pseudo classes (if any)
				for (PseudoElementType pseudo : ((DeclarationMap) source).pseudoSet((Element) current))
				{
				    NodeData pdata = CSSFactory.createNodeData();
	                declarations = ((DeclarationMap) source).get((Element) current, pseudo);
	                if (declarations != null) 
	                {
	                    for (Declaration d : declarations) {
	                        pdata.push(d);
	                    }
                        pdata.inheritFrom(main); //always inherit from the main element style
	                }
	                // concretize values and store them
	                result.put((Element) current, pseudo, pdata.concretize());
				}
				
			}
		};

		traversal.levelTraversal(nodes);

		return nodes;
	}

   public StyleMap evaluateDOM(Document doc, String media, final boolean inherit) {
       return evaluateDOM(doc, new MediaSpec(media), inherit);
   }

	/**
	 * Creates map of declarations assigned to each element of a DOM tree
	 * 
	 * @param doc
	 *            DOM document
	 * @param media
	 *            Media type to be used for declarations
	 * @param inherit
	 *            Inheritance (cascade propagation of values)
	 * @return Map of elements as keys and their declarations
	 */
	protected DeclarationMap assingDeclarationsToDOM(Document doc, MediaSpec media, final boolean inherit) {

		// classify the rules
	    classifyAllSheets(media);
		
		// resulting map
		DeclarationMap declarations = new DeclarationMap();
		
        // if the holder is empty skip evaluation
        if(rules!=null && !rules.isEmpty()) {
            
    		Traversal traversal = new Traversal(
    				doc, (Object) rules, NodeFilter.SHOW_ELEMENT) {
    			protected void processNode(DeclarationMap result,
    					Node current, Object source) {
    				assignDeclarationsToElement(result, walker, (Element) current,
    						(Holder) source);
    			}
    		};
    
    		// list traversal will be enough
    		if (!inherit)
    			traversal.listTraversal(declarations);
    		// we will do level traversal to economize blind returning
    		// in tree
    		else
    			traversal.levelTraversal(declarations);
        }

		return declarations;
	}

	/**
	 * Assigns declarations to one element.
	 * 
	 * @param declarations
	 *            Declarations of all processed elements
	 * @param walker
	 *            Tree walker
	 * @param e
	 *            DOM Element
	 * @param holder
	 *            Wrap
	 */
	protected void assignDeclarationsToElement(
			DeclarationMap declarations, TreeWalker walker,
			Element e, Holder holder) {

		if(log.isDebugEnabled()) {
			log.debug("Traversal of {} {}.", e.getNodeName(), e.getNodeValue());
		}
		
		// create set of possible candidates applicable to given element
		// set is automatically filtered to not contain duplicates
		Set candidates = new HashSet();

		// match element classes
		for (String cname : matcher.elementClasses(e)) {
			// holder contains rule with given class
			List rules = holder.get(HolderItem.CLASS, cname.toLowerCase());
			if (rules != null)
				candidates.addAll(rules);
		}
		log.trace("After CLASSes {} total candidates.", candidates.size());

		// match IDs
		String id = matcher.elementID(e);
		if (id != null && id.length() != 0) {
			List rules = holder.get(HolderItem.ID, id.toLowerCase());
			if (rules != null)
				candidates.addAll(rules);
		}
		log.trace("After IDs {} total candidates.", candidates.size());
		
		// match elements
		String name = matcher.elementName(e);
		if (name != null) {
			List rules = holder.get(HolderItem.ELEMENT, name.toLowerCase());
			if (rules != null)
				candidates.addAll(rules);
		}
		log.trace("After ELEMENTs {} total candidates.", candidates.size());

		// others
		candidates.addAll(holder.get(HolderItem.OTHER, null));
		
	    // transform to list to speed up traversal
		// and sort rules in order as they were found in CSS definition
		List clist = new ArrayList(candidates);
		Collections.sort(clist);
		
		log.debug("Totally {} candidates.", candidates.size());
		log.trace("With values: {}", clist);

		// resulting list of declaration for this element with no pseudo-selectors (main list)(local cache)
		List eldecl = new ArrayList();
		
		// existing pseudo selectors found
		Set pseudos = new HashSet<>();

		// for all candidates
		for (OrderedRule orule : clist) {
		    
			final RuleSet rule = orule.getRule();
			StyleSheet sheet = rule.getStyleSheet();
			if (sheet == null)
			    log.warn("No source style sheet set for rule: {}", rule.toString());
			StyleSheet.Origin origin = (sheet == null) ? StyleSheet.Origin.AGENT : sheet.getOrigin();
			
			// for all selectors inside
			for (CombinedSelector s : rule.getSelectors()) {
				// this method does automatic rewind of walker
				if (!matchSelector(s, e, walker)) {
					log.trace("CombinedSelector \"{}\" NOT matched!", s);
					continue;
				}

				log.trace("CombinedSelector \"{}\" matched", s);
				
				PseudoElementType pseudo = s.getPseudoElementType();
                CombinedSelector.Specificity spec = s.computeSpecificity();
				if (pseudo == null)
				{
    				// add to main list
    				for (Declaration d : rule)
    					eldecl.add(new AssignedDeclaration(d, spec, origin));
				}
				else
				{
				    // remember the pseudo element
				    pseudos.add(pseudo);
				    // add to pseudo lists
                    for (Declaration d : rule)
                        declarations.addDeclaration(e, pseudo, new AssignedDeclaration(d, spec, origin));
				}

			}
		}

		// sort declarations
		Collections.sort(eldecl); //sort the main list
		log.debug("Sorted {} declarations.", eldecl.size());
		log.trace("With values: {}", eldecl);
		for (PseudoElementType p : pseudos)
		    declarations.sortDeclarations(e, p); //sort pseudos
		
		// set the main list
		declarations.put(e, null, eldecl);
	}

	protected boolean elementSelectorMatches(final Selector s, final Element e) {
		return s.matches(e, matcher, matchCond);
	}

	protected boolean matchSelector(CombinedSelector sel, Element e, TreeWalker w) {

		// store current walker position
		Node current = w.getCurrentNode();

		boolean retval = false;
		Selector.Combinator combinator = null;
		// traverse simple selector backwards
		for (int i = sel.size() - 1; i >= 0; i--) {
			// last simple selector
			Selector s = sel.get(i);
			//log.trace("Iterating loop with selector {}, combinator {}",	s, combinator);

			// decide according to combinator anti-pattern
			if (combinator == null) {
				retval = this.elementSelectorMatches(s, e);
			} else if (combinator == Selector.Combinator.ADJACENT) {
				Element adjacent = (Element) w.previousSibling();
				retval = false;
				if (adjacent != null)
					retval = this.elementSelectorMatches(s, adjacent);
            } else if (combinator == Selector.Combinator.PRECEDING) {
                Element preceding;
                retval = false;
                while (!retval && (preceding = (Element) w.previousSibling()) != null) {
                    retval = this.elementSelectorMatches(s, preceding);
                }
			} else if (combinator == Selector.Combinator.DESCENDANT) {
                Element ancestor;
                retval = false;
                while (!retval && (ancestor = (Element) w.parentNode()) != null) {
                    retval = this.elementSelectorMatches(s, ancestor);
                }
			} else if (combinator == Selector.Combinator.CHILD) {
                Element parent = (Element) w.parentNode();
                retval = false;
                if (parent != null)
                    retval = this.elementSelectorMatches(s, parent);
			}

			// set combinator for next loop
			combinator = s.getCombinator();

			// leave loop if not matched
			if (!retval)
				break;
		}

		// restore walker position
		w.setCurrentNode(current);
		return retval;
	}

	/**
	 * Classifies the rules in all the style sheets.
	 * @param mediaspec The specification of the media for evaluating the media queries.
	 */
	protected void classifyAllSheets(MediaSpec mediaspec)
	{
	    rules = new Holder();

	    AnalyzerUtil.classifyAllSheets(sheets, rules, mediaspec);
	}
	
	/**
	 * Decides about holder item
	 * 
	 * @author kapy
	 */
	protected enum HolderItem {
		ELEMENT(0), ID(1), CLASS(2), OTHER(3);

		private int type;

		private HolderItem(int type) {
			this.type = type;
		}

		public int type() {
			return type;
		}
	}

	/**
	 * Holds holder item type and key value, that is two elements that are about
	 * to be used for storing in holder
	 * 
	 * @author kapy
	 * 
	 */
	protected static class HolderSelector {
		public HolderItem item;
		public String key;

		public HolderSelector(HolderItem item, String key) {
			this.item = item;
			this.key = key;
		}
	}

	/**
	 * Represents a ruleset and its order in the corresponding style sheet.
	 * 
	 * @author burgetr
	 */
	public static final class OrderedRule implements Comparable {
	    private final RuleSet rule;
        private final int order;
	    
        public OrderedRule(RuleSet rule, int order) {
            this.rule = rule;
            this.order = order;
        }

        public RuleSet getRule() {
            return rule;
        }

        public int getOrder() {
            return order;
        }

        public int compareTo(OrderedRule o) {
            return getOrder() - o.getOrder();
        }

        @Override
        public String toString() {
        	return "OR" + order + ", " + rule;
        }
        
	}
	
	/**
	 * Holds list of maps of list. This is used to classify rulesets into
	 * structure which is easily accessible by analyzator
	 * 
	 * @author kapy
	 * 
	 */
	public static class Holder {

		/** HolderItem.* except OTHER are stored there */
		private List>> items;

		/** OTHER rules are stored there */
		private List others;

		public Holder() {
			// create list of items
			this.items = new ArrayList>>(HolderItem.values().length - 1);

			// fill maps in list
			for (HolderItem hi : HolderItem.values()) {
				// this is special case, it is not map
				if (hi == HolderItem.OTHER)
					others = new ArrayList();
				else
					items.add(new HashMap>());
			}
		}

		public boolean isEmpty() {
			for(HolderItem hi: HolderItem.values()) {
				if(hi == HolderItem.OTHER) { 
					if(!others.isEmpty()) return false;
				}
				else if(!items.get(hi.type).isEmpty())
					return false;
			}			
			return true;
		}
		
		
		public static Holder union(Holder one, Holder two) {
			
			Holder union = new Holder();
			if(one==null) one = new Holder();
			if(two==null) two = new Holder();
			
			for(HolderItem hi: HolderItem.values()) {
				if(hi == HolderItem.OTHER) {
					union.others.addAll(one.others);
					union.others.addAll(two.others);
				}
				else {
					
					Map> oneMap, twoMap, unionMap;
					oneMap = one.items.get(hi.type);
					twoMap = two.items.get(hi.type);
					unionMap = union.items.get(hi.type);
					
					unionMap.putAll(oneMap);
					for(String key: twoMap.keySet()) {
						// map already contains this as key, append to list
						if(unionMap.containsKey(key)) {
							unionMap.get(key).addAll(twoMap.get(key));
						}
						// we could directly add elements
						else {
							unionMap.put(key, twoMap.get(key));
						}
					}
				}
				
			}
			return union;
		}
		
		/**
		 * Inserts Ruleset into group identified by HolderType, and optionally
		 * by key value
		 * 
		 * @param item
		 *            Identifier of holder's group
		 * @param key
		 *            Key, used in case other than OTHER
		 * @param value
		 *            Value to be store inside
		 */
		public void insert(HolderItem item, String key, OrderedRule value) {

			// check others and if so, insert item
			if (item == HolderItem.OTHER) {
				others.add(value);
				return;
			}

			// create list if empty
			Map> map = items.get(item.type);
			List list = map.get(key);
			if (list == null) {
				list = new ArrayList();
				map.put(key, list);
			}

			list.add(value);

		}

		/**
		 * Returns list of rules (ruleset) for given holder and key
		 * 
		 * @param item
		 *            Type of item to be returned
		 * @param key
		 *            Key or null in case of HolderItem.OTHER
		 * @return List of rules or null if not found under given
		 *         combination of key and item
		 */
		public List get(HolderItem item, String key) {

			// check others
			if (item == HolderItem.OTHER)
				return others;

			return items.get(item.type()).get(key);
		}
		
		
		public String contentCount(){
			StringBuilder sb = new StringBuilder();
			
			for(HolderItem hi: HolderItem.values()) {
				if(hi == HolderItem.OTHER) {
					sb.append(hi.name())
					  .append(": ")
					  .append(others.size())
					  .append(" ");
				}
				else {
					sb.append(hi.name())
					  .append(":")
					  .append(items.get(hi.type).size())
					  .append(" ");
				}
				
			}
			
			return sb.toString();
		}
		
		@Override
		public String toString() {
			StringBuilder sb = new StringBuilder();
			
			for(HolderItem hi: HolderItem.values()) {
				if(hi == HolderItem.OTHER) {
					sb.append(hi.name())
					  .append(" (")
					  .append(others.size())
					  .append("): ")
					  .append(others).append("\n");	
				}
				else {
					sb.append(hi.name())
					  .append(" (")
					  .append(items.get(hi.type).size())
					  .append("): ")
					  .append(items.get(hi.type)).append("\n");
				}
				
			}
			
			return sb.toString();
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy