package com.github.sommeri.less4j.core.compiler.stages;
import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.github.sommeri.less4j.LessCompiler.Cache;
import com.github.sommeri.less4j.LessCompiler.Configuration;
import com.github.sommeri.less4j.LessSource;
import com.github.sommeri.less4j.LessSource.CannotReadFile;
import com.github.sommeri.less4j.LessSource.FileNotFound;
import com.github.sommeri.less4j.LessSource.StringSourceException;
import com.github.sommeri.less4j.core.ast.ASTCssNode;
import com.github.sommeri.less4j.core.ast.FaultyNode;
import com.github.sommeri.less4j.core.ast.GeneralBody;
import com.github.sommeri.less4j.core.ast.Import;
import com.github.sommeri.less4j.core.ast.Import.ImportContent;
import com.github.sommeri.less4j.core.ast.InlineContent;
import com.github.sommeri.less4j.core.ast.Media;
import com.github.sommeri.less4j.core.ast.StyleSheet;
import com.github.sommeri.less4j.core.compiler.expressions.TypesConversionUtils;
import com.github.sommeri.less4j.core.parser.ANTLRParser;
import com.github.sommeri.less4j.core.parser.ASTBuilder;
import com.github.sommeri.less4j.core.parser.HiddenTokenAwareTree;
import com.github.sommeri.less4j.core.problems.ProblemsHandler;
public class SingleImportSolver {
private final ProblemsHandler problemsHandler;
private final Configuration configuration;
private TypesConversionUtils conversionUtils = new TypesConversionUtils();
private ASTManipulator astManipulator = new ASTManipulator();
private Cache astCache;
public SingleImportSolver(ProblemsHandler problemsHandler, Configuration configuration) {
this.problemsHandler = problemsHandler;
this.configuration = configuration;
this.astCache = configuration.getCache();
if (astCache == null) {
final HashMap<Object, Object> map = new HashMap<Object, Object>();
astCache = new Cache() {
@Override
public Object getAst(LessSource key) {
return map.get(key);
}
@Override
public void setAst(LessSource key, Object value) {
map.put(key, value);
}
};
}
}
public ASTCssNode importEncountered(Import importNode, LessSource source, AlreadyImportedSources alreadyImportedSources) {
String filename = conversionUtils.extractFilename(importNode.getUrlExpression(), problemsHandler, configuration);
if (filename == null) {
problemsHandler.errorWrongImport(importNode.getUrlExpression());
return null;
}
String urlParams = "";
int paramsIndx = filename.lastIndexOf("?");
if (paramsIndx != -1) {
urlParams = filename.substring(paramsIndx);
filename = filename.substring(0, paramsIndx);
}
// css file imports should be left as they are
// FIXME ! they should be relativized
if (!importNode.isInline() && treatAsCss(importNode, filename))
return null;
filename = addLessSuffixIfNeeded(filename, urlParams);
LessSource importedSource;
try {
importedSource = source.relativeSource(filename);
} catch (FileNotFound ex) {
return importFileNotFound(importNode, filename);
} catch (CannotReadFile e) {
problemsHandler.errorFileCanNotBeRead(importNode, filename);
return null;
} catch (StringSourceException ex) {
// imports are relative to current file and we do not know its location
problemsHandler.warnLessImportNoBaseDirectory(importNode.getUrlExpression());
return null;
}
// import once should not import a file that was already imported
if (importNode.isImportOnce() && alreadyImportedSources.alreadyVisited(importedSource)) {
astManipulator.removeFromBody(importNode);
return null;
}
StyleSheet importedAst;
try {
if (importNode.isInline()) {
ASTCssNode importedNode = replaceByInlineValue(importNode, importedSource.getContent());
alreadyImportedSources.add(importedSource);
configureVisibilityBlocks(importNode, Arrays.asList(importedNode));
return importedNode;
}
importedAst = buildImportedAst(importNode, importedSource);
alreadyImportedSources.add(importedSource);
} catch (FileNotFound e) {
return importFileNotFound(importNode, filename);
} catch (CannotReadFile e) {
problemsHandler.errorFileCanNotBeRead(importNode, filename);
return null;
}
List<ASTCssNode> importedNodes = importedAst.getChilds();
configureVisibilityBlocks(importNode, importedAst.getChilds());
astManipulator.replaceInBody(importNode, importedNodes);
return importedAst;
}
private void configureVisibilityBlocks(Import importNode, List<ASTCssNode> nodes) {
if (importNode.isReferenceOnly() || importNode.hasVisibilityBlock()) {
int childVisibilityBlocks = importNode.getVisibilityBlocks() + (importNode.isReferenceOnly()? 1 : 0);
for (ASTCssNode child : nodes) {
child.setVisibilityBlocks(childVisibilityBlocks);
}
}
}
private ASTCssNode replaceByInlineValue(Import node, String importedContent) {
HiddenTokenAwareTree underlyingStructure = node.getUnderlyingStructure();
StyleSheet result = new StyleSheet(underlyingStructure);
InlineContent content = new InlineContent(underlyingStructure, importedContent);
result.addMember(content);
result.configureParentToAllChilds();
astManipulator.replaceInBody(node, content);
return result;
}
private ASTCssNode importFileNotFound(Import node, String filename) {
if (!node.isOptional()) {
problemsHandler.errorFileNotFound(node, filename);
return null;
}
return replaceByInlineValue(node, "");
}
private StyleSheet buildImportedAst(Import node, LessSource source) throws FileNotFound, CannotReadFile {
StyleSheet importedAst = getImportedAst(node, source);
// add media queries if needed
if (node.hasMediums()) {
HiddenTokenAwareTree underlyingStructure = node.getUnderlyingStructure();
StyleSheet result = new StyleSheet(underlyingStructure);
Media media = new Media(underlyingStructure);
result.addMember(media);
media.setParent(result);
media.setMediums(node.getMediums());
GeneralBody mediaBody = new GeneralBody(underlyingStructure, importedAst.getMembers());
media.setBody(mediaBody);
media.configureParentToAllChilds();
mediaBody.configureParentToAllChilds();
return result;
}
return importedAst;
}
private StyleSheet getImportedAst(Import node, LessSource source) throws FileNotFound, CannotReadFile {
StyleSheet importedAst = (StyleSheet) astCache.getAst(source);
if (importedAst == null) {
importedAst = parseContent(node, source.getContent(), source);
astCache.setAst(source, importedAst);
}
return importedAst.clone();
}
private StyleSheet parseContent(Import importNode, String importedContent, LessSource source) {
ANTLRParser parser = new ANTLRParser();
ANTLRParser.ParseResult parsedSheet = parser.parseStyleSheet(importedContent, source);
if (parsedSheet.hasErrors()) {
StyleSheet result = new StyleSheet(importNode.getUnderlyingStructure());
result.addMember(new FaultyNode(importNode));
problemsHandler.addErrors(parsedSheet.getErrors());
return result;
}
ASTBuilder astBuilder = new ASTBuilder(problemsHandler);
StyleSheet lessStyleSheet = astBuilder.parseStyleSheet(parsedSheet.getTree());
return lessStyleSheet;
}
private String addLessSuffixIfNeeded(String filename, String urlParams) {
if ((new File(filename)).getName().contains("."))
return filename;
return filename + ".less" + urlParams;
}
private boolean treatAsCss(Import node, String filename) {
ImportContent contentKind = node.getContentKind();
return contentKind == ImportContent.CSS || (contentKind == ImportContent.SUFFIX_BASED && isCssFile(filename));
}
private boolean isCssFile(String filename) {
String lowerCase = filename.toLowerCase();
return lowerCase.endsWith(".css") || lowerCase.endsWith("/css");
}
public static class AlreadyImportedSources {
private Set<LessSource> sourcesThatCount = new HashSet<LessSource>();
private Set<LessSource> allImportedSources;
public AlreadyImportedSources(Set<LessSource> allImportedSources) {
this.allImportedSources = allImportedSources;
}
public void add(LessSource importedSource) {
sourcesThatCount.add(importedSource);
allImportedSources.add(importedSource);
}
public boolean alreadyVisited(LessSource importedSource) {
boolean result = sourcesThatCount.contains(importedSource);
return result;
}
}
}