blob: b00b2eb4916a49067d30bca329c60a2ca55a84a7 [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.ClientBundle.Source;
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.BooleanConditionCollector;
import com.google.gwt.resources.gss.CollectAndRemoveConstantDefinitions;
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.CssCompositeValueNode;
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.CreateForLoopNodes;
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.CreateVendorPrefixedKeyframes;
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.MarkNonFlippableNodes;
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.common.css.compiler.passes.UnrollLoops;
import com.google.gwt.thirdparty.common.css.compiler.passes.ValidatePropertyValues;
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.Predicate;
import com.google.gwt.thirdparty.guava.common.base.Predicates;
import com.google.gwt.thirdparty.guava.common.base.Strings;
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.ByteSource;
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.io.PrintWriter;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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 {
/**
* GssOptions contains the values of all configuration properties that can be used with
* GssResource.
*/
public static class GssOptions {
private final boolean enabled;
private final AutoConversionMode autoConversionMode;
private final boolean gssDefaultInUiBinder;
public GssOptions(boolean enabled, AutoConversionMode autoConversionMode, boolean gssDefaultInUiBinder) {
this.enabled = enabled;
this.autoConversionMode = autoConversionMode;
this.gssDefaultInUiBinder = gssDefaultInUiBinder;
}
public boolean isEnabled() {
return enabled;
}
public boolean isGssDefaultInUiBinder() {
return gssDefaultInUiBinder;
}
public boolean isAutoConversionOff() {
return autoConversionMode == AutoConversionMode.OFF;
}
public boolean isLenientConversion() {
return autoConversionMode == AutoConversionMode.LENIENT;
}
}
/**
* Different conversion modes from css to gss.
*/
public enum AutoConversionMode { STRICT, LENIENT, OFF }
/*
* TODO(dankurka): This is a nasty hack to get the compiler to output all @def's
* it has seen in a compile. Once GSS migration is done this needs to be removed.
*/
private static boolean shouldEmitVariables;
private static PrintWriter printWriter;
private static Set<String> writtenAtDefs = new HashSet<>();
private static final String KEY_ENABLE_GSS = "CssResource.enableGss";
private static final String KEY_GSS_DEFAULT_IN_UIBINDER = "CssResource.gssDefaultInUiBinder";
static {
String varFileName = System.getProperty("emitGssVarNameFile");
shouldEmitVariables = varFileName != null;
if (shouldEmitVariables) {
try {
File file = new File(varFileName);
file.createNewFile();
printWriter = new PrintWriter(new FileOutputStream(file));
} catch (Exception e) {
System.err.println("Error while opening file");
e.printStackTrace();
System.exit(-1);
}
}
}
public static SourceCode readUrlContent(URL fileUrl, TreeLogger logger) throws UnableToCompleteException {
TreeLogger branchLogger = logger.branch(TreeLogger.DEBUG,
"Reading GSS stylesheet " + fileUrl.toExternalForm());
try {
ByteSource byteSource = Resources.asByteSource(fileUrl);
// default charset
Charset charset = Charsets.UTF_8;
// check if the stylesheet doesn't include a @charset at-rule
String styleSheetCharset = extractCharset(byteSource);
if (styleSheetCharset != null) {
try {
charset = Charset.forName(styleSheetCharset);
} catch (UnsupportedCharsetException e) {
logger.log(Type.ERROR, "Unsupported charset found: " + styleSheetCharset);
throw new UnableToCompleteException();
}
}
String fileContent = byteSource.asCharSource(charset).read();
// If the stylesheet specified a charset, we have to remove the at-rule otherwise the GSS
// compiler will fail.
if (styleSheetCharset != null) {
int charsetAtRuleLength = CHARSET_MIN_LENGTH + styleSheetCharset.length();
// replace charset at-rule by blanks to keep correct source location of the rest of
// the stylesheet.
fileContent = Strings.repeat(" ", charsetAtRuleLength) +
fileContent.substring(charsetAtRuleLength);
}
return new SourceCode(fileUrl.getFile(), fileContent);
} catch (IOException e) {
branchLogger.log(TreeLogger.ERROR, "Unable to parse CSS", e);
}
throw new UnableToCompleteException();
}
public static GssOptions getGssOptions(PropertyOracle propertyOracle, TreeLogger logger) throws UnableToCompleteException {
boolean gssEnabled;
boolean gssDefaultInUiBinder;
AutoConversionMode conversionMode;
try {
ConfigurationProperty enableGssProp =
propertyOracle.getConfigurationProperty(KEY_ENABLE_GSS);
String enableGss = enableGssProp.getValues().get(0);
gssEnabled = Boolean.parseBoolean(enableGss);
} catch (BadPropertyValueException ex) {
logger.log(Type.ERROR, "Unable to determine if GSS need to be used");
throw new UnableToCompleteException();
}
try {
conversionMode = Enum.valueOf(AutoConversionMode.class, propertyOracle
.getConfigurationProperty(KEY_CONVERSION_MODE).getValues().get(0)
.toUpperCase(Locale.ROOT));
} catch (BadPropertyValueException ex) {
logger.log(Type.ERROR, "Unable to conversion mode for GSS");
throw new UnableToCompleteException();
}
try {
ConfigurationProperty uiBinderGssDefaultProp =
propertyOracle.getConfigurationProperty(KEY_GSS_DEFAULT_IN_UIBINDER);
String uiBinderGssDefaultValue = uiBinderGssDefaultProp.getValues().get(0);
gssDefaultInUiBinder = Boolean.parseBoolean(uiBinderGssDefaultValue);
} catch (BadPropertyValueException ex) {
logger.log(Type.ERROR, "Unable to determine default for GSS in UiBinder");
throw new UnableToCompleteException();
}
return new GssOptions(gssEnabled, conversionMode, gssDefaultInUiBinder);
}
private static synchronized void write(Set<String> variables) {
for (String atDef : variables) {
if (writtenAtDefs.add(atDef)) {
printWriter.println("@def " + atDef + " 1px;");
}
}
printWriter.flush();
}
/**
* {@link ErrorManager} used to log the errors and warning messages produced by the different
* {@link com.google.gwt.thirdparty.common.css.compiler.ast.CssCompilerPass}.
*/
public static class LoggerErrorManager implements ErrorManager {
private final TreeLogger logger;
private boolean hasErrors;
public 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;
final Set<String> trueConditions;
private CssParsingResult(CssTree tree, List<String> permutationAxis, Set<String> trueConditions,
Map<String, String> originalConstantNameMapping) {
this.tree = tree;
this.permutationAxes = permutationAxis;
this.originalConstantNameMapping = originalConstantNameMapping;
this.trueConditions = trueConditions;
}
}
/**
* Predicate implementation used during the conversion to GSS.
*/
private static class ConfigurationPropertyMatcher implements Predicate<String> {
private final PropertyOracle propertyOracle;
private final TreeLogger logger;
private boolean error;
ConfigurationPropertyMatcher(ResourceContext context, TreeLogger logger) {
this.logger = logger;
propertyOracle = context.getGeneratorContext().getPropertyOracle();
}
@Override
public boolean apply(String booleanCondition) {
// if the condition is negated, the string parameter contains the ! operator if this method
// is called during the conversion to GSS
if (booleanCondition.startsWith("!")) {
booleanCondition = booleanCondition.substring(1);
}
try {
ConfigurationProperty property = propertyOracle.getConfigurationProperty(booleanCondition);
boolean valid = checkPropertyIsSingleValueAndBoolean(property, logger);
error |= !valid;
return valid;
} catch (BadPropertyValueException e) {
return false;
}
}
}
// 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_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'};
// We follow CSS specification to detect the charset:
// - Authors using an @charset rule must place the rule at the very beginning of the style sheet,
// preceded by no characters.
// - @charset must be written literally, i.e., the 10 characters '@charset "' (lowercase, no
// backslash escapes), followed by the encoding name, followed by '";'.
// see: http://www.w3.org/TR/CSS2/syndata.html#charset
private static final Pattern CHARSET = Pattern.compile("^@charset \"([^\"]*)\";");
private static final int CHARSET_MIN_LENGTH = "@charset \"\";".length();
/**
* 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 static boolean checkPropertyIsSingleValueAndBoolean(ConfigurationProperty property,
TreeLogger logger) {
List<String> values = property.getValues();
if (values.size() > 1) {
logger.log(Type.ERROR, "The configuration property " + property.getName() + " is used in " +
"a conditional css and cannot be a multi-valued property");
return false;
}
String value = values.get(0);
if (!"true".equals(value) && !"false".equals(value)) {
logger.log(Type.ERROR, "The configuration property " + property.getName() + " is used in " +
"a conditional css. Its value must be either \"true\" or \"false\"");
return false;
}
return true;
}
/**
* Temporary method needed when GSS and the old CSS syntax are both supported by the sdk.
* It aims to choose the right resource file according to whether gss is enabled or not. If gss is
* enabled, it will try to find the resource file ending by .gss first. If GSS is disabled it will
* try to find the .css file. This logic is applied even if a
* {@link com.google.gwt.resources.client.ClientBundle.Source} annotation is used to define
* the resource file.
* <p>
* This method can be deleted once the support for the old CssResource is removed and use directly
* ResourceGeneratorUtil.findResources().
*/
static URL[] findResources(TreeLogger logger, ResourceContext context, JMethod method,
boolean gssEnabled) throws UnableToCompleteException {
boolean isSourceAnnotationUsed = method.getAnnotation(Source.class) != null;
if (!isSourceAnnotationUsed) {
// ResourceGeneratorUtil will try to find automatically the resource file. Give him the right
// extension to use first
String[] extensions = gssEnabled ?
new String[]{".gss", ".css"} : new String[]{".css", ".gss"};
return ResourceGeneratorUtil.findResources(logger, context, method, extensions);
}
// find the original resource files specified by the @Source annotation
URL[] originalResources = ResourceGeneratorUtil.findResources(logger, context, method);
URL[] resourcesToUse = new URL[originalResources.length];
String preferredExtension = gssEnabled ? ".gss" : ".css";
// Try to find all the resources by using the preferred extension according to whether gss is
// enabled or not. If one file with the preferred extension is missing, return the original
// resource files otherwise return the preferred files.
String[] sourceFiles = method.getAnnotation(Source.class).value();
for (int i = 0; i < sourceFiles.length; i++) {
String original = sourceFiles[i];
if (!original.endsWith(preferredExtension) && original.length() > 4) {
String preferredFile = original.substring(0, original.length() - 4) + preferredExtension;
// try to find the resource relative to the package
String path = method.getEnclosingType().getPackage().getName().replace('.', '/') + '/';
URL preferredUrl = ResourceGeneratorUtil
.tryFindResource(logger, context.getGeneratorContext(), context, path + preferredFile);
if (preferredUrl == null) {
// if it doesn't exist, assume it is absolute
preferredUrl = ResourceGeneratorUtil
.tryFindResource(logger, context.getGeneratorContext(), context, preferredFile);
}
if (preferredUrl == null) {
// avoid to mix gss and css, if one file with the preferred extension is missing
return originalResources;
}
logger.log(Type.DEBUG, "Preferred resource file found: " + preferredFile + ". This file " +
"will be used in replacement of " + original);
resourcesToUse[i] = preferredUrl;
} else {
// gss and css files shouldn't be used together for a same resource. So if one of the file
// is using the the preferred extension, return the original resources. If the dev has mixed
// gss and ccs files, that will fail later.
return originalResources;
}
}
return resourcesToUse;
}
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 final GssOptions gssOptions;
public GssResourceGenerator(GssOptions gssOptions) {
this.gssOptions = gssOptions;
}
@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();
Map<JMethod, String> actualReplacements = writeMethods(logger, context, method, sw, constantDefinitions,
cssParsingResult.originalConstantNameMapping, renamingResult.mapping);
sw.outdent();
sw.println("}");
CssResourceGenerator.outputCssMapArtifact(logger, context, method, actualReplacements);
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 + ". Fix by adding String accessor "
+ "method(s) to the CssResource interface for obfuscated classes, "
+ "or use an @external declaration for unobfuscated classes.");
hasError = true;
}
}
if (hasError) {
throw new UnableToCompleteException();
}
}
@Override
public void init(TreeLogger logger, ResourceContext context) throws UnableToCompleteException {
cssParsingResultMap = new IdentityHashMap<>();
errorManager = new LoggerErrorManager(logger);
allowedNonStandardFunctions = new HashSet<>();
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());
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_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);
}
@SuppressWarnings("unchecked")
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<>(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 = findResources(logger, context, method, gssOptions.isEnabled());
if (resourceUrls.length == 0) {
logger.log(TreeLogger.ERROR, "At least one source must be specified");
throw new UnableToCompleteException();
}
CssParsingResult cssParsingResult = parseResources(Lists.newArrayList(resourceUrls), context,
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<>(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 void 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 CreateForLoopNodes(cssTree.getMutatingVisitController(), errorManager).runPass();
new CreateComponentNodes(cssTree.getMutatingVisitController(), errorManager).runPass();
new ValidatePropertyValues(cssTree.getVisitController(), errorManager).runPass();
new HandleUnknownAtRuleNodes(cssTree.getMutatingVisitController(), errorManager,
allowedAtRules, true, false).runPass();
new ProcessKeyframes(cssTree.getMutatingVisitController(), errorManager, true, true).runPass();
new CreateVendorPrefixedKeyframes(cssTree.getMutatingVisitController(),
errorManager).runPass();
new UnrollLoops(cssTree.getMutatingVisitController(), errorManager).runPass();
new ProcessRefiners(cssTree.getMutatingVisitController(), errorManager, true).runPass();
new MarkNonFlippableNodes(cssTree.getMutatingVisitController(), errorManager).runPass();
}
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<>(cssTree.getMutatingVisitController(), errorManager).runPass();
RuntimeConditionalBlockCollector runtimeConditionalBlockCollector = new
RuntimeConditionalBlockCollector(cssTree.getVisitController());
runtimeConditionalBlockCollector.runPass();
Set<String> trueCompileTimeConditions = ImmutableSet.<String>builder()
.addAll(getCurrentDeferredBindingProperties(context, cssParsingResult.permutationAxes,
logger))
.addAll(getTrueConfigurationProperties(context, cssParsingResult.trueConditions, logger))
.build();
new ExtendedEliminateConditionalNodes(cssTree.getMutatingVisitController(),
trueCompileTimeConditions, runtimeConditionalBlockCollector.getRuntimeConditionalBlock())
.runPass();
new ValidateRuntimeConditionalNode(cssTree.getVisitController(), errorManager,
gssOptions.isLenientConversion()).runPass();
// Don't continue if errors exist
checkErrors();
CollectConstantDefinitions collectConstantDefinitionsPass = new CollectConstantDefinitions(
cssTree);
collectConstantDefinitionsPass.runPass();
ReplaceConstantReferences replaceConstantReferences = new ReplaceConstantReferences(cssTree,
collectConstantDefinitionsPass.getConstantDefinitions(), false, 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();
// collect the final value of the constants and remove them.
collectConstantDefinitionsPass = new CollectAndRemoveConstantDefinitions(cssTree);
collectConstantDefinitionsPass.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();
new MarkNonFlippableNodes(cssTree.getMutatingVisitController(), errorManager).runPass();
}
return collectConstantDefinitionsPass.getConstantDefinitions();
}
private Set<String> getTrueConfigurationProperties(ResourceContext context,
Set<String> configurationProperties, TreeLogger logger) throws UnableToCompleteException {
Builder<String> setBuilder = ImmutableSet.builder();
PropertyOracle oracle = context.getGeneratorContext().getPropertyOracle();
for (String property : configurationProperties) {
try {
// TODO : only check configuration properties ?
ConfigurationProperty confProp = oracle.getConfigurationProperty(property);
if (!checkPropertyIsSingleValueAndBoolean(confProp, logger)) {
throw new UnableToCompleteException();
}
if ("true".equals(confProp.getValues().get(0))) {
setBuilder.add(property);
}
} catch (BadPropertyValueException e1) {
logger.log(Type.ERROR, "Unknown configuration property [" + property + "]");
throw new UnableToCompleteException();
}
}
return setBuilder.build();
}
private Set<String> getCurrentDeferredBindingProperties(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;
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, ResourceContext context,
TreeLogger logger) throws UnableToCompleteException {
List<SourceCode> sourceCodes = new ArrayList<>(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 && gssOptions.isAutoConversionOff()) {
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.conversionMode\""
+ " value=\"strict\" /> "
+ "Details on how to migrate to GSS can be found at: http://goo.gl/tEQnmJ");
throw new UnableToCompleteException();
}
if (css) {
String concatenatedCss = concatCssFiles(resources, logger);
ConversionResult result = convertToGss(concatenatedCss, context, logger);
if (shouldEmitVariables) {
write(result.defNameMapping.keySet());
}
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) {
sourceCodes.add(readUrlContent(stylesheet, logger));
}
}
CssTree tree;
try {
tree = new GssParser(sourceCodes).parse();
} catch (GssParserException e) {
logger.log(TreeLogger.ERROR, "Unable to parse CSS", e);
throw new UnableToCompleteException();
}
// create more explicit nodes
finalizeTree(tree);
checkErrors();
// collect boolean conditions that have to be mapped to configuration properties
BooleanConditionCollector booleanConditionCollector = new BooleanConditionCollector(tree
.getMutatingVisitController());
booleanConditionCollector.runPass();
// collect permutations axis used in conditionals.
PermutationsCollector permutationsCollector = new PermutationsCollector(tree
.getMutatingVisitController());
permutationsCollector.runPass();
return new CssParsingResult(tree, permutationsCollector.getPermutationAxes(),
booleanConditionCollector.getBooleanConditions(), constantNameMappingBuilder.build());
}
private static String extractCharset(ByteSource byteSource) throws IOException {
String firstLine = byteSource.asCharSource(Charsets.UTF_8).readFirstLine();
if (firstLine != null) {
Matcher matcher = CHARSET.matcher(firstLine);
if (matcher.matches()) {
return matcher.group(1);
}
}
return null;
}
private ConversionResult convertToGss(String concatenatedCss, ResourceContext context,
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();
ConfigurationPropertyMatcher configurationPropertyMatcher =
new ConfigurationPropertyMatcher(context, logger);
Css2Gss converter = new Css2Gss(tempFile.toURI().toURL(), logger,
gssOptions.isLenientConversion(), configurationPropertyMatcher);
String gss = converter.toGss();
if (configurationPropertyMatcher.error) {
throw new UnableToCompleteException();
}
return new ConversionResult(gss, converter.getDefNameMapping());
} catch (Css2GssConversionException e) {
String message = "An error occurs during the automatic conversion: " + e.getMessage();
if (!gssOptions.isLenientConversion()) {
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);
}
}
}
public static 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);
// when a constant refers to another constant, closure-stylesheet wrap the CssNumericNode in
// a CssCompositeValueNode. Unwrap it.
if (valueNode instanceof CssCompositeValueNode) {
CssCompositeValueNode toUnwrap = (CssCompositeValueNode) valueNode;
if (toUnwrap.getValues().size() == 1) {
valueNode = toUnwrap.getValues().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 Map<JMethod, String> 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;
Map<JMethod, String> methodToClassName = new LinkedHashMap<>();
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, methodToClassName);
}
}
if (!success) {
throw new UnableToCompleteException();
}
return methodToClassName;
}
private boolean writeUserMethod(TreeLogger logger, JMethod userMethod, SourceWriter sw,
ConstantDefinitions constantDefinitions, Map<String, String> originalConstantNameMapping,
Map<String, String> substitutionMap, Map<JMethod, String> methodToClassName)
throws UnableToCompleteException {
String className = getClassName(userMethod);
// method to access style class ?
if (substitutionMap.containsKey(className) &&
isReturnTypeString(userMethod.getReturnType().isClass())) {
methodToClassName.put(userMethod, substitutionMap.get(className));
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
*/
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);
if (hasSharedAnnotation(method)) {
// We always use the base type for obfuscation if this is a shared method
replacement = obfuscationStyle.getPrettyName(styleClass, method.getEnclosingType(),
obfuscatedClassName);
replacementsForSharedMethods.put(method, replacement);
}
replacements.put(styleClass, replacement);
}
}
}
return replacements;
}
private boolean hasSharedAnnotation(JMethod method) {
JClassType enclosingType = method.getEnclosingType();
Shared shared = enclosingType.getAnnotation(Shared.class);
return shared != null;
}
}