/*
* Copyright 2013, We The Internet Ltd.
*
* All rights reserved.
*
* Distributed under a modified BSD License as follow:
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* Redistribution in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution, unless otherwise
* agreed to in a written document signed by a director of We The Internet Ltd.
*
* Neither the name of We The Internet nor the names of its contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
package xapi.dev.template;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import xapi.collect.impl.SimpleFifo;
import xapi.dev.source.ImportSection;
import xapi.dev.source.SourceBuilder;
import xapi.log.api.LogLevel;
import xapi.log.api.LogService;
import xapi.log.impl.JreLog;
public class TemplateToJava {
private static final String templateSuffix = System.getProperty("template.suffix", ".x");
private static final Pattern lineMatcher = Pattern.compile("\\s*//.*//\\s*");
private static final Charset utf8 = Charset.forName("UTF-8");
public static void main(String[] templates) {
TemplateToJava generator = new TemplateToJava();
LogService logger = new JreLog();
TemplateGeneratorOptions options = new TemplateGeneratorOptions();
if (options.processArgs(templates)) {
logger.setLogLevel(options.getLogLevel());
for (String template : options.getTemplates()) {
generator.generate(logger, template, options);
}
} else {
throw new RuntimeException("Invalid arguments specified;" + " see console logs for help.");
}
}
private Class<?> generatorClass;
private final Map<Class<?>,Object> generators = new HashMap<Class<?>,Object>();
public void generate(LogService logger, String template, TemplateGeneratorOptions options) {
SourceBuilder<?> context = options.getContext(logger, template);
InputStream input = null;
try {
if (new File(template).exists()) {
input = new FileInputStream(template);
} else {
URL url = getClass().getClassLoader().getResource(template);
if (url == null) {
url = ClassLoader.getSystemResource(template);
if (url == null) {
logger.log(LogLevel.ERROR, "You requested code generation for template " + template +
", but the file could not be found.");
throw new CompilationFailed();
}
}
input = url.openStream();
}
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
String line;
while ((line = reader.readLine()) != null) {
if (lineMatcher.matcher(line).matches()) {
// we have a template command to parse!
applyTemplate(logger, reader, context, options, line.trim());
} else {
context.getBuffer().append(line).append('\n');
}
}
exportClass(logger, template, context, options);
} catch (Exception e) {
throw new CompilationFailed("Unable to generate java source file for template " + template, e);
} finally {
if (input != null) {
try {
input.close();
} catch (IOException e) {}
}
}
}
private static final int BUF_SIZE = 16 * 1024;
private void exportClass(LogService logger, String filename, SourceBuilder<?> context,
TemplateGeneratorOptions opts) {
File outputFile = new File(opts.getOutputLocation());
// normalize filename
if (filename.endsWith(templateSuffix))
filename = filename.substring(0, filename.length() - templateSuffix.length());
if (!filename.endsWith(".java")) filename = filename + ".java";
// repackage if requested; useful for generating non-transient super-source
String packageName = context.getPackage();
if (packageName != null) {
filename = packageName.replace('.', File.separatorChar) + File.separator +
filename.substring(filename.lastIndexOf('/'));
}
// save the source to file
outputFile = new File(outputFile.getAbsoluteFile(), filename);
outputFile.getParentFile().mkdirs();
try {
logger.log(LogLevel.INFO, "Writing generated output to " + outputFile.getAbsolutePath());
InputStream in = new ByteArrayInputStream(context.toString().getBytes(utf8));
OutputStream out = new FileOutputStream(outputFile);
byte[] buf = new byte[BUF_SIZE];
try {
int i;
while ((i = in.read(buf)) != -1) {
out.write(buf, 0, i);
}
} finally {
in.close();
out.close();
}
} catch (IOException e) {
logger.doLog(LogLevel.ERROR, new SimpleFifo<Object>()
.give("Error streaming generated output to file")
.give(outputFile.getAbsolutePath())
.give(e));
}
}
private void applyTemplate(LogService logger, BufferedReader input, SourceBuilder<?> context,
TemplateGeneratorOptions options, String line) throws IOException {
if (lineMatcher.matcher(line).matches())
line = line.substring(2, line.length() - 2);
else {
context.getBuffer().println(line);
return;
}
if (line.startsWith("@")) {
// we have a state-modifying command
String[] parts = stripBrackets(line.substring(1), input);
String command = parts[1];
switch (TemplateProcessingInstruction.valueOf(parts[0])) {
case classDefinition:
String next = input.readLine();
if (next.contains("class ") || next.contains("interface ")) {
// We have an @classDefinition above an actual well-formatted class {
// so we tell the class-def to not supply a }, as the file is
// assumed to be well-formatted as well.
context.setClassDefinition(next, true);
context.setClassDefinition(command, true);
} else {
context.setClassDefinition(command, false);
applyTemplate(logger, input, context, options, next);
return;
}
break;
case generateWith:
logger.log(LogLevel.TRACE, "Setting generator to " + command);
try {
generatorClass = Class.forName(command);
// Using X_Inject allows payloads to be specified as interfaces.
// This will allow generators and payloads to be reused and combined
// by creating a master payload which overrides all injected payload
// types needed.
// Mixing generators with different payloads cannot succeed without a
// master payload instance.
Object generator = inject(generatorClass);
if (generator instanceof TemplateClassGenerator)
((TemplateClassGenerator)generator).initialize(logger, options);
generators.put(generatorClass, generator);
} catch (Exception e) {
throw new CompilationFailed("Could not instantiate requested generator " + command + "; " +
"please ensure this class is available on your code generation classpath.", e);
}
break;
case emulated:
// when emulated, we need to pull in the next line, as we're rewriting a
// package statement.
String packageStatement = input.readLine().trim();
if (!packageStatement.startsWith("package ")) {
throw new CompilationFailed("An //@emulated()// command must appear on the line directly "
+ "above the package statement.");
}
String repackage = packageStatement.substring(8);
logger.log(LogLevel.TRACE, "Repackaging emulated source into " + command + "." + repackage);
context.getBuffer().append("package " + repackage);
context.setPackage(command + "." + repackage);
break;
case repackaged:
packageStatement = input.readLine().trim();
if (!packageStatement.startsWith("package ")) {
throw new CompilationFailed("An //@repackage()// command must appear on the line directly "
+ "above the package statement.");
}
repackage = command + ";";
logger.log(LogLevel.TRACE, "Repackaging source into " + repackage);
context.getBuffer().append("package " + repackage);
context.setPackage(repackage);
break;
case imports:
// declares an ImportSection that we can write to lazily
ImportSection imports = context.getImports();
String existing;
while ((existing = input.readLine()) != null) {
if (existing.trim().length() == 0) {
continue;
}
if (existing.startsWith("import ")) {
imports.addImport(existing);
} else {
applyTemplate(logger, input, context, options, existing);
return;
}
}
break;
case skipline:
int lines = Integer.parseInt(command);
while (lines-- > 0) {
input.readLine();
}
break;
}
} else {
String[] parts = stripBrackets(line, input);
if (generatorClass == null)
throw new CompilationFailed("TemplateToJava encountered a call to a template method, " + line +
", before a generator was called. " +
"\nPlease add //@generateWith(package.Class)// to the top of your template");
Object generator = generators.get(generatorClass);
if (generator == null)
throw new CompilationFailed("TemplateToJava encountered a call to a template method, " + line +
", before a generator was loaded. " +
"\nPlease add //@generateWith(package.Class)// to the top of your template");
Method method;
try {
method = generatorClass.getDeclaredMethod(parts[0], LogService.class, SourceBuilder.class,
String.class);
method.setAccessible(true);
} catch (Exception e) {
throw new CompilationFailed("Unable to find instance method " + parts[0] + " in generator class " +
generatorClass.getName(), e);
}
try {
method.invoke(generator, logger, context, parts[1]);
int skipLines = context.getLinesToSkip();
while (skipLines-- > 0)
input.readLine();
} catch (Exception e) {
throw new CompilationFailed("Failure invoking instance method " + parts[0] + " in generator class " +
generatorClass.getName(), e);
}
}
}
/**
* This method blindly instantiates the zero-arg constructor of the class. It
* it protected for subclasses to implement custom instantiation strategies
*
* @param cls - The class to inject
* @return cls.newInstance();
*/
protected Object inject(Class<?> cls) {
try {
return cls.newInstance();
} catch (Exception e) {
throw new CompilationFailed("Unable to instantiate class " + cls, e);
}
}
private String[] stripBrackets(String line, BufferedReader input) {
int start = line.indexOf('(');
int end = line.lastIndexOf(')');
if (start < 0 || end < 0)
throw new CompilationFailed("The command " + line + " was malformed. Expected () brackets in " + line);
String body = line.substring(0, start).trim();
String params = line.substring(start + 1, end).trim();
if (end < line.length() - 1) {
try {
String extra = line.substring(end + 1).trim();
if (extra.length() > 0) {
ProcessingInstructionOptions opts = new ProcessingInstructionOptions();
if (opts.processArgs(extra.split(" "))) {
if (opts.wordsToSkip > 0) {
// eat opening space
while ((input.read()) == ' ')
;
}
while (opts.wordsToSkip-- > 0) {
while ((input.read()) != ' ')
;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
return new String[] {body, params};
}
}