| /* |
| * 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; |
| } |
| } |