com.google.gwt.resources.rg.GssResourceGenerator.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.resources.rg.GssResourceGenerator.java

Source

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