ProjectAres/PGM/src/main/java/tc/oc/pgm/map/MapFilePreprocessor.java

203 lines
7.5 KiB
Java

package tc.oc.pgm.map;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.inject.assistedinject.Assisted;
import org.jdom2.Content;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.JDOMParseException;
import org.jdom2.input.SAXBuilder;
import tc.oc.commons.core.logging.Loggers;
import tc.oc.commons.core.util.HashingInputStream;
import tc.oc.pgm.utils.XMLUtils;
import tc.oc.pgm.xml.InvalidXMLException;
import tc.oc.pgm.xml.Node;
import static com.google.common.base.Preconditions.checkNotNull;
public class MapFilePreprocessor {
public interface Factory {
MapFilePreprocessor create(MapSource source);
}
private final SAXBuilder builder;
private final Logger logger;
private final MapConfiguration mapConfiguration;
private final MapSource source;
private final Stack<Path> includeStack = new Stack<>();
private final Map<Path, HashCode> includedFiles = new HashMap<>();
@Inject MapFilePreprocessor(@Assisted MapSource source, Loggers loggers, SAXBuilder builder, MapConfiguration mapConfiguration) {
this.source = source;
this.logger = loggers.get(getClass(), source.getPath().toString());
this.builder = builder;
this.mapConfiguration = mapConfiguration;
}
public Map<Path, HashCode> getIncludedFiles() {
return includedFiles;
}
public Document readRootDocument(Path file) throws InvalidXMLException {
checkNotNull(file, "file");
this.includeStack.clear();
Document result = this.readDocument(file);
this.includeStack.clear();
if(source.globalIncludes()) {
for(Path globalInclude : mapConfiguration.globalIncludes()) {
final Path includePath = findIncludeFile(null, globalInclude, null);
if(includePath != null) {
result.getRootElement().addContent(0, readIncludedDocument(includePath, null));
}
}
}
return result;
}
private Document readDocument(Path absolutePath) throws InvalidXMLException {
final Path relativePath = source.getPath().relativize(absolutePath);
Document doc;
try(HashingInputStream istream = new HashingInputStream(Hashing.sha256(), new FileInputStream(absolutePath.toFile()))) {
doc = this.builder.build(istream);
doc.setBaseURI(relativePath.toString());
this.includedFiles.put(absolutePath, istream.hash());
} catch(FileNotFoundException e) {
throw new InvalidXMLException("File not found", absolutePath.toString());
} catch(IOException e) {
throw new InvalidXMLException("Error reading file: " + e.getMessage(), absolutePath.toString());
} catch(JDOMParseException e) {
throw InvalidXMLException.fromJDOM(e, absolutePath.toString());
} catch(JDOMException e) {
throw new InvalidXMLException("Unhandled " + e.getClass().getSimpleName(), absolutePath.toString(), e);
}
this.processChildren(absolutePath, doc.getRootElement());
return doc;
}
private @Nullable Path findIncludeFile(@Nullable Path basePath, Path includeFile, @Nullable Element includeElement) throws InvalidXMLException {
Iterable<Path> includePaths = mapConfiguration.includePaths();
if(basePath != null) {
includePaths = Iterables.concat(includePaths, Collections.singleton(basePath));
}
for(Path includePath : includePaths) {
Path fullPath = includePath.resolve(includeFile);
if(Files.isRegularFile(fullPath)) {
return fullPath.toAbsolutePath();
}
}
return null;
}
private List<Content> readIncludedDocument(@Nullable Path basePath, Path includeFile, @Nullable Element includeElement) throws InvalidXMLException {
final Path fullPath = findIncludeFile(basePath, includeFile, includeElement);
if(fullPath == null) {
throw new InvalidXMLException("Failed to find include: " + includeFile, includeElement);
}
return readIncludedDocument(fullPath, includeElement);
}
private List<Content> readIncludedDocument(Path fullPath, @Nullable Element includeElement) throws InvalidXMLException {
if(includeStack.contains(fullPath)) {
throw new InvalidXMLException("Circular include: " + Joiner.on(" --> ").join(includeStack), includeElement);
}
includeStack.push(fullPath);
try {
return readDocument(fullPath).getRootElement().cloneContent();
} finally {
includeStack.pop();
}
}
private List<Content> processIncludeElement(Path baseFile, Element el) throws InvalidXMLException {
Path path = XMLUtils.parseRelativePath(Node.fromRequiredAttr(el, "src"));
return readIncludedDocument(baseFile.getParent(), path, el);
}
private <T> T getEnvironment(String key, Class<T> type, Node node) throws InvalidXMLException {
Object value = mapConfiguration.environment().get(key);
if(value == null) {
logger.warning("Unknown environment variable '" + key + "', using default value: false");
value = false;
}
if(!type.isInstance(value)) {
throw new InvalidXMLException("Wrong variable type, expected " + type.getSimpleName() + ", was " + value.getClass().getSimpleName(), node);
}
return type.cast(value);
}
private List<Content> processConditional(Element el, boolean invert) throws InvalidXMLException {
for(Node attr : Node.fromAttrs(el)) {
boolean expected = XMLUtils.parseBoolean(attr);
boolean actual = getEnvironment(attr.getName(), Boolean.class, attr);
if(expected != actual) {
return invert ? el.cloneContent() : Collections.<Content>emptyList();
}
}
return invert ? Collections.<Content>emptyList() : el.cloneContent();
}
private void processChildren(Path file, Element parent) throws InvalidXMLException {
for(int i = 0; i < parent.getContentSize(); i++) {
Content content = parent.getContent(i);
if(!(content instanceof Element)) continue;
Element child = (Element) content;
List<Content> replacement = null;
switch(child.getName()) {
case "include":
replacement = processIncludeElement(file, child);
break;
case "if":
replacement = processConditional(child, false);
break;
case "unless":
replacement = processConditional(child, true);
break;
}
if(replacement != null) {
parent.removeContent(i);
parent.addContent(i, replacement);
i--; // Process replacement content
} else {
processChildren(file, child);
}
}
}
}