Java tutorial
/* * Copyright 2013 Julien Dramaix. * * 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.common.base.CaseFormat; import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.base.Predicates; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet.Builder; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.css.MinimalSubstitutionMap; import com.google.common.css.PrefixingSubstitutionMap; import com.google.common.css.SourceCode; import com.google.common.css.SourceCodeLocation; import com.google.common.css.SubstitutionMap; import com.google.common.css.compiler.ast.CssDefinitionNode; import com.google.common.css.compiler.ast.CssNumericNode; import com.google.common.css.compiler.ast.CssTree; import com.google.common.css.compiler.ast.CssValueNode; import com.google.common.css.compiler.ast.ErrorManager; import com.google.common.css.compiler.ast.GssError; import com.google.common.css.compiler.ast.GssFunction; import com.google.common.css.compiler.ast.GssParser; import com.google.common.css.compiler.ast.GssParserException; import com.google.common.css.compiler.passes.AbbreviatePositionalValues; import com.google.common.css.compiler.passes.CheckDependencyNodes; import com.google.common.css.compiler.passes.CollectConstantDefinitions; import com.google.common.css.compiler.passes.CollectMixinDefinitions; import com.google.common.css.compiler.passes.ColorValueOptimizer; import com.google.common.css.compiler.passes.ConstantDefinitions; import com.google.common.css.compiler.passes.CreateComponentNodes; import com.google.common.css.compiler.passes.CreateConditionalNodes; import com.google.common.css.compiler.passes.CreateConstantReferences; import com.google.common.css.compiler.passes.CreateDefinitionNodes; import com.google.common.css.compiler.passes.CreateMixins; import com.google.common.css.compiler.passes.CreateStandardAtRuleNodes; import com.google.common.css.compiler.passes.CssClassRenaming; import com.google.common.css.compiler.passes.DisallowDuplicateDeclarations; import com.google.common.css.compiler.passes.EliminateEmptyRulesetNodes; import com.google.common.css.compiler.passes.EliminateUnitsFromZeroNumericValues; import com.google.common.css.compiler.passes.EliminateUselessRulesetNodes; import com.google.common.css.compiler.passes.HandleUnknownAtRuleNodes; import com.google.common.css.compiler.passes.MarkRemovableRulesetNodes; import com.google.common.css.compiler.passes.MergeAdjacentRulesetNodesWithSameDeclarations; import com.google.common.css.compiler.passes.MergeAdjacentRulesetNodesWithSameSelector; import com.google.common.css.compiler.passes.ProcessComponents; import com.google.common.css.compiler.passes.ProcessKeyframes; import com.google.common.css.compiler.passes.ProcessRefiners; import com.google.common.css.compiler.passes.ReplaceConstantReferences; import com.google.common.css.compiler.passes.ReplaceMixins; import com.google.common.css.compiler.passes.ResolveCustomFunctionNodes; import com.google.common.css.compiler.passes.SplitRulesetNodes; import com.google.common.io.Resources; import com.google.gwt.core.ext.BadPropertyValueException; import com.google.gwt.core.ext.ConfigurationProperty; import com.google.gwt.core.ext.Generator; import com.google.gwt.core.ext.PropertyOracle; import com.google.gwt.core.ext.SelectionProperty; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.TreeLogger.Type; import com.google.gwt.core.ext.UnableToCompleteException; import com.google.gwt.core.ext.typeinfo.JClassType; import com.google.gwt.core.ext.typeinfo.JMethod; import com.google.gwt.core.ext.typeinfo.JPrimitiveType; import com.google.gwt.core.ext.typeinfo.JType; import com.google.gwt.core.ext.typeinfo.NotFoundException; import com.google.gwt.core.ext.typeinfo.TypeOracle; import com.google.gwt.dev.util.Util; import com.google.gwt.i18n.client.LocaleInfo; import com.google.gwt.resources.client.CssResource; import com.google.gwt.resources.client.CssResource.ClassName; import com.google.gwt.resources.client.CssResource.Import; import com.google.gwt.resources.client.CssResource.ImportedWithPrefix; import com.google.gwt.resources.client.CssResource.NotStrict; import com.google.gwt.resources.client.CssResource.Shared; import com.google.gwt.resources.client.GssResource; import com.google.gwt.resources.client.ResourcePrototype; import com.google.gwt.resources.converter.Css2Gss; import com.google.gwt.resources.ext.ClientBundleRequirements; import com.google.gwt.resources.ext.ResourceContext; import com.google.gwt.resources.ext.ResourceGeneratorUtil; import com.google.gwt.resources.ext.SupportsGeneratorResultCaching; import com.google.gwt.resources.gss.CreateRuntimeConditionalNodes; import com.google.gwt.resources.gss.CssPrinter; import com.google.gwt.resources.gss.DisallowDefInsideRuntimeConditionalNode; 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.RuntimeConditionalNodeCollector; import com.google.gwt.resources.rg.CssResourceGenerator.JClassOrderComparator; 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.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.zip.Adler32; public class GssResourceGenerator extends AbstractCssResourceGenerator implements SupportsGeneratorResultCaching { /** * {@link ErrorManager} used to log the errors and warning messages produced by the different * {@link com.google.common.css.compiler.ast.CssCompilerPass} */ private static class LoggerErrorManager implements ErrorManager { private final TreeLogger logger; private boolean hasErrors; private LoggerErrorManager(TreeLogger logger) { this.logger = logger; } @Override public void generateReport() { // do nothing } @Override public boolean hasErrors() { return hasErrors; } @Override public void report(GssError error) { String fileName = ""; String location = ""; SourceCodeLocation codeLocation = error.getLocation(); if (codeLocation != null) { fileName = codeLocation.getSourceCode().getFileName(); location = "[line:" + codeLocation.getBeginLineNumber() + " column:" + codeLocation.getBeginIndexInLine() + "]"; } logger.log(Type.ERROR, "Error in " + fileName + location + ": " + error.getMessage()); hasErrors = true; } @Override public void reportWarning(GssError warning) { logger.log(Type.WARN, warning.getMessage()); } } // TODO rename this class private static class OptimizationInfo { final ConstantDefinitions constantDefinitions; private OptimizationInfo(ConstantDefinitions constantDefinitions) { this.constantDefinitions = constantDefinitions; } } private static class ExtendedCssTree { private final CssTree tree; private final List<String> permutationAxes; private ExtendedCssTree(CssTree tree, List<String> permutationAxis) { this.tree = tree; this.permutationAxes = permutationAxis; } public CssTree getCssTree() { return tree; } public List<String> getPermutationAxes() { return permutationAxes; } } private static final Cache<List<URL>, ExtendedCssTree> TREE_CACHE = CacheBuilder.newBuilder().softValues() .build(); private static final Cache<List<URL>, Long> LAST_MODIFIED_CACHE = CacheBuilder.newBuilder().build(); // TO be sure to avoid conflict during the style classes renaming between different GssResource, // we will create a different prefix for each GssResource. We use a MinimalSubstitutionMap // that will create a String with 1-6 characters in length but keeping the length of the prefix // as short as possible. For instance if we have two GssResources to compile, the prefix // for the first resource will be 'a' and the prefix for the second resource will be 'b' and so on private static final SubstitutionMap resourcePrefixBuilder = new MinimalSubstitutionMap(); private static final String KEY_LEGACY = "CssResource.legacy"; private static final String KEY_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' }; private static String encode(long id) { assert id >= 0; StringBuilder b = new StringBuilder(); // Use only guaranteed-alpha characters for the first character b.append(BASE32_CHARS[(int) (id & 0xf)]); id >>= 4; while (id != 0) { b.append(BASE32_CHARS[(int) (id & 0x1f)]); id >>= 5; } return b.toString(); } private Map<JMethod, ExtendedCssTree> cssTreeMap; private Set<String> allowedNonStandardFunctions; private LoggerErrorManager errorManager; private JMethod getTextMethod; private JMethod ensuredInjectedMethod; private JMethod getNameMethod; private String obfuscationPrefix; private CssObfuscationStyle obfuscationStyle; private Set<String> allowedAtRules; private Map<JClassType, Map<String, String>> replacementsByClassAndMethod; private Map<JMethod, String> replacementsForSharedMethods; private boolean allowLegacy; @Override public String createAssignment(TreeLogger logger, ResourceContext context, JMethod method) throws UnableToCompleteException { ExtendedCssTree extendedCssTree = cssTreeMap.get(method); Map<String, String> substitutionMap = doClassRenaming(extendedCssTree.getCssTree(), method, logger, context); // TODO : Should we foresee configuration properties for simplifyCss and eliminateDeadCode // booleans ? OptimizationInfo optimizationInfo = optimize(extendedCssTree, context, true, true); checkErrors(); SourceWriter sw = new StringSourceWriter(); sw.println("new " + method.getReturnType().getQualifiedSourceName() + "() {"); sw.indent(); writeMethods(logger, context, method, sw, optimizationInfo, substitutionMap); sw.outdent(); sw.println("}"); return sw.toString(); } @Override public void init(TreeLogger logger, ResourceContext context) throws UnableToCompleteException { cssTreeMap = new IdentityHashMap<JMethod, ExtendedCssTree>(); errorManager = new LoggerErrorManager(logger); allowedNonStandardFunctions = new HashSet<String>(); allowedAtRules = Sets.newHashSet(ExternalClassesCollector.EXTERNAL_AT_RULE); try { PropertyOracle propertyOracle = context.getGeneratorContext().getPropertyOracle(); ConfigurationProperty styleProp = propertyOracle.getConfigurationProperty(KEY_STYLE); obfuscationStyle = CssObfuscationStyle.getObfuscationStyle(styleProp.getValues().get(0)); obfuscationPrefix = getObfuscationPrefix(propertyOracle, context); ConfigurationProperty allowedAtRuleProperty = propertyOracle.getConfigurationProperty(ALLOWED_AT_RULE); allowedAtRules.addAll(allowedAtRuleProperty.getValues()); ConfigurationProperty allowedFunctionsProperty = propertyOracle .getConfigurationProperty(ALLOWED_FUNCTIONS); allowedNonStandardFunctions.addAll(allowedFunctionsProperty.getValues()); allowLegacy = "true".equals(propertyOracle.getConfigurationProperty(KEY_LEGACY).getValues().get(0)); ClientBundleRequirements requirements = context.getRequirements(); requirements.addConfigurationProperty(KEY_STYLE); requirements.addConfigurationProperty(KEY_OBFUSCATION_PREFIX); } catch (BadPropertyValueException e) { logger.log(TreeLogger.ERROR, "Unable to query module property", e); throw new UnableToCompleteException(); } TypeOracle typeOracle = context.getGeneratorContext().getTypeOracle(); JClassType cssResourceInterface = typeOracle.findType(CssResource.class.getCanonicalName()); JClassType resourcePrototypeInterface = typeOracle.findType(ResourcePrototype.class.getCanonicalName()); try { getTextMethod = cssResourceInterface.getMethod("getText", new JType[0]); ensuredInjectedMethod = cssResourceInterface.getMethod("ensureInjected", new JType[0]); getNameMethod = resourcePrototypeInterface.getMethod("getName", new JType[0]); } catch (NotFoundException e) { logger.log(TreeLogger.ERROR, "Unable to lookup methods from CssResource and " + "ResourcePrototype interface", e); throw new UnableToCompleteException(); } initReplacement(context); } private void initReplacement(ResourceContext context) { if (context.getCachedData(KEY_HAS_CACHED_DATA, Boolean.class) != Boolean.TRUE) { context.putCachedData(KEY_SHARED_METHODS, new IdentityHashMap<JMethod, String>()); context.putCachedData(KEY_BY_CLASS_AND_METHOD, new IdentityHashMap<JClassType, Map<String, String>>()); context.putCachedData(KEY_HAS_CACHED_DATA, Boolean.TRUE); } replacementsByClassAndMethod = context.getCachedData(KEY_BY_CLASS_AND_METHOD, Map.class); replacementsForSharedMethods = context.getCachedData(KEY_SHARED_METHODS, Map.class); } private String getObfuscationPrefix(PropertyOracle propertyOracle, ResourceContext context) throws BadPropertyValueException { String prefix = propertyOracle.getConfigurationProperty(KEY_OBFUSCATION_PREFIX).getValues().get(0); if ("empty".equalsIgnoreCase(prefix)) { return ""; } else if ("default".equalsIgnoreCase(prefix)) { return getDefaultObfuscationPrefix(context); } return prefix; } private String getDefaultObfuscationPrefix(ResourceContext context) { String prefix = context.getCachedData(KEY_CLASS_PREFIX, String.class); if (prefix == null) { prefix = computeDefaultPrefix(context); context.putCachedData(KEY_CLASS_PREFIX, prefix); } return prefix; } private String computeDefaultPrefix(ResourceContext context) { SortedSet<JClassType> gssResources = computeOperableTypes(context); Adler32 checksum = new Adler32(); for (JClassType type : gssResources) { checksum.update(Util.getBytes(type.getQualifiedSourceName())); } int seed = Math.abs((int) checksum.getValue()); return encode(seed) + "-"; } private SortedSet<JClassType> computeOperableTypes(ResourceContext context) { TypeOracle typeOracle = context.getGeneratorContext().getTypeOracle(); JClassType baseInterface = typeOracle.findType(GssResource.class.getCanonicalName()); SortedSet<JClassType> toReturn = new TreeSet<JClassType>(new JClassOrderComparator()); JClassType[] cssResourceSubtypes = baseInterface.getSubtypes(); for (JClassType type : cssResourceSubtypes) { if (type.isInterface() != null) { toReturn.add(type); } } return toReturn; } @Override public void prepare(final TreeLogger logger, final ResourceContext context, ClientBundleRequirements requirements, JMethod method) throws UnableToCompleteException { if (method.getReturnType().isInterface() == null) { logger.log(TreeLogger.ERROR, "Return type must be an interface"); throw new UnableToCompleteException(); } URL[] resourceUrls = ResourceGeneratorUtil.findResources(logger, context, method); if (resourceUrls.length == 0) { logger.log(TreeLogger.ERROR, "At least one source must be specified"); throw new UnableToCompleteException(); } final long lastModified = ResourceGeneratorUtil.getLastModified(resourceUrls, logger); final List<URL> resources = Lists.newArrayList(resourceUrls); maybeInvalidateCacheFor(resources, lastModified, logger); ExtendedCssTree extTree; try { extTree = TREE_CACHE.get(resources, new Callable<ExtendedCssTree>() { @Override public ExtendedCssTree call() throws Exception { ExtendedCssTree tree = parseResources(resources, logger); // add last modified time in cache LAST_MODIFIED_CACHE.put(resources, lastModified); return tree; } }); } catch (ExecutionException e) { if (e.getCause() instanceof UnableToCompleteException) { throw (UnableToCompleteException) e.getCause(); } else { logger.log(Type.ERROR, "Unexpected error occurred", e.getCause()); throw new UnableToCompleteException(); } } ExtendedCssTree finalTree = new ExtendedCssTree(deepCopy(extTree.getCssTree()), extTree.getPermutationAxes()); cssTreeMap.put(method, finalTree); for (String permutationAxis : extTree.getPermutationAxes()) { 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 = cssTreeMap.get(method).getCssTree(); 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 CssTree deepCopy(CssTree cssTree) { return new CssTree(cssTree.getSourceCode(), cssTree.getRoot().deepCopy()); } private Map<String, String> doClassRenaming(CssTree cssTree, JMethod method, TreeLogger logger, ResourceContext context) throws UnableToCompleteException { Map<String, Map<String, String>> replacementsWithPrefix = computeReplacements(method, logger, context); Set<String> externalClasses = collectExternalClasses(cssTree); RenamingSubstitutionMap substitutionMap = new RenamingSubstitutionMap(replacementsWithPrefix, externalClasses, isStrictResource(method), logger); new CssClassRenaming(cssTree.getMutatingVisitController(), substitutionMap, null).runPass(); if (substitutionMap.hasError()) { throw new UnableToCompleteException(); } Map<String, String> mapping = replacementsWithPrefix.get(""); mapping = Maps.newHashMap(Maps.filterKeys(mapping, Predicates.in(substitutionMap.getStyleClasses()))); // add external classes in the mapping for (String external : externalClasses) { mapping.put(external, external); } return mapping; } private boolean isStrictResource(JMethod method) { NotStrict notStrict = method.getAnnotation(NotStrict.class); return notStrict == null; } private Set<String> collectExternalClasses(CssTree cssTree) { ExternalClassesCollector externalClassesCollector = new ExternalClassesCollector( cssTree.getMutatingVisitController()); externalClassesCollector.runPass(); return externalClassesCollector.getExternalClassNames(); } private List<String> finalizeTree(CssTree cssTree) throws UnableToCompleteException { new CheckDependencyNodes(cssTree.getMutatingVisitController(), errorManager, false).runPass(); // Don't continue if errors exist checkErrors(); new CreateStandardAtRuleNodes(cssTree.getMutatingVisitController(), errorManager).runPass(); new CreateMixins(cssTree.getMutatingVisitController(), errorManager).runPass(); new CreateDefinitionNodes(cssTree.getMutatingVisitController(), errorManager).runPass(); new CreateConstantReferences(cssTree.getMutatingVisitController()).runPass(); new CreateConditionalNodes(cssTree.getMutatingVisitController(), errorManager).runPass(); new CreateRuntimeConditionalNodes(cssTree.getMutatingVisitController()).runPass(); new CreateComponentNodes(cssTree.getMutatingVisitController(), errorManager).runPass(); new HandleUnknownAtRuleNodes(cssTree.getMutatingVisitController(), errorManager, allowedAtRules, true, false).runPass(); new ProcessKeyframes(cssTree.getMutatingVisitController(), errorManager, true, true).runPass(); new ProcessRefiners(cssTree.getMutatingVisitController(), errorManager, true).runPass(); PermutationsCollector permutationsCollector = new PermutationsCollector( cssTree.getMutatingVisitController(), errorManager); permutationsCollector.runPass(); return permutationsCollector.getPermutationAxes(); } private void maybeInvalidateCacheFor(List<URL> resources, long lastModified, TreeLogger logger) { Long lastModifiedFromCache = LAST_MODIFIED_CACHE.getIfPresent(resources); if (lastModifiedFromCache == null || lastModified == 0 || (lastModified > lastModifiedFromCache)) { TREE_CACHE.invalidate(resources); } } private OptimizationInfo optimize(ExtendedCssTree extendedCssTree, ResourceContext context, boolean simplifyCss, boolean eliminateDeadStyles) throws UnableToCompleteException { CssTree cssTree = extendedCssTree.getCssTree(); // Collect mixin definitions and replace mixins CollectMixinDefinitions collectMixinDefinitions = new CollectMixinDefinitions( cssTree.getMutatingVisitController(), errorManager); collectMixinDefinitions.runPass(); new ReplaceMixins(cssTree.getMutatingVisitController(), errorManager, collectMixinDefinitions.getDefinitions()).runPass(); new ProcessComponents<Object>(cssTree.getMutatingVisitController(), errorManager).runPass(); RuntimeConditionalNodeCollector runtimeConditionalNodeCollector = new RuntimeConditionalNodeCollector( cssTree.getVisitController()); runtimeConditionalNodeCollector.runPass(); new ExtendedEliminateConditionalNodes(cssTree.getMutatingVisitController(), getPermutationsConditions(context, extendedCssTree.getPermutationAxes()), runtimeConditionalNodeCollector.getRuntimeConditionalNodes()).runPass(); new DisallowDefInsideRuntimeConditionalNode(cssTree.getVisitController(), errorManager).runPass(); // Don't continue if errors exist checkErrors(); CollectConstantDefinitions collectConstantDefinitionsPass = new CollectConstantDefinitions(cssTree); collectConstantDefinitionsPass.runPass(); ReplaceConstantReferences replaceConstantReferences = new ReplaceConstantReferences(cssTree, collectConstantDefinitionsPass.getConstantDefinitions(), true, errorManager, false); replaceConstantReferences.runPass(); new ImageSpriteCreator(cssTree.getMutatingVisitController(), context, errorManager).runPass(); Map<String, GssFunction> gssFunctionMap = new GwtGssFunctionMapProvider(context).get(); new ResolveCustomFunctionNodes(cssTree.getMutatingVisitController(), errorManager, gssFunctionMap, true, allowedNonStandardFunctions).runPass(); if (simplifyCss) { // Eliminate empty rules. new EliminateEmptyRulesetNodes(cssTree.getMutatingVisitController()).runPass(); // Eliminating units for zero values. new EliminateUnitsFromZeroNumericValues(cssTree.getMutatingVisitController()).runPass(); // Optimize color values. new ColorValueOptimizer(cssTree.getMutatingVisitController()).runPass(); // Compress redundant top-right-bottom-left value lists. new AbbreviatePositionalValues(cssTree.getMutatingVisitController()).runPass(); } if (eliminateDeadStyles) { // Report errors for duplicate declarations new DisallowDuplicateDeclarations(cssTree.getVisitController(), errorManager).runPass(); // Split rules by selector and declaration. new SplitRulesetNodes(cssTree.getMutatingVisitController()).runPass(); // Dead code elimination. new MarkRemovableRulesetNodes(cssTree).runPass(); new EliminateUselessRulesetNodes(cssTree).runPass(); // Merge of rules with same selector. new MergeAdjacentRulesetNodesWithSameSelector(cssTree).runPass(); new EliminateUselessRulesetNodes(cssTree).runPass(); // Merge of rules with same styles. new MergeAdjacentRulesetNodesWithSameDeclarations(cssTree).runPass(); new EliminateUselessRulesetNodes(cssTree).runPass(); } return new OptimizationInfo(collectConstantDefinitionsPass.getConstantDefinitions()); } private Set<String> getPermutationsConditions(ResourceContext context, List<String> permutationAxes) { Builder<String> setBuilder = ImmutableSet.builder(); PropertyOracle oracle = context.getGeneratorContext().getPropertyOracle(); for (String permutationAxis : permutationAxes) { String propValue = null; try { SelectionProperty selProp = oracle.getSelectionProperty(null, permutationAxis); propValue = selProp.getCurrentValue(); } catch (BadPropertyValueException e) { try { ConfigurationProperty confProp = oracle.getConfigurationProperty(permutationAxis); propValue = confProp.getValues().get(0); } catch (BadPropertyValueException e1) { e1.printStackTrace(); } } if (propValue != null) { setBuilder.add(permutationAxis + ":" + propValue); } } return setBuilder.build(); } private ExtendedCssTree parseResources(List<URL> resources, TreeLogger logger) throws UnableToCompleteException { List<SourceCode> sourceCodes = new ArrayList<SourceCode>(resources.size()); // assert that we only support either gss or css on one resource. boolean css = ensureEitherCssOrGss(resources, logger); if (css && !allowLegacy) { // TODO(dankurka): add link explaining the situation in detail. logger.log(Type.ERROR, "Your ClientBundle is referencing css files instead of gss. " + "You will need to either convert these files to gss using the " + "converter tool or turn on auto convertion in your gwt.xml file. " + "Note: Autoconversion will be removed after 2.7, you will need to move to gss." + "Add this line to your gwt.xml file to temporary avoid this:" + "<set-configuration-property name=\"CssResource.legacy\" value=\"true\" />"); throw new UnableToCompleteException(); } if (css) { String concatenatedCss = concatCssFiles(resources, logger); String gss = convertToGss(concatenatedCss, logger); sourceCodes.add(new SourceCode("[auto-converted gss files]", gss)); } else { for (URL stylesheet : resources) { TreeLogger branchLogger = logger.branch(TreeLogger.DEBUG, "Parsing GSS stylesheet " + stylesheet.toExternalForm()); try { // TODO : always use UTF-8 to read the file ? String fileContent = Resources.asByteSource(stylesheet).asCharSource(Charsets.UTF_8).read(); sourceCodes.add(new SourceCode(stylesheet.getFile(), fileContent)); continue; } catch (IOException e) { branchLogger.log(TreeLogger.ERROR, "Unable to parse CSS", e); } throw new UnableToCompleteException(); } } CssTree tree; try { tree = new GssParser(sourceCodes).parse(); } catch (GssParserException e) { logger.log(TreeLogger.ERROR, "Unable to parse CSS", e); throw new UnableToCompleteException(); } List<String> permutationAxes = finalizeTree(tree); checkErrors(); return new ExtendedCssTree(tree, permutationAxes); } private String convertToGss(String concatenatedCss, TreeLogger logger) throws UnableToCompleteException { File tempFile = null; FileOutputStream fos = null; try { // We actually need a URL for the old CssResource to work. So create a temp file. tempFile = File.createTempFile(UUID.randomUUID() + "css_converter", "css.tmp"); fos = new FileOutputStream(tempFile); IOUtils.write(concatenatedCss, fos); fos.close(); return new Css2Gss(tempFile.toURI().toURL(), new PrintWriter(System.err), true).toGss(); } catch (IOException e) { logger.log(Type.ERROR, "Error while writing temporary css file", e); throw new UnableToCompleteException(); } finally { if (tempFile != null) { tempFile.delete(); } if (fos != null) { IOUtils.closeQuietly(fos); } } } private String concatCssFiles(List<URL> resources, TreeLogger logger) throws UnableToCompleteException { StringBuffer buffer = new StringBuffer(); for (URL stylesheet : resources) { try { String fileContent = Resources.asByteSource(stylesheet).asCharSource(Charsets.UTF_8).read(); buffer.append(fileContent); buffer.append("\n"); } catch (IOException e) { logger.log(TreeLogger.ERROR, "Unable to parse CSS", e); throw new UnableToCompleteException(); } } return buffer.toString(); } private boolean ensureEitherCssOrGss(List<URL> resources, TreeLogger logger) throws UnableToCompleteException { boolean css = resources.get(0).toString().endsWith(".css"); for (URL stylesheet : resources) { if (css && !stylesheet.toString().endsWith(".css")) { logger.log(Type.ERROR, "Only either css files or gss files are supported on one interface"); throw new UnableToCompleteException(); } else if (!css && !stylesheet.toString().endsWith(".gss")) { logger.log(Type.ERROR, "Only either css files or gss files are supported on one interface"); throw new UnableToCompleteException(); } } return css; } private String printCssTree(CssTree tree) { CssPrinter cssPrinterPass = new CssPrinter(tree); cssPrinterPass.runPass(); return cssPrinterPass.getCompactPrintedString(); } private boolean writeClassMethod(TreeLogger logger, JMethod userMethod, Map<String, String> substitutionMap, SourceWriter sw) throws UnableToCompleteException { if (!isReturnTypeString(userMethod.getReturnType().isClass())) { logger.log(Type.ERROR, "The return type of the method [" + userMethod.getName() + "] must " + "be java.lang.String."); throw new UnableToCompleteException(); } if (userMethod.getParameters().length > 0) { logger.log(Type.ERROR, "The method [" + userMethod.getName() + "] shouldn't contain any " + "parameters"); throw new UnableToCompleteException(); } String name = getClassName(userMethod); String value = substitutionMap.get(name); if (value == null) { logger.log(Type.ERROR, "The following style class [" + name + "] is missing from the source" + " CSS file"); return false; } else { writeSimpleGetter(userMethod, "\"" + value + "\"", sw); } return true; } private String getClassName(JMethod method) { String name = method.getName(); ClassName classNameOverride = method.getAnnotation(ClassName.class); if (classNameOverride != null) { name = classNameOverride.value(); } return name; } private boolean writeDefMethod(CssDefinitionNode definitionNode, TreeLogger logger, JMethod userMethod, SourceWriter sw) throws UnableToCompleteException { String name = userMethod.getName(); JClassType classReturnType = userMethod.getReturnType().isClass(); List<CssValueNode> params = definitionNode.getParameters(); if (params.size() != 1 && !isReturnTypeString(classReturnType)) { logger.log(TreeLogger.ERROR, "@def rule " + name + " must define exactly one value or return type must be String"); return false; } String returnExpr; if (isReturnTypeString(classReturnType)) { List<String> returnValues = new ArrayList<String>(); for (CssValueNode valueNode : params) { returnValues.add(Generator.escape(valueNode.toString())); } returnExpr = "\"" + Joiner.on(" ").join(returnValues) + "\""; } else { JPrimitiveType returnType = userMethod.getReturnType().isPrimitive(); if (returnType == null) { logger.log(TreeLogger.ERROR, name + ": Return type must be primitive type " + "or String for @def accessors"); return false; } CssValueNode valueNode = params.get(0); if (!(valueNode instanceof CssNumericNode)) { logger.log(TreeLogger.ERROR, "The value of the constant defined by @" + name + " is not a" + " numeric"); return false; } String numericValue = ((CssNumericNode) valueNode).getNumericPart(); if (returnType == JPrimitiveType.INT || returnType == JPrimitiveType.LONG) { returnExpr = "" + Long.parseLong(numericValue); } else if (returnType == JPrimitiveType.FLOAT) { returnExpr = numericValue + "F"; } else if (returnType == JPrimitiveType.DOUBLE) { returnExpr = "" + numericValue; } else { logger.log(TreeLogger.ERROR, returnType.getQualifiedSourceName() + " is not a valid primitive return type for @def accessors"); return false; } } writeSimpleGetter(userMethod, returnExpr, sw); return true; } private void writeMethods(TreeLogger logger, ResourceContext context, JMethod method, SourceWriter sw, OptimizationInfo optimizationInfo, Map<String, String> substitutionMap) throws UnableToCompleteException { JClassType gssResource = method.getReturnType().isInterface(); boolean success = true; for (JMethod toImplement : gssResource.getOverridableMethods()) { if (toImplement == getTextMethod) { writeGetText(logger, context, method, sw); } else if (toImplement == ensuredInjectedMethod) { writeEnsureInjected(sw); } else if (toImplement == getNameMethod) { writeGetName(method, sw); } else { success &= writeUserMethod(logger, toImplement, sw, optimizationInfo, substitutionMap); } } if (!success) { throw new UnableToCompleteException(); } } private boolean writeUserMethod(TreeLogger logger, JMethod userMethod, SourceWriter sw, OptimizationInfo optimizationInfo, Map<String, String> substitutionMap) throws UnableToCompleteException { String className = getClassName(userMethod); // method to access style class ? if (substitutionMap.containsKey(className)) { return writeClassMethod(logger, userMethod, substitutionMap, sw); } // method to access constant value ? ConstantDefinitions constantDefinitions = optimizationInfo.constantDefinitions; CssDefinitionNode definitionNode = constantDefinitions.getConstantDefinition(userMethod.getName()); if (definitionNode == null) { // try with upper case definitionNode = constantDefinitions.getConstantDefinition(toUpperCase(userMethod.getName())); } if (definitionNode != null) { return writeDefMethod(definitionNode, logger, userMethod, sw); } // the method doesn't match a style class nor a constant logger.log(Type.ERROR, "The following method [" + userMethod.getName() + "()] doesn't match a constant" + " nor a style class. You could fix that by adding ." + className + " {}"); return false; } /** * Transform a camel case string to upper case. Each word is separated by a '_' * * @param camelCase * @return */ private String toUpperCase(String camelCase) { return CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, camelCase); } private Map<String, Map<String, String>> computeReplacements(JMethod method, TreeLogger logger, ResourceContext context) throws UnableToCompleteException { Map<String, Map<String, String>> replacementsWithPrefix = new HashMap<String, Map<String, String>>(); replacementsWithPrefix.put("", computeReplacementsForType(method.getReturnType().isInterface())); // Process the Import annotation if any Import imp = method.getAnnotation(Import.class); if (imp != null) { boolean fail = false; TypeOracle typeOracle = context.getGeneratorContext().getTypeOracle(); for (Class<? extends CssResource> clazz : imp.value()) { JClassType importType = typeOracle.findType(clazz.getName().replace('$', '.')); assert importType != null : "TypeOracle does not have type " + clazz.getName(); // add this import type as a requirement for this generator context.getRequirements().addTypeHierarchy(importType); String prefix = getImportPrefix(importType); if (replacementsWithPrefix.put(prefix, computeReplacementsForType(importType)) != null) { logger.log(TreeLogger.ERROR, "Multiple imports that would use the prefix " + prefix); fail = true; } } if (fail) { throw new UnableToCompleteException(); } } return replacementsWithPrefix; } private Map<String, String> computeReplacementsForType(JClassType cssResource) { Map<String, String> replacements = replacementsByClassAndMethod.get(cssResource); if (replacements == null) { replacements = new HashMap<String, String>(); replacementsByClassAndMethod.put(cssResource, replacements); String resourcePrefix = resourcePrefixBuilder.get(cssResource.getQualifiedSourceName()); // This substitution map will prefix each renamed class with the resource prefix and use a // MinimalSubstitutionMap for computing the obfuscated name. SubstitutionMap prefixingSubstitutionMap = new PrefixingSubstitutionMap(new MinimalSubstitutionMap(), obfuscationPrefix + resourcePrefix + "-"); for (JMethod method : cssResource.getOverridableMethods()) { if (method == getNameMethod || method == getTextMethod || method == ensuredInjectedMethod) { continue; } String styleClass = getClassName(method); if (replacementsForSharedMethods.containsKey(method)) { replacements.put(styleClass, replacementsForSharedMethods.get(method)); } else { String obfuscatedClassName = prefixingSubstitutionMap.get(styleClass); String replacement = obfuscationStyle.getPrettyName(styleClass, cssResource, obfuscatedClassName); replacements.put(styleClass, replacement); maybeHandleSharedMethod(method, replacement); } } } return replacements; } private void maybeHandleSharedMethod(JMethod method, String obfuscatedClassName) { JClassType enclosingType = method.getEnclosingType(); Shared shared = enclosingType.getAnnotation(Shared.class); if (shared != null) { replacementsForSharedMethods.put(method, obfuscatedClassName); } } /** * Returns the import prefix for a type, including the trailing hyphen. */ private String getImportPrefix(JClassType importType) { String prefix = importType.getSimpleSourceName(); ImportedWithPrefix exp = importType.getAnnotation(ImportedWithPrefix.class); if (exp != null) { prefix = exp.value(); } return prefix + "-"; } }