blob: 6e6353852c524cf6994db51fd34f77e91afa477f [file] [log] [blame]
/*
* Copyright 2014 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.rg;
import com.google.gwt.core.ext.BadPropertyValueException;
import com.google.gwt.core.ext.ConfigurationProperty;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.PropertyOracle;
import com.google.gwt.core.ext.SelectionProperty;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.util.Util;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.CssResource.ClassName;
import com.google.gwt.resources.client.CssResource.Import;
import com.google.gwt.resources.client.CssResource.ImportedWithPrefix;
import com.google.gwt.resources.client.CssResource.NotStrict;
import com.google.gwt.resources.client.CssResource.Shared;
import com.google.gwt.resources.client.ResourcePrototype;
import com.google.gwt.resources.converter.Css2Gss;
import com.google.gwt.resources.converter.Css2GssConversionException;
import com.google.gwt.resources.ext.ClientBundleRequirements;
import com.google.gwt.resources.ext.ResourceContext;
import com.google.gwt.resources.ext.ResourceGeneratorUtil;
import com.google.gwt.resources.ext.SupportsGeneratorResultCaching;
import com.google.gwt.resources.gss.CreateRuntimeConditionalNodes;
import com.google.gwt.resources.gss.CssPrinter;
import com.google.gwt.resources.gss.ExtendedEliminateConditionalNodes;
import com.google.gwt.resources.gss.ExternalClassesCollector;
import com.google.gwt.resources.gss.GwtGssFunctionMapProvider;
import com.google.gwt.resources.gss.ImageSpriteCreator;
import com.google.gwt.resources.gss.PermutationsCollector;
import com.google.gwt.resources.gss.RecordingBidiFlipper;
import com.google.gwt.resources.gss.RenamingSubstitutionMap;
import com.google.gwt.resources.gss.RuntimeConditionalBlockCollector;
import com.google.gwt.resources.gss.ValidateRuntimeConditionalNode;
import com.google.gwt.resources.rg.CssResourceGenerator.JClassOrderComparator;
import com.google.gwt.thirdparty.common.css.MinimalSubstitutionMap;
import com.google.gwt.thirdparty.common.css.PrefixingSubstitutionMap;
import com.google.gwt.thirdparty.common.css.SourceCode;
import com.google.gwt.thirdparty.common.css.SourceCodeLocation;
import com.google.gwt.thirdparty.common.css.SubstitutionMap;
import com.google.gwt.thirdparty.common.css.compiler.ast.CssDefinitionNode;
import com.google.gwt.thirdparty.common.css.compiler.ast.CssNumericNode;
import com.google.gwt.thirdparty.common.css.compiler.ast.CssTree;
import com.google.gwt.thirdparty.common.css.compiler.ast.CssValueNode;
import com.google.gwt.thirdparty.common.css.compiler.ast.ErrorManager;
import com.google.gwt.thirdparty.common.css.compiler.ast.GssError;
import com.google.gwt.thirdparty.common.css.compiler.ast.GssFunction;
import com.google.gwt.thirdparty.common.css.compiler.ast.GssParser;
import com.google.gwt.thirdparty.common.css.compiler.ast.GssParserException;
import com.google.gwt.thirdparty.common.css.compiler.passes.AbbreviatePositionalValues;
import com.google.gwt.thirdparty.common.css.compiler.passes.CheckDependencyNodes;
import com.google.gwt.thirdparty.common.css.compiler.passes.CollectConstantDefinitions;
import com.google.gwt.thirdparty.common.css.compiler.passes.CollectMixinDefinitions;
import com.google.gwt.thirdparty.common.css.compiler.passes.ColorValueOptimizer;
import com.google.gwt.thirdparty.common.css.compiler.passes.ConstantDefinitions;
import com.google.gwt.thirdparty.common.css.compiler.passes.CreateComponentNodes;
import com.google.gwt.thirdparty.common.css.compiler.passes.CreateConditionalNodes;
import com.google.gwt.thirdparty.common.css.compiler.passes.CreateConstantReferences;
import com.google.gwt.thirdparty.common.css.compiler.passes.CreateDefinitionNodes;
import com.google.gwt.thirdparty.common.css.compiler.passes.CreateMixins;
import com.google.gwt.thirdparty.common.css.compiler.passes.CreateStandardAtRuleNodes;
import com.google.gwt.thirdparty.common.css.compiler.passes.CssClassRenaming;
import com.google.gwt.thirdparty.common.css.compiler.passes.DisallowDuplicateDeclarations;
import com.google.gwt.thirdparty.common.css.compiler.passes.EliminateEmptyRulesetNodes;
import com.google.gwt.thirdparty.common.css.compiler.passes.EliminateUnitsFromZeroNumericValues;
import com.google.gwt.thirdparty.common.css.compiler.passes.EliminateUselessRulesetNodes;
import com.google.gwt.thirdparty.common.css.compiler.passes.HandleUnknownAtRuleNodes;
import com.google.gwt.thirdparty.common.css.compiler.passes.MarkRemovableRulesetNodes;
import com.google.gwt.thirdparty.common.css.compiler.passes.MergeAdjacentRulesetNodesWithSameDeclarations;
import com.google.gwt.thirdparty.common.css.compiler.passes.MergeAdjacentRulesetNodesWithSameSelector;
import com.google.gwt.thirdparty.common.css.compiler.passes.ProcessComponents;
import com.google.gwt.thirdparty.common.css.compiler.passes.ProcessKeyframes;
import com.google.gwt.thirdparty.common.css.compiler.passes.ProcessRefiners;
import com.google.gwt.thirdparty.common.css.compiler.passes.ReplaceConstantReferences;
import com.google.gwt.thirdparty.common.css.compiler.passes.ReplaceMixins;
import com.google.gwt.thirdparty.common.css.compiler.passes.ResolveCustomFunctionNodes;
import com.google.gwt.thirdparty.common.css.compiler.passes.SplitRulesetNodes;
import com.google.gwt.thirdparty.guava.common.base.CaseFormat;
import com.google.gwt.thirdparty.guava.common.base.Charsets;
import com.google.gwt.thirdparty.guava.common.base.Joiner;
import com.google.gwt.thirdparty.guava.common.base.Predicates;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableSet;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableSet.Builder;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
import com.google.gwt.thirdparty.guava.common.collect.Maps;
import com.google.gwt.thirdparty.guava.common.collect.Sets;
import com.google.gwt.thirdparty.guava.common.io.Resources;
import com.google.gwt.user.rebind.SourceWriter;
import com.google.gwt.user.rebind.StringSourceWriter;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.UUID;
import java.util.zip.Adler32;
/**
* This generator parses and compiles a GSS file to a css string and generates the implementation
* of the corresponding CssResource interface.
*/
public class GssResourceGenerator extends AbstractCssResourceGenerator implements
SupportsGeneratorResultCaching {
/**
* {@link ErrorManager} used to log the errors and warning messages produced by the different
* {@link com.google.gwt.thirdparty.common.css.compiler.ast.CssCompilerPass}.
*/
private static class LoggerErrorManager implements ErrorManager {
private final TreeLogger logger;
private boolean hasErrors;
private LoggerErrorManager(TreeLogger logger) {
this.logger = logger;
}
@Override
public void generateReport() {
// do nothing
}
@Override
public boolean hasErrors() {
return hasErrors;
}
@Override
public void report(GssError error) {
String fileName = "";
String location = "";
SourceCodeLocation codeLocation = error.getLocation();
if (codeLocation != null) {
fileName = codeLocation.getSourceCode().getFileName();
location = "[line: " + codeLocation.getBeginLineNumber() + " column: " + codeLocation
.getBeginIndexInLine() + "]";
}
logger.log(Type.ERROR, "Error in " + fileName + location + ": " + error.getMessage());
hasErrors = true;
}
@Override
public void reportWarning(GssError warning) {
logger.log(Type.WARN, warning.getMessage());
}
}
private static class ConversionResult {
final String gss;
final Map<String, String> defNameMapping;
private ConversionResult(String gss, Map<String, String> defNameMapping) {
this.gss = gss;
this.defNameMapping = defNameMapping;
}
}
private static class RenamingResult {
final Map<String, String> mapping;
final Set<String> externalClassCandidate;
private RenamingResult(Map<String, String> mapping, Set<String> externalClassCandidate) {
this.mapping = mapping;
this.externalClassCandidate = externalClassCandidate;
}
}
private static class CssParsingResult {
final CssTree tree;
final List<String> permutationAxes;
final Map<String, String> originalConstantNameMapping;
private CssParsingResult(CssTree tree, List<String> permutationAxis,
Map<String, String> originalConstantNameMapping) {
this.tree = tree;
this.permutationAxes = permutationAxis;
this.originalConstantNameMapping = originalConstantNameMapping;
}
}
// To be sure to avoid conflict during the style classes renaming between different GssResources,
// we will create a different prefix for each GssResource. We use a MinimalSubstitutionMap
// that will create a String with 1-6 characters in length but keeping the length of the prefix
// as short as possible. For instance if we have two GssResources to compile, the prefix
// for the first resource will be 'a' and the prefix for the second resource will be 'b' and so on
private static final SubstitutionMap resourcePrefixBuilder = new MinimalSubstitutionMap();
private static final String KEY_LEGACY = "CssResource.legacy";
private static final String KEY_CONVERSION_MODE = "CssResource.conversionMode";
private static final String KEY_STYLE = "CssResource.style";
private static final String ALLOWED_AT_RULE = "CssResource.allowedAtRules";
private static final String ALLOWED_FUNCTIONS = "CssResource.allowedFunctions";
private static final String KEY_OBFUSCATION_PREFIX = "CssResource.obfuscationPrefix";
private static final String KEY_CLASS_PREFIX = "cssResourcePrefix";
private static final String KEY_BY_CLASS_AND_METHOD = "cssResourceClassAndMethod";
private static final String KEY_HAS_CACHED_DATA = "hasCachedData";
private static final String KEY_SHARED_METHODS = "sharedMethods";
private static final char[] BASE32_CHARS = new char[]{
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', '0', '1',
'2', '3', '4', '5', '6'};
/**
* Returns the import prefix for a type, including the trailing hyphen.
*/
public static String getImportPrefix(JClassType importType) {
String prefix = importType.getSimpleSourceName();
ImportedWithPrefix exp = importType.getAnnotation(ImportedWithPrefix.class);
if (exp != null) {
prefix = exp.value();
}
return prefix + "-";
}
private static String encode(long id) {
assert id >= 0;
StringBuilder b = new StringBuilder();
// Use only guaranteed-alpha characters for the first character
b.append(BASE32_CHARS[(int) (id & 0xf)]);
id >>= 4;
while (id != 0) {
b.append(BASE32_CHARS[(int) (id & 0x1f)]);
id >>= 5;
}
return b.toString();
}
private Map<JMethod, CssParsingResult> cssParsingResultMap;
private Set<String> allowedNonStandardFunctions;
private LoggerErrorManager errorManager;
private JMethod getTextMethod;
private JMethod ensuredInjectedMethod;
private JMethod getNameMethod;
private String obfuscationPrefix;
private CssObfuscationStyle obfuscationStyle;
private Set<String> allowedAtRules;
private Map<JClassType, Map<String, String>> replacementsByClassAndMethod;
private Map<JMethod, String> replacementsForSharedMethods;
private boolean allowLegacy;
private boolean lenientConversion;
@Override
public String createAssignment(TreeLogger logger, ResourceContext context, JMethod method)
throws UnableToCompleteException {
CssParsingResult cssParsingResult = cssParsingResultMap.get(method);
CssTree cssTree = cssParsingResult.tree;
RenamingResult renamingResult = doClassRenaming(cssTree, method, logger, context);
// TODO : Should we foresee configuration properties for simplifyCss and eliminateDeadCode
// booleans ?
ConstantDefinitions constantDefinitions = optimizeTree(cssParsingResult, context, true, true,
logger);
checkErrors();
Set<String> externalClasses = revertRenamingOfExternalClasses(cssTree, renamingResult);
checkErrors();
// Validate that classes not assigned to one of the interface methods are external
validateExternalClasses(externalClasses, renamingResult.externalClassCandidate, method, logger);
SourceWriter sw = new StringSourceWriter();
sw.println("new " + method.getReturnType().getQualifiedSourceName() + "() {");
sw.indent();
writeMethods(logger, context, method, sw, constantDefinitions,
cssParsingResult.originalConstantNameMapping, renamingResult.mapping);
sw.outdent();
sw.println("}");
return sw.toString();
}
private void validateExternalClasses(Set<String> externalClasses,
Set<String> externalClassCandidates, JMethod method,
TreeLogger logger) throws UnableToCompleteException {
if (!isStrictResource(method)) {
return;
}
boolean hasError = false;
for (String candidate : externalClassCandidates) {
if (!externalClasses.contains(candidate)) {
logger.log(Type.ERROR, "The following non-obfuscated class is present in a strict " +
"CssResource: " + candidate);
hasError = true;
}
}
if (hasError) {
throw new UnableToCompleteException();
}
}
@Override
public void init(TreeLogger logger, ResourceContext context) throws UnableToCompleteException {
cssParsingResultMap = new IdentityHashMap<JMethod, CssParsingResult>();
errorManager = new LoggerErrorManager(logger);
allowedNonStandardFunctions = new HashSet<String>();
allowedAtRules = Sets.newHashSet(ExternalClassesCollector.EXTERNAL_AT_RULE);
try {
PropertyOracle propertyOracle = context.getGeneratorContext().getPropertyOracle();
ConfigurationProperty styleProp = propertyOracle.getConfigurationProperty(KEY_STYLE);
obfuscationStyle = CssObfuscationStyle.getObfuscationStyle(styleProp.getValues().get(0));
obfuscationPrefix = getObfuscationPrefix(propertyOracle, context);
ConfigurationProperty allowedAtRuleProperty = propertyOracle
.getConfigurationProperty(ALLOWED_AT_RULE);
allowedAtRules.addAll(allowedAtRuleProperty.getValues());
ConfigurationProperty allowedFunctionsProperty = propertyOracle
.getConfigurationProperty(ALLOWED_FUNCTIONS);
allowedNonStandardFunctions.addAll(allowedFunctionsProperty.getValues());
allowLegacy = "true".equals(propertyOracle.getConfigurationProperty(KEY_LEGACY).getValues()
.get(0));
// enable lenient conversion when legacy mode is enabled
lenientConversion = allowLegacy && "lenient".equals(propertyOracle
.getConfigurationProperty(KEY_CONVERSION_MODE).getValues().get(0));
ClientBundleRequirements requirements = context.getRequirements();
requirements.addConfigurationProperty(KEY_STYLE);
requirements.addConfigurationProperty(KEY_OBFUSCATION_PREFIX);
requirements.addConfigurationProperty(ALLOWED_AT_RULE);
requirements.addConfigurationProperty(ALLOWED_FUNCTIONS);
requirements.addConfigurationProperty(KEY_LEGACY);
requirements.addConfigurationProperty(KEY_CONVERSION_MODE);
} catch (BadPropertyValueException e) {
logger.log(TreeLogger.ERROR, "Unable to query module property", e);
throw new UnableToCompleteException();
}
TypeOracle typeOracle = context.getGeneratorContext().getTypeOracle();
JClassType cssResourceInterface = typeOracle.findType(CssResource.class.getCanonicalName());
JClassType resourcePrototypeInterface = typeOracle.findType(ResourcePrototype.class
.getCanonicalName());
try {
getTextMethod = cssResourceInterface.getMethod("getText", new JType[0]);
ensuredInjectedMethod = cssResourceInterface.getMethod("ensureInjected", new JType[0]);
getNameMethod = resourcePrototypeInterface.getMethod("getName", new JType[0]);
} catch (NotFoundException e) {
logger.log(TreeLogger.ERROR, "Unable to lookup methods from CssResource and " +
"ResourcePrototype interface", e);
throw new UnableToCompleteException();
}
initReplacement(context);
}
private void initReplacement(ResourceContext context) {
if (context.getCachedData(KEY_HAS_CACHED_DATA, Boolean.class) != Boolean.TRUE) {
context.putCachedData(KEY_SHARED_METHODS, new IdentityHashMap<JMethod, String>());
context.putCachedData(KEY_BY_CLASS_AND_METHOD, new IdentityHashMap<JClassType, Map<String,
String>>());
context.putCachedData(KEY_HAS_CACHED_DATA, Boolean.TRUE);
}
replacementsByClassAndMethod = context.getCachedData(KEY_BY_CLASS_AND_METHOD, Map.class);
replacementsForSharedMethods = context.getCachedData(KEY_SHARED_METHODS, Map.class);
}
private String getObfuscationPrefix(PropertyOracle propertyOracle, ResourceContext context)
throws BadPropertyValueException {
String prefix = propertyOracle.getConfigurationProperty(KEY_OBFUSCATION_PREFIX)
.getValues().get(0);
if ("empty".equalsIgnoreCase(prefix)) {
return "";
} else if ("default".equalsIgnoreCase(prefix)) {
return getDefaultObfuscationPrefix(context);
}
return prefix;
}
private String getDefaultObfuscationPrefix(ResourceContext context) {
String prefix = context.getCachedData(KEY_CLASS_PREFIX, String.class);
if (prefix == null) {
prefix = computeDefaultPrefix(context);
context.putCachedData(KEY_CLASS_PREFIX, prefix);
}
return prefix;
}
private String computeDefaultPrefix(ResourceContext context) {
SortedSet<JClassType> gssResources = computeOperableTypes(context);
Adler32 checksum = new Adler32();
for (JClassType type : gssResources) {
checksum.update(Util.getBytes(type.getQualifiedSourceName()));
}
int seed = Math.abs((int) checksum.getValue());
return encode(seed) + "-";
}
private SortedSet<JClassType> computeOperableTypes(ResourceContext context) {
TypeOracle typeOracle = context.getGeneratorContext().getTypeOracle();
JClassType baseInterface = typeOracle.findType(CssResource.class.getCanonicalName());
SortedSet<JClassType> toReturn = new TreeSet<JClassType>(new JClassOrderComparator());
JClassType[] cssResourceSubtypes = baseInterface.getSubtypes();
for (JClassType type : cssResourceSubtypes) {
if (type.isInterface() != null) {
toReturn.add(type);
}
}
return toReturn;
}
@Override
public void prepare(TreeLogger logger, ResourceContext context,
ClientBundleRequirements requirements, JMethod method) throws UnableToCompleteException {
if (method.getReturnType().isInterface() == null) {
logger.log(TreeLogger.ERROR, "Return type must be an interface");
throw new UnableToCompleteException();
}
URL[] resourceUrls = ResourceGeneratorUtil.findResources(logger, context, method);
if (resourceUrls.length == 0) {
logger.log(TreeLogger.ERROR, "At least one source must be specified");
throw new UnableToCompleteException();
}
CssParsingResult cssParsingResult = parseResources(Lists.newArrayList(resourceUrls), logger);
cssParsingResultMap.put(method, cssParsingResult);
for (String permutationAxis : cssParsingResult.permutationAxes) {
try {
context.getRequirements().addPermutationAxis(permutationAxis);
} catch (BadPropertyValueException e) {
logger.log(TreeLogger.ERROR, "Unknown deferred-binding property " + permutationAxis, e);
throw new UnableToCompleteException();
}
}
}
@Override
protected String getCssExpression(TreeLogger logger, ResourceContext context,
JMethod method) throws UnableToCompleteException {
CssTree cssTree = cssParsingResultMap.get(method).tree;
String standard = printCssTree(cssTree);
// TODO add configuration properties for swapLtrRtlInUrl, swapLeftRightInUrl and
// shouldFlipConstantReferences booleans
RecordingBidiFlipper recordingBidiFlipper =
new RecordingBidiFlipper(cssTree.getMutatingVisitController(), false, false, true);
recordingBidiFlipper.runPass();
if (recordingBidiFlipper.nodeFlipped()) {
String reversed = printCssTree(cssTree);
return LocaleInfo.class.getName() + ".getCurrentLocale().isRTL() ? "
+ reversed + " : " + standard;
} else {
return standard;
}
}
private void checkErrors() throws UnableToCompleteException {
if (errorManager.hasErrors()) {
throw new UnableToCompleteException();
}
}
private RenamingResult doClassRenaming(CssTree cssTree, JMethod method, TreeLogger logger,
ResourceContext context) throws UnableToCompleteException {
Map<String, Map<String, String>> replacementsWithPrefix = computeReplacements(method, logger,
context);
RenamingSubstitutionMap substitutionMap = new RenamingSubstitutionMap(replacementsWithPrefix);
new CssClassRenaming(cssTree.getMutatingVisitController(), substitutionMap, null).runPass();
Map<String, String> mapping = replacementsWithPrefix.get("");
mapping = Maps.newHashMap(Maps.filterKeys(mapping, Predicates.in(substitutionMap
.getStyleClasses())));
return new RenamingResult(mapping, substitutionMap.getExternalClassCandidates());
}
/**
* When the tree is fully processed, we can now collect the external classes and revert the
* renaming for these classes. We cannot collect the external classes during the original renaming
* because some external at-rule could be located inside a conditional block and could be
* removed when these blocks are evaluated.
*/
private Set<String> revertRenamingOfExternalClasses(CssTree cssTree, RenamingResult renamingResult) {
ExternalClassesCollector externalClassesCollector = new ExternalClassesCollector(cssTree
.getMutatingVisitController(), errorManager);
externalClassesCollector.runPass();
Map<String, String> styleClassesMapping = renamingResult.mapping;
// set containing all the style classes before the renaming.
Set<String> allStyleClassSet = Sets.newHashSet(styleClassesMapping.keySet());
// add the style classes that aren't associated to a method
allStyleClassSet.addAll(renamingResult.externalClassCandidate);
Set<String> externalClasses = externalClassesCollector.getExternalClassNames(allStyleClassSet,
renamingResult.externalClassCandidate);
final Map<String, String> revertMap = new HashMap<String, String>(externalClasses.size());
for (String external : externalClasses) {
revertMap.put(styleClassesMapping.get(external), external);
// override the mapping
styleClassesMapping.put(external, external);
}
SubstitutionMap revertExternalClasses = new SubstitutionMap() {
@Override
public String get(String key) {
return revertMap.get(key);
}
};
new CssClassRenaming(cssTree.getMutatingVisitController(), revertExternalClasses, null)
.runPass();
return externalClasses;
}
private boolean isStrictResource(JMethod method) {
NotStrict notStrict = method.getAnnotation(NotStrict.class);
return notStrict == null;
}
private List<String> finalizeTree(CssTree cssTree) throws UnableToCompleteException {
new CheckDependencyNodes(cssTree.getMutatingVisitController(), errorManager, false).runPass();
// Don't continue if errors exist
checkErrors();
new CreateStandardAtRuleNodes(cssTree.getMutatingVisitController(), errorManager).runPass();
new CreateMixins(cssTree.getMutatingVisitController(), errorManager).runPass();
new CreateDefinitionNodes(cssTree.getMutatingVisitController(), errorManager).runPass();
new CreateConstantReferences(cssTree.getMutatingVisitController()).runPass();
new CreateConditionalNodes(cssTree.getMutatingVisitController(), errorManager).runPass();
new CreateRuntimeConditionalNodes(cssTree.getMutatingVisitController()).runPass();
new CreateComponentNodes(cssTree.getMutatingVisitController(), errorManager).runPass();
new HandleUnknownAtRuleNodes(cssTree.getMutatingVisitController(), errorManager,
allowedAtRules, true, false).runPass();
new ProcessKeyframes(cssTree.getMutatingVisitController(), errorManager, true, true).runPass();
new ProcessRefiners(cssTree.getMutatingVisitController(), errorManager, true).runPass();
PermutationsCollector permutationsCollector = new PermutationsCollector(cssTree
.getMutatingVisitController(), errorManager);
permutationsCollector.runPass();
return permutationsCollector.getPermutationAxes();
}
private ConstantDefinitions optimizeTree(CssParsingResult cssParsingResult, ResourceContext context,
boolean simplifyCss, boolean eliminateDeadStyles, TreeLogger logger)
throws UnableToCompleteException {
CssTree cssTree = cssParsingResult.tree;
// Collect mixin definitions and replace mixins
CollectMixinDefinitions collectMixinDefinitions = new CollectMixinDefinitions(
cssTree.getMutatingVisitController(), errorManager);
collectMixinDefinitions.runPass();
new ReplaceMixins(cssTree.getMutatingVisitController(), errorManager,
collectMixinDefinitions.getDefinitions()).runPass();
new ProcessComponents<Object>(cssTree.getMutatingVisitController(), errorManager).runPass();
RuntimeConditionalBlockCollector runtimeConditionalBlockCollector = new
RuntimeConditionalBlockCollector(cssTree.getVisitController());
runtimeConditionalBlockCollector.runPass();
new ExtendedEliminateConditionalNodes(cssTree.getMutatingVisitController(),
getPermutationsConditions(context, cssParsingResult.permutationAxes, logger),
runtimeConditionalBlockCollector.getRuntimeConditionalBlock()).runPass();
new ValidateRuntimeConditionalNode(cssTree.getVisitController(), errorManager,
lenientConversion).runPass();
// Don't continue if errors exist
checkErrors();
CollectConstantDefinitions collectConstantDefinitionsPass = new CollectConstantDefinitions(
cssTree);
collectConstantDefinitionsPass.runPass();
ReplaceConstantReferences replaceConstantReferences = new ReplaceConstantReferences(cssTree,
collectConstantDefinitionsPass.getConstantDefinitions(), true, errorManager, false);
replaceConstantReferences.runPass();
new ImageSpriteCreator(cssTree.getMutatingVisitController(), context, errorManager).runPass();
Map<String, GssFunction> gssFunctionMap = new GwtGssFunctionMapProvider(context).get();
new ResolveCustomFunctionNodes(cssTree.getMutatingVisitController(), errorManager,
gssFunctionMap, true, allowedNonStandardFunctions).runPass();
if (simplifyCss) {
// Eliminate empty rules.
new EliminateEmptyRulesetNodes(cssTree.getMutatingVisitController()).runPass();
// Eliminating units for zero values.
new EliminateUnitsFromZeroNumericValues(cssTree.getMutatingVisitController()).runPass();
// Optimize color values.
new ColorValueOptimizer(cssTree.getMutatingVisitController()).runPass();
// Compress redundant top-right-bottom-left value lists.
new AbbreviatePositionalValues(cssTree.getMutatingVisitController()).runPass();
}
if (eliminateDeadStyles) {
// Report errors for duplicate declarations
new DisallowDuplicateDeclarations(cssTree.getVisitController(), errorManager).runPass();
// Split rules by selector and declaration.
new SplitRulesetNodes(cssTree.getMutatingVisitController()).runPass();
// Dead code elimination.
new MarkRemovableRulesetNodes(cssTree).runPass();
new EliminateUselessRulesetNodes(cssTree).runPass();
// Merge of rules with same selector.
new MergeAdjacentRulesetNodesWithSameSelector(cssTree).runPass();
new EliminateUselessRulesetNodes(cssTree).runPass();
// Merge of rules with same styles.
new MergeAdjacentRulesetNodesWithSameDeclarations(cssTree).runPass();
new EliminateUselessRulesetNodes(cssTree).runPass();
}
return collectConstantDefinitionsPass.getConstantDefinitions();
}
private Set<String> getPermutationsConditions(ResourceContext context,
List<String> permutationAxes, TreeLogger logger) throws UnableToCompleteException {
Builder<String> setBuilder = ImmutableSet.builder();
PropertyOracle oracle = context.getGeneratorContext().getPropertyOracle();
for (String permutationAxis : permutationAxes) {
String propValue = null;
try {
SelectionProperty selProp = oracle.getSelectionProperty(null,
permutationAxis);
propValue = selProp.getCurrentValue();
} catch (BadPropertyValueException e) {
try {
ConfigurationProperty confProp = oracle.getConfigurationProperty(permutationAxis);
propValue = confProp.getValues().get(0);
} catch (BadPropertyValueException e1) {
logger.log(Type.ERROR, "Unknown configuration property [" + permutationAxis + "]");
throw new UnableToCompleteException();
}
}
if (propValue != null) {
setBuilder.add(permutationAxis + ":" + propValue);
}
}
return setBuilder.build();
}
private CssParsingResult parseResources(List<URL> resources, TreeLogger logger)
throws UnableToCompleteException {
List<SourceCode> sourceCodes = new ArrayList<SourceCode>(resources.size());
ImmutableMap.Builder<String, String> constantNameMappingBuilder = ImmutableMap.builder();
// assert that we only support either gss or css on one resource.
boolean css = ensureEitherCssOrGss(resources, logger);
if (css && !allowLegacy) {
// TODO(dankurka): add link explaining the situation in detail.
logger.log(Type.ERROR,
"Your ClientBundle is referencing css files instead of gss. "
+ "You will need to either convert these files to gss using the "
+ "converter tool or turn on auto convertion in your gwt.xml file. "
+ "Note: Autoconversion will be removed in the next version of GWT, "
+ "you will need to move to gss."
+ "Add this line to your gwt.xml file to temporary avoid this:"
+ "<set-configuration-property name=\"CssResource.legacy\" value=\"true\" />");
throw new UnableToCompleteException();
}
if (css) {
String concatenatedCss = concatCssFiles(resources, logger);
ConversionResult result = convertToGss(concatenatedCss, logger);
String gss = result.gss;
String name = "[auto-converted gss files from : " + resources + "]";
sourceCodes.add(new SourceCode(name, gss));
constantNameMappingBuilder.putAll(result.defNameMapping);
} else {
for (URL stylesheet : resources) {
TreeLogger branchLogger = logger.branch(TreeLogger.DEBUG,
"Parsing GSS stylesheet " + stylesheet.toExternalForm());
try {
// TODO : always use UTF-8 to read the file ?
String fileContent =
Resources.asByteSource(stylesheet).asCharSource(Charsets.UTF_8).read();
sourceCodes.add(new SourceCode(stylesheet.getFile(), fileContent));
continue;
} catch (IOException e) {
branchLogger.log(TreeLogger.ERROR, "Unable to parse CSS", e);
}
throw new UnableToCompleteException();
}
}
CssTree tree;
try {
tree = new GssParser(sourceCodes).parse();
} catch (GssParserException e) {
logger.log(TreeLogger.ERROR, "Unable to parse CSS", e);
throw new UnableToCompleteException();
}
List<String> permutationAxes = finalizeTree(tree);
checkErrors();
return new CssParsingResult(tree, permutationAxes, constantNameMappingBuilder.build());
}
private ConversionResult convertToGss(String concatenatedCss, TreeLogger logger)
throws UnableToCompleteException {
File tempFile = null;
FileOutputStream fos = null;
try {
// We actually need a URL for the old CssResource to work. So create a temp file.
tempFile = File.createTempFile(UUID.randomUUID() + "css_converter", "css.tmp");
fos = new FileOutputStream(tempFile);
IOUtils.write(concatenatedCss, fos);
fos.close();
Css2Gss converter = new Css2Gss(tempFile.toURI().toURL(), logger, lenientConversion);
return new ConversionResult(converter.toGss(), converter.getDefNameMapping());
} catch (Css2GssConversionException e) {
String message = "An error occurs during the automatic conversion: " + e.getMessage();
if (!lenientConversion) {
message += "\n You should try to change the faulty css to fix this error. If you are " +
"unable to change the css, you can setup the automatic conversion to be lenient. Add " +
"the following line to your gwt.xml file: " +
"<set-configuration-property name=\"CssResource.conversionMode\" value=\"lenient\" />";
}
logger.log(Type.ERROR, message, e);
throw new UnableToCompleteException();
} catch (IOException e) {
logger.log(Type.ERROR, "Error while writing temporary css file", e);
throw new UnableToCompleteException();
} finally {
if (tempFile != null) {
tempFile.delete();
}
if (fos != null) {
IOUtils.closeQuietly(fos);
}
}
}
private String concatCssFiles(List<URL> resources, TreeLogger logger)
throws UnableToCompleteException {
StringBuffer buffer = new StringBuffer();
for (URL stylesheet : resources) {
try {
String fileContent = Resources.asByteSource(stylesheet).asCharSource(Charsets.UTF_8)
.read();
buffer.append(fileContent);
buffer.append("\n");
} catch (IOException e) {
logger.log(TreeLogger.ERROR, "Unable to parse CSS", e);
throw new UnableToCompleteException();
}
}
return buffer.toString();
}
private boolean ensureEitherCssOrGss(List<URL> resources, TreeLogger logger)
throws UnableToCompleteException {
boolean css = resources.get(0).toString().endsWith(".css");
for (URL stylesheet : resources) {
if (css && !stylesheet.toString().endsWith(".css")) {
logger.log(Type.ERROR,
"Only either css files or gss files are supported on one interface");
throw new UnableToCompleteException();
} else if (!css && !stylesheet.toString().endsWith(".gss")) {
logger.log(Type.ERROR,
"Only either css files or gss files are supported on one interface");
throw new UnableToCompleteException();
}
}
return css;
}
private String printCssTree(CssTree tree) {
CssPrinter cssPrinterPass = new CssPrinter(tree);
cssPrinterPass.runPass();
return cssPrinterPass.getCompactPrintedString();
}
private boolean writeClassMethod(TreeLogger logger, JMethod userMethod,
Map<String, String> substitutionMap, SourceWriter sw) throws
UnableToCompleteException {
if (userMethod.getParameters().length > 0) {
logger.log(Type.ERROR, "The method [" + userMethod.getName() + "] shouldn't contain any " +
"parameters");
throw new UnableToCompleteException();
}
String name = getClassName(userMethod);
String value = substitutionMap.get(name);
if (value == null) {
logger.log(Type.ERROR, "The following style class [" + name + "] is missing from the source" +
" CSS file");
return false;
} else {
writeSimpleGetter(userMethod, "\"" + value + "\"", sw);
}
return true;
}
private String getClassName(JMethod method) {
String name = method.getName();
ClassName classNameOverride = method.getAnnotation(ClassName.class);
if (classNameOverride != null) {
name = classNameOverride.value();
}
return name;
}
private boolean writeDefMethod(CssDefinitionNode definitionNode, TreeLogger logger,
JMethod userMethod, SourceWriter sw) throws UnableToCompleteException {
String name = userMethod.getName();
JClassType classReturnType = userMethod.getReturnType().isClass();
List<CssValueNode> params = definitionNode.getParameters();
if (params.size() != 1 && !isReturnTypeString(classReturnType)) {
logger.log(TreeLogger.ERROR, "@def rule " + name
+ " must define exactly one value or return type must be String");
return false;
}
String returnExpr;
if (isReturnTypeString(classReturnType)) {
List<String> returnValues = new ArrayList<String>();
for (CssValueNode valueNode : params) {
returnValues.add(Generator.escape(valueNode.toString()));
}
returnExpr = "\"" + Joiner.on(" ").join(returnValues) + "\"";
} else {
JPrimitiveType returnType = userMethod.getReturnType().isPrimitive();
if (returnType == null) {
logger.log(TreeLogger.ERROR, name + ": Return type must be primitive type " +
"or String for @def accessors");
return false;
}
CssValueNode valueNode = params.get(0);
if (!(valueNode instanceof CssNumericNode)) {
logger.log(TreeLogger.ERROR, "The value of the constant defined by @" + name + " is not a" +
" numeric");
return false;
}
String numericValue = ((CssNumericNode) valueNode).getNumericPart();
if (returnType == JPrimitiveType.INT || returnType == JPrimitiveType.LONG) {
returnExpr = "" + Long.parseLong(numericValue);
} else if (returnType == JPrimitiveType.FLOAT) {
returnExpr = numericValue + "F";
} else if (returnType == JPrimitiveType.DOUBLE) {
returnExpr = "" + numericValue;
} else {
logger.log(TreeLogger.ERROR, returnType.getQualifiedSourceName()
+ " is not a valid primitive return type for @def accessors");
return false;
}
}
writeSimpleGetter(userMethod, returnExpr, sw);
return true;
}
private void writeMethods(TreeLogger logger, ResourceContext context, JMethod method,
SourceWriter sw, ConstantDefinitions constantDefinitions,
Map<String, String> originalConstantNameMapping, Map<String, String> substitutionMap)
throws UnableToCompleteException {
JClassType gssResource = method.getReturnType().isInterface();
boolean success = true;
for (JMethod toImplement : gssResource.getOverridableMethods()) {
if (toImplement == getTextMethod) {
writeGetText(logger, context, method, sw);
} else if (toImplement == ensuredInjectedMethod) {
writeEnsureInjected(sw);
} else if (toImplement == getNameMethod) {
writeGetName(method, sw);
} else {
success &= writeUserMethod(logger, toImplement, sw, constantDefinitions,
originalConstantNameMapping, substitutionMap);
}
}
if (!success) {
throw new UnableToCompleteException();
}
}
private boolean writeUserMethod(TreeLogger logger, JMethod userMethod,
SourceWriter sw, ConstantDefinitions constantDefinitions,
Map<String, String> originalConstantNameMapping, Map<String, String> substitutionMap)
throws UnableToCompleteException {
String className = getClassName(userMethod);
// method to access style class ?
if (substitutionMap.containsKey(className) &&
isReturnTypeString(userMethod.getReturnType().isClass())) {
return writeClassMethod(logger, userMethod, substitutionMap, sw);
}
// method to access constant value ?
CssDefinitionNode definitionNode;
String methodName = userMethod.getName();
if (originalConstantNameMapping.containsKey(methodName)) {
// method name maps a constant that has been renamed during the auto conversion
String constantName = originalConstantNameMapping.get(methodName);
definitionNode = constantDefinitions.getConstantDefinition(constantName);
} else {
definitionNode = constantDefinitions.getConstantDefinition(methodName);
if (definitionNode == null) {
// try with upper case
definitionNode = constantDefinitions.getConstantDefinition(toUpperCase(methodName));
}
}
if (definitionNode != null) {
return writeDefMethod(definitionNode, logger, userMethod, sw);
}
if (substitutionMap.containsKey(className)) {
// method matched a class name but not a constant and the return type is not a string
logger.log(Type.ERROR, "The return type of the method [" + userMethod.getName() + "] must " +
"be java.lang.String.");
throw new UnableToCompleteException();
}
// the method doesn't match a style class nor a constant
logger.log(Type.ERROR,
"The following method [" + userMethod.getName() + "()] doesn't match a constant" +
" nor a style class. You could fix that by adding ." + className + " {}"
);
return false;
}
/**
* Transform a camel case string to upper case. Each word is separated by a '_'
*
* @param camelCase
* @return
*/
private String toUpperCase(String camelCase) {
return CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, camelCase);
}
private Map<String, Map<String, String>> computeReplacements(JMethod method, TreeLogger logger,
ResourceContext context) throws UnableToCompleteException {
Map<String, Map<String, String>> replacementsWithPrefix = new HashMap<String, Map<String,
String>>();
replacementsWithPrefix
.put("", computeReplacementsForType(method.getReturnType().isInterface()));
// Process the Import annotation if any
Import imp = method.getAnnotation(Import.class);
if (imp != null) {
boolean fail = false;
TypeOracle typeOracle = context.getGeneratorContext().getTypeOracle();
for (Class<? extends CssResource> clazz : imp.value()) {
JClassType importType = typeOracle.findType(clazz.getName().replace('$', '.'));
assert importType != null : "TypeOracle does not have type " + clazz.getName();
// add this import type as a requirement for this generator
context.getRequirements().addTypeHierarchy(importType);
String prefix = getImportPrefix(importType);
if (replacementsWithPrefix.put(prefix, computeReplacementsForType(importType)) != null) {
logger.log(TreeLogger.ERROR, "Multiple imports that would use the prefix " + prefix);
fail = true;
}
}
if (fail) {
throw new UnableToCompleteException();
}
}
return replacementsWithPrefix;
}
private Map<String, String> computeReplacementsForType(JClassType cssResource) {
Map<String, String> replacements = replacementsByClassAndMethod.get(cssResource);
if (replacements == null) {
replacements = new HashMap<String, String>();
replacementsByClassAndMethod.put(cssResource, replacements);
String resourcePrefix = resourcePrefixBuilder.get(cssResource.getQualifiedSourceName());
// This substitution map will prefix each renamed class with the resource prefix and use a
// MinimalSubstitutionMap for computing the obfuscated name.
SubstitutionMap prefixingSubstitutionMap = new PrefixingSubstitutionMap(
new MinimalSubstitutionMap(), obfuscationPrefix + resourcePrefix + "-");
for (JMethod method : cssResource.getOverridableMethods()) {
if (method == getNameMethod || method == getTextMethod || method == ensuredInjectedMethod) {
continue;
}
String styleClass = getClassName(method);
if (replacementsForSharedMethods.containsKey(method)) {
replacements.put(styleClass, replacementsForSharedMethods.get(method));
} else {
String obfuscatedClassName = prefixingSubstitutionMap.get(styleClass);
String replacement = obfuscationStyle.getPrettyName(styleClass, cssResource,
obfuscatedClassName);
replacements.put(styleClass, replacement);
maybeHandleSharedMethod(method, replacement);
}
}
}
return replacements;
}
private void maybeHandleSharedMethod(JMethod method, String obfuscatedClassName) {
JClassType enclosingType = method.getEnclosingType();
Shared shared = enclosingType.getAnnotation(Shared.class);
if (shared != null) {
replacementsForSharedMethods.put(method, obfuscatedClassName);
}
}
}