/* * Copyright 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.gwt.resources.css.ast; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Stack; /** * Clones CssNodes. */ public class CssNodeCloner extends CssVisitor { /** * Clone a list of nodes. */ public static <T extends CssNode> List<T> clone(Class<T> clazz, List<T> nodes) { // Push a fake context that will contain the cloned node List<CssNode> topContext = new ArrayList<CssNode>(); final List<CssProperty> topProperties = new ArrayList<CssProperty>(); final List<CssSelector> topSelectors = new ArrayList<CssSelector>(); CssNodeCloner cloner = new CssNodeCloner(); cloner.curentNodes.push(topContext); cloner.currentHasProperties = new HasProperties() { public List<CssProperty> getProperties() { return topProperties; } }; cloner.currentHasSelectors = new HasSelectors() { public List<CssSelector> getSelectors() { return topSelectors; } }; // Process the nodes cloner.accept(nodes); /* * Return the requested data. Different AST nodes will have been collected * in different parts of the initial context. */ List<?> toIterate; if (CssProperty.class.isAssignableFrom(clazz)) { toIterate = topProperties; } else if (CssSelector.class.isAssignableFrom(clazz)) { toIterate = topSelectors; } else { toIterate = topContext; } assert toIterate.size() == nodes.size() : "Wrong number of nodes in top " + "context. Expecting: " + nodes.size() + " Found: " + toIterate.size(); // Create the final return value List<T> toReturn = new ArrayList<T>(toIterate.size()); for (Object node : toIterate) { assert clazz.isInstance(node) : "Return type mismatch. Expecting: " + clazz.getName() + " Found: " + node.getClass().getName(); // Cast to the correct type to avoid an unchecked generic cast toReturn.add(clazz.cast(node)); } return toReturn; } /** * Clone a single node. */ public static <T extends CssNode> T clone(Class<T> clazz, T node) { return clone(clazz, Collections.singletonList(node)).get(0); } private HasProperties currentHasProperties; private HasSelectors currentHasSelectors; private final Stack<List<CssNode>> curentNodes = new Stack<List<CssNode>>(); private CssNodeCloner() { } @Override public void endVisit(CssMediaRule x, Context ctx) { popNodes(x); } @Override public void endVisit(CssNoFlip x, Context ctx) { popNodes(x); } @Override public void endVisit(CssStylesheet x, Context ctx) { popNodes(x); } @Override public boolean visit(CssDef x, Context ctx) { CssDef newDef = new CssDef(x.getKey()); newDef.getValues().addAll(x.getValues()); addToNodes(newDef); return true; } @Override public boolean visit(CssEval x, Context ctx) { assert x.getValues().size() == 1; assert x.getValues().get(0).isExpressionValue() != null; String value = x.getValues().get(0).isExpressionValue().getExpression(); CssEval newEval = new CssEval(x.getKey(), value); addToNodes(newEval); return true; } @Override public boolean visit(CssExternalSelectors x, Context ctx) { CssExternalSelectors newExternals = new CssExternalSelectors(); newExternals.getClasses().addAll(x.getClasses()); addToNodes(newExternals); return true; } /** * A CssIf has two lists of nodes, so we want to handle traversal in this * visitor. */ @Override public boolean visit(CssIf x, Context ctx) { CssIf newIf = new CssIf(); if (x.getExpression() != null) { newIf.setExpression(x.getExpression()); } else { newIf.setProperty(x.getPropertyName()); String[] newValues = new String[x.getPropertyValues().length]; System.arraycopy(x.getPropertyValues(), 0, newValues, 0, newValues.length); newIf.setPropertyValues(newValues); newIf.setNegated(x.isNegated()); } // Handle the "then" part pushNodes(newIf); accept(x.getNodes()); popNodes(x, newIf); /* * Push the "else" part as though it were its own node, but don't add it as * its own top-level node. */ CollapsedNode oldElseNodes = new CollapsedNode(x.getElseNodes()); CollapsedNode newElseNodes = new CollapsedNode(newIf.getElseNodes()); pushNodes(newElseNodes, false); accept(oldElseNodes); popNodes(oldElseNodes, newElseNodes); return false; } @Override public boolean visit(CssMediaRule x, Context ctx) { CssMediaRule newRule = new CssMediaRule(); newRule.getMedias().addAll(newRule.getMedias()); pushNodes(newRule); return true; } @Override public boolean visit(CssNoFlip x, Context ctx) { pushNodes(new CssNoFlip()); return true; } @Override public boolean visit(CssPageRule x, Context ctx) { CssPageRule newRule = new CssPageRule(); newRule.setPseudoPage(x.getPseudoPage()); addToNodes(newRule); return true; } @Override public boolean visit(CssProperty x, Context ctx) { CssProperty newProperty = new CssProperty(x.getName(), x.getValues(), x.isImportant()); currentHasProperties.getProperties().add(newProperty); return true; } @Override public boolean visit(CssRule x, Context ctx) { CssRule newRule = new CssRule(); addToNodes(newRule); return true; } @Override public boolean visit(CssSelector x, Context ctx) { CssSelector newSelector = new CssSelector(x.getSelector()); currentHasSelectors.getSelectors().add(newSelector); return true; } @Override public boolean visit(CssSprite x, Context ctx) { CssSprite newSprite = new CssSprite(); newSprite.setResourceFunction(x.getResourceFunction()); addToNodes(newSprite); return true; } @Override public boolean visit(CssStylesheet x, Context ctx) { CssStylesheet newSheet = new CssStylesheet(); pushNodes(newSheet); return true; } @Override public boolean visit(CssUrl x, Context ctx) { assert x.getValues().size() == 1; assert x.getValues().get(0).isDotPathValue() != null; CssUrl newUrl = new CssUrl(x.getKey(), x.getValues().get(0).isDotPathValue()); addToNodes(newUrl); return true; } @Override public boolean visit(CssUnknownAtRule x, Context ctx) { CssUnknownAtRule newRule = new CssUnknownAtRule(x.getRule()); addToNodes(newRule); return true; } /** * Add a cloned node instance to the output. */ private void addToNodes(CssNode node) { curentNodes.peek().add(node); currentHasProperties = node instanceof HasProperties ? (HasProperties) node : null; currentHasSelectors = node instanceof HasSelectors ? (HasSelectors) node : null; } /** * Remove a frame. * * @param original the node that was being cloned so that validity checks may * be performed */ private List<CssNode> popNodes(HasNodes original) { List<CssNode> toReturn = curentNodes.pop(); if (toReturn.size() != original.getNodes().size()) { throw new RuntimeException("Insufficient number of nodes for a " + original.getClass().getName() + " Expected: " + original.getNodes().size() + " Found: " + toReturn.size()); } return toReturn; } /** * Remove a frame. * * @param original the node that was being cloned so that validity checks may * be performed * @param expected the HasNodes whose nodes were being populated by the frame * being removed. */ private List<CssNode> popNodes(HasNodes original, HasNodes expected) { List<CssNode> toReturn = popNodes(original); if (toReturn != expected.getNodes()) { throw new RuntimeException("Incorrect parent node list popped"); } return toReturn; } /** * Push a new frame, adding the new parent as a child of the current parent. */ private <T extends CssNode & HasNodes> void pushNodes(T parent) { pushNodes(parent, true); } /** * Push a new frame. * * @param addToNodes if <code>true</code> add the new parent node as a child * of the current parent. */ private <T extends CssNode & HasNodes> void pushNodes(T parent, boolean addToNodes) { if (addToNodes) { addToNodes(parent); } this.curentNodes.push(parent.getNodes()); } }