package com.github.sommeri.less4j.utils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.io.IOUtils;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.github.sommeri.less4j.LessCompiler.CompilationResult;
import com.github.sommeri.sourcemap.FilePosition;
import com.github.sommeri.sourcemap.SourceMapConsumerV3;
import com.github.sommeri.sourcemap.SourceMapConsumerV3.EntryVisitor;
import com.github.sommeri.sourcemap.SourceMapParseException;
public class SourceMapValidator {
private static final String CALCULATED_SYMBOLS_PROPERTY = "calculatedNames";
private static final String SYMBOLS_PROPERTY = "names";
private static final String SOURCES_PROPERTY = "sources";
private static final String SOURCES_CEONTENT_PROPERTY = "sourcesContent";
private static final String CSS_FILE_PROPERTY = "file";
private static final String SOURCE_ROOT_PROPERTY = "sourceRoot";
protected static final String ALL = "*all*";
private Map<String, MappedFile> mappedFiles = new HashMap<String, MappedFile>();
private MappedFile cssFile;
private Map<String, String> contents = new HashMap<String, String>();
private String customRoot = null;
public SourceMapValidator() {
}
public SourceMapValidator(String customRoot) {
this.customRoot = customRoot;
}
public SourceMapValidator(Map<String, String> contents) {
this.contents = contents;
}
public void validateSourceMap(CompilationResult compilationResult, File mapdataFile) {
validateSourceMap(compilationResult, mapdataFile, null);
}
public void validateSourceMap(CompilationResult compilationResult, File mapdataFile, File cssFileLocation) {
initializeGeneratedCss(compilationResult);
SourceMapConsumerV3 sourceMap = parseGeneratedMap(compilationResult);
Mapdata mapdata = checkAgainstMapdataFile(sourceMap, mapdataFile);
loadMappedSourceFiles(sourceMap.getOriginalSources(), getSourceRoot(cssFileLocation), sourceMap.getOriginalSourcesContent());
validateSymbolMappings(sourceMap, mapdata);
}
private void validateSymbolMappings(SourceMapConsumerV3 sourceMap, Mapdata mapdata) {
MappingEntryValidation mappingEntryValidation = new MappingEntryValidation(mapdata);
sourceMap.visitMappings(mappingEntryValidation);
}
private String getSourceRoot(File cssFileLocation) {
if (customRoot!=null)
return customRoot;
if (cssFileLocation != null)
return URIUtils.addPLatformSlashIfNeeded(cssFileLocation.getParentFile().getAbsolutePath());
return "";
}
private SourceMapConsumerV3 parseGeneratedMap(CompilationResult compilationResult) {
try {
SourceMapConsumerV3 sourceMap = new SourceMapConsumerV3();
sourceMap.parse(compilationResult.getSourceMap());
return sourceMap;
} catch (SourceMapParseException e) {
throw new RuntimeException(e);
}
}
private Mapdata checkAgainstMapdataFile(SourceMapConsumerV3 sourceMap, File mapdataFile) {
Mapdata expectedMapdata = loadMapdata(mapdataFile);
// validate mapdata file
if (expectedMapdata.hasSources()) {
CustomAssertions.assertEqualsAsSets(expectedMapdata.getSources(), sourceMap.getOriginalSources());
}
if (expectedMapdata.hasSourcesContent()) {
CustomAssertions.assertEqualsAsSets(expectedMapdata.getSourcesContent(), sourceMap.getOriginalSourcesContent());
}
if (expectedMapdata.hasSymbols()) {
CustomAssertions.assertEqualsAsSets(expectedMapdata.getSymbols(), allSymbols(sourceMap));
}
if (expectedMapdata.hasFile()) {
assertEquals(expectedMapdata.getFile(), sourceMap.getFile());
}
if (expectedMapdata.hasSourceRoot()) {
assertEquals(expectedMapdata.getSourceRoot(), sourceMap.getSourceRoot());
}
return expectedMapdata;
}
private Collection<String> allSymbols(SourceMapConsumerV3 sourceMap) {
SymbolsCollector symbolsCollector = new SymbolsCollector();
sourceMap.visitMappings(symbolsCollector);
return symbolsCollector.getSymbols();
}
private Mapdata loadMapdata(File mapdataFile) {
// mapdata file not available - it is assumed to be empty
if (mapdataFile == null || !mapdataFile.exists())
return new Mapdata();
try {
JsonParser parser = new JsonParser();
JsonObject mapdata = parser.parse(new InputStreamReader(new FileInputStream(mapdataFile), "utf-8")).getAsJsonObject();
List<String> expectedSources = JSONUtils.getStringList(mapdata, SOURCES_PROPERTY);
List<String> expectedSourcesContent = JSONUtils.getStringList(mapdata, SOURCES_CEONTENT_PROPERTY);
List<String> expectedSymbols = JSONUtils.getStringList(mapdata, SYMBOLS_PROPERTY);
List<String> interpolatedSymbols = JSONUtils.getStringList(mapdata, CALCULATED_SYMBOLS_PROPERTY);
String file = JSONUtils.getString(mapdata, CSS_FILE_PROPERTY);
String sourceRoot = JSONUtils.getString(mapdata, SOURCE_ROOT_PROPERTY);
return new Mapdata(expectedSources, expectedSourcesContent, expectedSymbols, interpolatedSymbols, file, sourceRoot);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
private void loadMappedSourceFiles(Collection<String> originalSources, String root, Collection<String> originalSourcesContent) {
Iterator<String> iterator = originalSourcesContent.iterator();
for (String name : originalSources)
try {
String content = getFileContent(root, name, iterator.next());
MappedFile mapped = toMappedFile(name, content);
mappedFiles.put(name, mapped);
} catch (Throwable th) {
throw new RuntimeException("Could not read source file " + name, th);
}
}
private void initializeGeneratedCss(CompilationResult compilationResult) {
cssFile = toMappedFile("", compilationResult.getCss());
}
private MappedFile toMappedFile(String name, String content) {
String[] lines = content.split("\r?\n|\r");
MappedFile mapped = new MappedFile(lines, name);
return mapped;
}
private String getFileContent(String root, String name, String fallbackContent) throws UnsupportedEncodingException, IOException, FileNotFoundException {
if (contents.containsKey(name))
return contents.get(name);
String filename = URLDecoder.decode(name, "utf-8");
File sourcefile = toFile(filename);
File file = sourcefile.isAbsolute()? sourcefile : new File(root + sourcefile.getPath());
if (fallbackContent!=null && !file.exists()) {
return fallbackContent;
}
String content = IOUtils.toString(new InputStreamReader(new FileInputStream(file), "utf-8"));
return content;
}
private File toFile(String filename) {
//this is not a production quality, but works well enough for tests
try {
return new File((new URI(filename)).getPath());
} catch (URISyntaxException e) {
return new File(filename);
}
}
private final class SymbolsCollector implements EntryVisitor {
private final Set<String> symbols = new HashSet<String>();
private SymbolsCollector() {
}
@Override
public void visit(String sourceName, String sourceContent, String symbolName, FilePosition sourceStartPosition, FilePosition startPosition, FilePosition endPosition) {
if (symbolName != null)
symbols.add(symbolName);
}
public Set<String> getSymbols() {
return symbols;
}
}
class MappingEntryValidation implements EntryVisitor {
private Mapdata mapdata;
public MappingEntryValidation(Mapdata mapdata) {
this.mapdata = mapdata;
}
@Override
public void visit(String sourceName, String sourceContent, String symbolName, FilePosition sourceStartPosition, FilePosition startPosition, FilePosition endPosition) {
MappedFile mappedFile = mappedFiles.get(sourceName);
if (symbolName != null && !mapdata.isInterpolated(symbolName)) {
String sourceSnippet = mappedFile.getSnippet(sourceStartPosition, symbolName.length());
assertNotNull(sourceName + ": css symbol " + symbolName + " " + ts(startPosition) + " is mapped non-existent source position " + ts(sourceStartPosition), sourceSnippet);
assertEquals(sourceName + ": css symbol " + symbolName + " " + ts(startPosition) + " is mapped to less " + sourceSnippet + " " + ts(sourceStartPosition), symbolName, sourceSnippet);
}
if (symbolName != null && cssFile.isAvailable()) {
String cssSnippet = cssFile.getSnippet(startPosition, symbolName.length());
assertEquals(cssFile.getName() + ": position " + ts(startPosition) + " should contain " + symbolName + " it has " + cssSnippet + " instead", symbolName, cssSnippet);
}
}
private String ts(FilePosition p) {
StringBuilder builder = new StringBuilder();
builder.append("[").append(p.getLine() + 1).append(":").append(p.getColumn() + 1).append("]");
return builder.toString();
}
}
}
class MappedFile {
private final String name;
private final String[] lines;
public MappedFile() {
this(new String[0], null);
}
public MappedFile(String[] lines, String name) {
this.lines = lines;
this.name = name;
}
public boolean isAvailable() {
return name != null;
}
public String getSnippet(FilePosition start, int length) {
if (start.getLine() >= lines.length)
return null;
String line = lines[start.getLine()];
int startColumn = start.getColumn();
int end = startColumn + length;
if (end >= line.length())
end = line.length();
String substring = line.substring(startColumn, end);
return substring;
}
public String getName() {
return name;
}
}
class Mapdata {
private List<String> sources = null;
private List<String> sourcesContent = null;
private List<String> symbols = null;
private List<String> interpolatedSymbols = null;
private String file;
private String sourceRoot;
public Mapdata(List<String> sources, List<String> sourcesContent, List<String> symbols, List<String> interpolatedSymbols, String file, String sourceRoot) {
this.sources = sources;
this.sourcesContent = sourcesContent;
this.symbols = symbols;
this.interpolatedSymbols = interpolatedSymbols;
this.file = file;
this.sourceRoot = sourceRoot;
}
public Mapdata() {
}
public boolean hasSources() {
return sources != null;
}
public boolean hasSourcesContent() {
return sourcesContent != null;
}
public List<String> getSources() {
return sources;
}
public Collection<String> getSourcesContent() {
return sourcesContent;
}
public List<String> getSymbols() {
return symbols;
}
public boolean hasSymbols() {
return symbols != null;
}
public List<String> getInterpolatedSymbols() {
return interpolatedSymbols;
}
public boolean hasInterpolatedSymbols() {
return interpolatedSymbols != null;
}
public boolean isInterpolated(String symbolName) {
if (!hasInterpolatedSymbols())
return false;
return interpolatedSymbols.contains(SourceMapValidator.ALL) || interpolatedSymbols.contains(symbolName);
}
public String getFile() {
return file;
}
public boolean hasFile() {
return file != null;
}
public String getSourceRoot() {
return sourceRoot;
}
public boolean hasSourceRoot() {
return sourceRoot != null;
}
}