/*
* This file is part of the Illarion project.
*
* Copyright © 2015 - Illarion e.V.
*
* Illarion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Illarion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
package illarion.compile;
import illarion.compile.impl.Compile;
import org.apache.commons.cli.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
import javax.annotation.Nonnull;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
/**
* This the the main class for the compiler. It determines the kind of compiler required for the set file and performs
* the compiling operation.
*
* @author Martin Karing <nitram@illarion.org>
*/
public final class Compiler {
@Nonnull
private static final Logger LOGGER = LoggerFactory.getLogger(Compiler.class);
private static Map<CompilerType, Path> storagePaths;
private Compiler() {
}
public static void main(String... args) throws UnsupportedEncodingException {
ByteArrayOutputStream stdOutBuffer = new ByteArrayOutputStream();
PrintStream orgStdOut = System.out;
System.setOut(new PrintStream(stdOutBuffer, true, Charset.defaultCharset().toString()));
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
Options options = new Options();
Option npcDir = new Option("n", "npc-dir", true, "The place where the compiled NPC files are stored.");
npcDir.setArgs(1);
npcDir.setArgName("directory");
npcDir.setRequired(false);
options.addOption(npcDir);
Option questDir = new Option("q", "quest-dir", true,
"The place where the compiled Quest files are stored.");
questDir.setArgs(1);
questDir.setArgName("directory");
questDir.setRequired(false);
options.addOption(questDir);
Option type = new Option("t", "type", true,
"This option is used to set what kind of parser is supposed to be used in case" +
" the content of standard input is processed."
);
type.setArgs(1);
type.setArgName("type");
type.setRequired(false);
options.addOption(type);
Option jobs = new Option("j", "jobs", true,
"This option defines how many jobs may run in parallel. Default is 1. 'Auto' is a " +
"possible value to leave it to the VM how many execution threads are used.");
jobs.setArgs(1);
jobs.setArgName("jobs");
jobs.setRequired(false);
options.addOption(jobs);
CommandLineParser parser = new DefaultParser();
try {
CommandLine cmd = parser.parse(options, args);
String[] files = cmd.getArgs();
if (files.length > 0) {
System.setOut(orgStdOut);
stdOutBuffer.writeTo(orgStdOut);
processFileMode(cmd);
} else {
System.setOut(orgStdOut);
processStdIn(cmd);
}
} catch (ParseException e) {
HelpFormatter helpFormatter = new HelpFormatter();
helpFormatter.printHelp("java -jar compiler.jar [Options] File", options, true);
System.exit(-1);
} catch (IOException e) {
LOGGER.error(e.getLocalizedMessage());
System.exit(-1);
}
}
private static void processFileMode(@Nonnull CommandLine cmd) throws IOException {
storagePaths = new EnumMap<>(CompilerType.class);
String npcPath = cmd.getOptionValue('n');
if (npcPath != null) {
storagePaths.put(CompilerType.easyNPC, Paths.get(npcPath));
}
String questPath = cmd.getOptionValue('q');
if (questPath != null) {
storagePaths.put(CompilerType.easyQuest, Paths.get(questPath));
}
String jobsOption = cmd.getOptionValue('j');
ExecutorService executor;
if (jobsOption == null) {
executor = Executors.newSingleThreadExecutor();
} else if ("auto".equalsIgnoreCase(jobsOption)) {
executor = Executors.newCachedThreadPool();
} else {
try {
int numberOfJobs = Integer.parseInt(jobsOption);
executor = Executors.newFixedThreadPool(numberOfJobs);
} catch (NumberFormatException e) {
LOGGER.error("Invalid value for jobs option: {}", jobsOption);
executor = Executors.newSingleThreadExecutor();
}
}
List<Future<Integer>> results = new ArrayList<>();
for (String file : cmd.getArgs()) {
Path path = Paths.get(file);
if (Files.isDirectory(path)) {
ExecutorService finalExecutor = executor;
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
FileVisitResult result = super.visitFile(file, attrs);
if (result == FileVisitResult.CONTINUE) {
results.add(processPath(finalExecutor, file));
return FileVisitResult.CONTINUE;
}
return result;
}
});
} else {
results.add(processPath(executor, path));
}
}
executor.shutdown();
try {
executor.awaitTermination(1L, TimeUnit.HOURS);
} catch (InterruptedException e) {
LOGGER.error("Interruption received.", e);
}
int returnCode = 3;
for (Future<Integer> result : results) {
try {
returnCode = Math.min(returnCode, result.get());
} catch (InterruptedException e) {
LOGGER.error("Interruption received.", e);
} catch (ExecutionException e) {
LOGGER.error("Error while performing parsing.", e);
}
}
System.exit(returnCode);
}
private static void processStdIn(@Nonnull CommandLine cmd) throws IOException {
String dataType = cmd.getOptionValue('t');
CompilerType usedType = null;
if (dataType != null) {
switch (dataType) {
case "npc":
usedType = CompilerType.easyNPC;
break;
case "quest":
usedType = CompilerType.easyQuest;
break;
}
}
if (usedType == null) {
LOGGER.error("Standard input mode requires a valid definition of the type option.");
System.exit(-1);
}
Compile compile = usedType.getImplementation();
System.exit(compile.compileStream(System.in, System.out));
}
private static Future<Integer> processPath(
@Nonnull ExecutorService executor, @Nonnull Path path) throws IOException {
if (Files.isDirectory(path)) {
return new CompletedFuture<>(0);
}
for (CompilerType type : CompilerType.values()) {
if (type.isValidFile(path)) {
Compile compile = type.getImplementation();
if (path.isAbsolute()) {
if (storagePaths.containsKey(type)) {
compile.setTargetDir(storagePaths.get(type));
} else {
compile.setTargetDir(path.getParent());
}
} else {
if (storagePaths.containsKey(type)) {
Path parent = path.getParent();
if (parent == null) {
compile.setTargetDir(storagePaths.get(type));
} else {
compile.setTargetDir(storagePaths.get(type).resolve(parent));
}
} else {
Path parent = path.getParent();
if (parent == null) {
compile.setTargetDir(path.toAbsolutePath().getParent());
} else {
compile.setTargetDir(parent);
}
}
}
return executor.submit(() -> {
int result = compile.compileFile(path.toAbsolutePath());
if (result == 1) {
LOGGER.info("Skipped file: {}", path.getFileName());
return -2;
}
return result;
});
}
}
return new CompletedFuture<>(-2);
}
private static final class CompletedFuture<T> implements Future<T> {
private final T result;
private CompletedFuture(T result) {
this.result = result;
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
@Override
public boolean isCancelled() {
return false;
}
@Override
public boolean isDone() {
return false;
}
@Override
public T get() throws InterruptedException, ExecutionException {
return result;
}
@Override
public T get(long timeout, @Nonnull TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
return result;
}
}
}