Skip to content
This repository has been archived by the owner on Feb 23, 2022. It is now read-only.

Commit

Permalink
Rework substitution maps to allow reusing them across compiles.
Browse files Browse the repository at this point in the history
I need to be able to compile multiple CSS bundles using a rename map,
then recompile just a few based on hash checking.

There are two problems with using CssCompiler.execute(renameFile, ...).
1. CssCompiler writes out the rename map using only the entries actually
   encountered by keeping a side table of mappings, not getting the
   entire mapping from the SubstitutionMap.
2. MinimalSubstitutionMap and others have no standard way to reconstitute
   a substitution map from a string->string relation.

This addresses both problems.

I added read() methods to OutputRenamingMapFormat.

I added an Initializable interface to SubstitutionMap that allows
reconstituting a renamer from a rename map, and reworked
RecordingSubstitutionMap and friends to implement Initializable.

Along the way, I cleared up an ambiguity in how SplittingSubstitutionMap
& RecodingSubstitutionMap interpreted renamings that caused problems
when a prefixing substitution map effectively prefixed only the first
renaming in a chain.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=131714015
  • Loading branch information
msamuel authored and iflan committed Aug 30, 2016
1 parent 5a88d3c commit d4476e5
Show file tree
Hide file tree
Showing 7 changed files with 459 additions and 85 deletions.
13 changes: 10 additions & 3 deletions src/com/google/common/css/MinimalSubstitutionMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;

import java.util.Arrays;
import java.util.Map;
import java.util.Set;
Expand All @@ -31,7 +30,7 @@
*
* @author bolinfest@google.com (Michael Bolin)
*/
public class MinimalSubstitutionMap implements SubstitutionMap {
public class MinimalSubstitutionMap implements SubstitutionMap.Initializable {

/** Possible first chars in a CSS class name */
private static final char[] START_CHARS = {
Expand Down Expand Up @@ -91,7 +90,7 @@ public class MinimalSubstitutionMap implements SubstitutionMap {
/**
* A set of CSS class names that may not be output from this substitution map.
*/
private final Set<String> outputValueBlacklist;
private ImmutableSet<String> outputValueBlacklist;

public MinimalSubstitutionMap() {
this(ImmutableSet.<String>of());
Expand Down Expand Up @@ -156,6 +155,14 @@ public String get(String key) {
return value;
}

@Override
public void initializeWithMappings(Map<? extends String, ? extends String> m) {
Preconditions.checkState(renamedCssClasses.isEmpty());
this.outputValueBlacklist =
ImmutableSet.<String>builder().addAll(outputValueBlacklist).addAll(m.values()).build();
this.renamedCssClasses.putAll(m);
}

/**
* Converts a 32-bit integer to a unique short string whose first character
* is in {@link #START_CHARS} and whose subsequent characters, if any, are
Expand Down
252 changes: 208 additions & 44 deletions src/com/google/common/css/OutputRenamingMapFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,25 @@

package com.google.common.css;


import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.escape.CharEscaperBuilder;
import com.google.common.escape.Escaper;
import com.google.common.io.CharStreams;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import com.google.gson.JsonParser;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
Expand Down Expand Up @@ -52,36 +63,14 @@ public enum OutputRenamingMapFormat {
CLOSURE_COMPILED_BY_WHOLE("goog.setCssNameMapping(%s, 'BY_WHOLE');\n"),

/**
* Before writing the mapping as CLOSURE_COMPILED, split the css name maps
* by hyphens and write out each piece individually. see
* {@code CLOSURE_COMPILED}
* Before writing the mapping as CLOSURE_COMPILED, split the css name maps by hyphens and write
* out each piece individually. see {@code CLOSURE_COMPILED}
*/
CLOSURE_COMPILED_SPLIT_HYPHENS {
CLOSURE_COMPILED_SPLIT_HYPHENS("goog.setCssNameMapping(%s);\n") {
@Override
public void writeRenamingMap(Map<String, String> renamingMap,
PrintWriter renamingMapWriter) {
Map<String, String> newSplitRenamingMap = Maps.newHashMap();
for (Map.Entry<String, String> entry : renamingMap.entrySet()) {
Iterator<String> parts =
HYPHEN_SPLITTER.split(entry.getKey()).iterator();
Iterator<String> partsNew =
HYPHEN_SPLITTER.split(entry.getValue()).iterator();
while (parts.hasNext() && partsNew.hasNext()) {
newSplitRenamingMap.put(parts.next(), partsNew.next());
}
if (parts.hasNext()) {
throw new AssertionError("Not all parts of the original class "
+ "name were output. Class: " + entry.getKey() + " Next Part:"
+ parts.next());
}
if (partsNew.hasNext()) {
throw new AssertionError("Not all parts of the renamed class were "
+ "output. Class: " + entry.getKey() + " Renamed Class: "
+ entry.getValue() + " Next Part:" + partsNew.next());
}
}
OutputRenamingMapFormat.CLOSURE_COMPILED.writeRenamingMap(
newSplitRenamingMap, renamingMapWriter);
public void writeRenamingMap(Map<String, String> renamingMap, Writer renamingMapWriter)
throws IOException {
super.writeRenamingMap(splitEntriesOnHyphens(renamingMap), renamingMapWriter);
}
},

Expand All @@ -103,17 +92,22 @@ public void writeRenamingMap(Map<String, String> renamingMap,
*/
PROPERTIES {
@Override
public void writeRenamingMap(Map<String, String> renamingMap,
PrintWriter renamingMapWriter) {
public void writeRenamingMap(Map<String, String> renamingMap, Writer renamingMapWriter)
throws IOException {
writeOnePerLine('=', renamingMap, renamingMapWriter);
// We write the properties directly rather than using
// Properties#store() because it is impossible to suppress the timestamp
// comment: http://goo.gl/6hsrN. As noted on the Stack Overflow thread,
// the timestamp results in unnecessary diffs between runs. Further, those
// who are using a language other than Java to parse this file should not
// have to worry about adding support for comments.
for (Map.Entry<String, String> entry : renamingMap.entrySet()) {
renamingMapWriter.format("%s=%s\n", entry.getKey(), entry.getValue());
}
}

@Override
void readMapInto(
BufferedReader in, ImmutableMap.Builder<? super String, ? super String> builder)
throws IOException {
readOnePerLine('=', in, builder);
}
},

Expand All @@ -123,16 +117,18 @@ public void writeRenamingMap(Map<String, String> renamingMap,
*/
JSCOMP_VARIABLE_MAP {
@Override
public void writeRenamingMap(Map<String, String> renamingMap,
PrintWriter renamingMapWriter) {
for (Map.Entry<String, String> entry : renamingMap.entrySet()) {
renamingMapWriter.format("%s:%s\n", entry.getKey(), entry.getValue());
}
public void writeRenamingMap(Map<String, String> renamingMap, Writer renamingMapWriter)
throws IOException {
writeOnePerLine(':', renamingMap, renamingMapWriter);
}
};

// Splitter used for CLOSURE_COMPILED_SPLIT_HYPHENS format.
private static final Splitter HYPHEN_SPLITTER = Splitter.on("-");
@Override
void readMapInto(
BufferedReader in, ImmutableMap.Builder<? super String, ? super String> builder)
throws IOException {
readOnePerLine(':', in, builder);
}
};

private final String formatString;

Expand All @@ -146,11 +142,13 @@ private OutputRenamingMapFormat() {
}

/**
* Writes the renaming map.
*
* @see com.google.common.css.compiler.commandline.DefaultCommandLineCompiler
* #writeRenamingMap(Map, PrintWriter)
*/
public void writeRenamingMap(Map<String, String> renamingMap,
PrintWriter renamingMapWriter) {
public void writeRenamingMap(Map<String, String> renamingMap, Writer renamingMapWriter)
throws IOException {
// Build up the renaming map as a JsonObject.
JsonObject properties = new JsonObject();
for (Map.Entry<String, String> entry : renamingMap.entrySet()) {
Expand All @@ -162,4 +160,170 @@ public void writeRenamingMap(Map<String, String> renamingMap,
renamingMapWriter.write(String.format(formatString,
gson.toJson(properties)));
}

/**
* Like {@writeRenamingMap(java.util.Map, java.io.Writer)} but does not throw when writes fail.
*/
public final void writeRenamingMap(
Map<String, String> renamingMap, PrintWriter renamingMapWriter) {
try {
writeRenamingMap(renamingMap, (Writer) renamingMapWriter);
} catch (IOException ex) {
throw (AssertionError) new AssertionError("IOException from PrintWriter").initCause(ex);
}
}

/**
* Reads the output of {@link #writeRenamingMap} so a renaming map can be reused from one compile
* to another.
*/
public ImmutableMap<String, String> readRenamingMap(Reader in) throws IOException {
String subsitutionMarker = "%s";
int formatStringSubstitutionIndex = formatString.indexOf(subsitutionMarker);
Preconditions.checkState(formatStringSubstitutionIndex >= 0, formatString);

String formatPrefix = formatString.substring(0, formatStringSubstitutionIndex);
String formatSuffix =
formatString.substring(formatStringSubstitutionIndex + subsitutionMarker.length());

// GSON's JSONParser does not stop reading bytes when it sees a bracket that
// closes the value.
// We read the whole input in, then strip prefixes and suffixes and then parse
// the rest.
String content = CharStreams.toString(in);

content = content.trim();
formatPrefix = formatPrefix.trim();
formatSuffix = formatSuffix.trim();

if (!content.startsWith(formatPrefix)
|| !content.endsWith(formatSuffix)
|| content.length() < formatPrefix.length() + formatSuffix.length()) {
throw new IOException("Input does not match format " + formatString + " : " + content);
}

content = content.substring(formatPrefix.length(), content.length() - formatSuffix.length());

ImmutableMap.Builder<String, String> b = ImmutableMap.builder();
BufferedReader br = new BufferedReader(new StringReader(content));
readMapInto(br, b);
requireEndOfInput(br);

return b.build();
}

/**
* Reads the mapping portion of the formatted output.
*
* <p>This default implementation works for formats that substitute a JSON mapping from rewritten
* names to originals into their format string, and may be overridden by formats that do something
* different.
*/
void readMapInto(BufferedReader in, ImmutableMap.Builder<? super String, ? super String> builder)
throws IOException {
JsonElement json = new JsonParser().parse(in);
for (Map.Entry<String, JsonElement> e : json.getAsJsonObject().entrySet()) {
builder.put(e.getKey(), e.getValue().getAsString());
}
}

/**
* Raises an IOException if there are any non-space characters on in, and consumes the remaining
* characters on in.
*/
private static void requireEndOfInput(BufferedReader in) throws IOException {
for (int ch; (ch = in.read()) >= 0; ) {
if (!Character.isSpace((char) ch)) {
throw new IOException("Expected end of input, not '" + escape((char) ch) + "'");
}
}
}

private static final Escaper ESCAPER =
new CharEscaperBuilder()
.addEscape('\t', "\\t")
.addEscape('\n', "\\n")
.addEscape('\r', "\\r")
.addEscape('\\', "\\\\")
.addEscape('\'', "\\'")
.toEscaper();

private static String escape(char ch) {
return ESCAPER.escape(new String(new char[] {ch}));
}

/** Splitter used for CLOSURE_COMPILED_SPLIT_HYPHENS format. */
private static final Splitter HYPHEN_SPLITTER = Splitter.on("-");

/**
* <code>{ "foo-bar": "f-b" }</code> => <code>{ "foo": "f", "bar": "b" }</code>.
*
* @see SplittingSubstitutionMap
*/
private static Map<String, String> splitEntriesOnHyphens(Map<String, String> renamingMap) {
Map<String, String> newSplitRenamingMap = Maps.newLinkedHashMap();
for (Map.Entry<String, String> entry : renamingMap.entrySet()) {
Iterator<String> keyParts = HYPHEN_SPLITTER.split(entry.getKey()).iterator();
Iterator<String> valueParts = HYPHEN_SPLITTER.split(entry.getValue()).iterator();
while (keyParts.hasNext() && valueParts.hasNext()) {
String keyPart = keyParts.next();
String valuePart = valueParts.next();
String oldValuePart = newSplitRenamingMap.put(keyPart, valuePart);
// Splitting by part to make a simple map shouldn't involve mapping two old names
// to the same new name. It's ok the other way around, but the part relation should
// be a partial function.
Preconditions.checkState(oldValuePart == null || oldValuePart.equals(valuePart));
}
if (keyParts.hasNext()) {
throw new AssertionError(
"Not all parts of the original class "
+ "name were output. Class: "
+ entry.getKey()
+ " Next Part:"
+ keyParts.next());
}
if (valueParts.hasNext()) {
throw new AssertionError(
"Not all parts of the renamed class were "
+ "output. Class: "
+ entry.getKey()
+ " Renamed Class: "
+ entry.getValue()
+ " Next Part:"
+ valueParts.next());
}
}
return newSplitRenamingMap;
}

private static void writeOnePerLine(
char separator, Map<String, String> renamingMap, Writer renamingMapWriter)
throws IOException {
for (Map.Entry<String, String> entry : renamingMap.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
Preconditions.checkState(key.indexOf(separator) < 0);
Preconditions.checkState(key.indexOf('\n') < 0);
Preconditions.checkState(value.indexOf('\n') < 0);

renamingMapWriter.write(key);
renamingMapWriter.write(separator);
renamingMapWriter.write(value);
renamingMapWriter.write('\n');
}
}

private static void readOnePerLine(
char separator,
BufferedReader in,
ImmutableMap.Builder<? super String, ? super String> builder)
throws IOException {
for (String line; (line = in.readLine()) != null; ) {
int eq = line.indexOf(separator);
if (eq < 0 && !line.isEmpty()) {
throw new IOException("Line is missing a '" + separator + "': " + line);
}
builder.put(line.substring(0, eq), line.substring(eq + 1));
}
}
}
Loading

0 comments on commit d4476e5

Please sign in to comment.