package com.carrotsearch.hppc.generator; import java.io.File; import java.io.IOException; import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.collections.ExtendedProperties; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; import org.apache.velocity.VelocityContext; import org.apache.velocity.runtime.RuntimeConstants; import org.apache.velocity.runtime.RuntimeInstance; import org.apache.velocity.runtime.log.NullLogChute; import com.carrotsearch.hppc.generator.intrinsics.Add; import com.carrotsearch.hppc.generator.intrinsics.Cast; import com.carrotsearch.hppc.generator.intrinsics.Empty; import com.carrotsearch.hppc.generator.intrinsics.Equals; import com.carrotsearch.hppc.generator.intrinsics.IsEmpty; import com.carrotsearch.hppc.generator.intrinsics.NewArray; import com.carrotsearch.hppc.generator.parser.SignatureProcessor; import com.google.common.base.Stopwatch; /** * Maven mojo applying preprocessor templates. */ @Mojo(name = "template-processor", defaultPhase = LifecyclePhase.GENERATE_SOURCES, threadSafe = true, requiresProject = true) public class TemplateProcessorMojo extends AbstractMojo { private final HashMap<String, IntrinsicMethod> intrinsics; { intrinsics = new HashMap<>(); intrinsics.put("empty", new Empty()); intrinsics.put("isEmpty", new IsEmpty()); intrinsics.put("newArray", new NewArray()); intrinsics.put("cast", new Cast()); intrinsics.put("add", new Add()); intrinsics.put("equals", new Equals()); } @Parameter(property = "project", readonly = true, required = true) private MavenProject project; @Parameter(defaultValue = "false") public boolean verbose; @Parameter(property = "template.processor.incremental", defaultValue = "true") public boolean incremental; @Parameter(required = true) public String attachSources; @Parameter(required = true) public File templatesDir; @Parameter(required = true) public File outputDir; private RuntimeInstance velocity; private Path templatesPath; private Path outputPath; @Override public void execute() throws MojoExecutionException { try { execute0(); } catch (IOException e) { throw new MojoExecutionException("Couldn't process templates.", e); } } private void execute0() throws IOException, MojoExecutionException { velocity = new RuntimeInstance(); final ExtendedProperties p = new ExtendedProperties(); p.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, NullLogChute.class.getName()); p.setProperty(RuntimeConstants.SET_NULL_ALLOWED, "false"); velocity.setConfiguration(p); // Cater for Eclipse's insanity -- we can't just specify a separate launch config that'd run // after a manual 'clean'. String eclipseLauncherBuildType = System.getenv("ECLIPSE_BUILD_TYPE"); if ("full".equals(eclipseLauncherBuildType)) { if (incremental) { getLog().info("Disabling incremental processing (Eclipse built type: " + eclipseLauncherBuildType + ")"); incremental = false; } } this.templatesPath = templatesDir.toPath().toAbsolutePath().normalize(); this.outputPath = outputDir.toPath().toAbsolutePath().normalize(); Path basedir = project.getBasedir().toPath().toAbsolutePath().normalize(); getLog().info(String.format(Locale.ROOT, "Processing templates from %s => %s", basedir.relativize(templatesPath), basedir.relativize(outputPath))); final Stopwatch sw = Stopwatch.createStarted(); final List<TemplateFile> templates = collectTemplateFiles(templatesPath); final List<OutputFile> generated = processTemplates(templates); final List<Path> removed = removeOtherFiles(outputPath, generated); int updated = generated.size(); for (OutputFile o : generated) { if (o.upToDate) updated--; } getLog().info(String.format(Locale.ROOT, "Processed %d templates in %.2f sec. (%d output files: %d updated, %d deleted).", templates.size(), sw.elapsed(TimeUnit.MILLISECONDS) / 1000.0f, generated.size(), updated, removed.size())); switch (attachSources.toLowerCase(Locale.ROOT)) { case "main": project.addCompileSourceRoot(outputPath.toString()); break; case "test": project.addTestCompileSourceRoot(outputPath.toString()); break; default: throw new MojoExecutionException("Invalid source attachment option ('source' or 'test' allowed): " + attachSources); } } private List<Path> removeOtherFiles(Path outputPath, List<OutputFile> keep) throws IOException { final Set<String> keepPaths = new HashSet<>(); for (OutputFile o : keep) { keepPaths.add(o.path.toRealPath().toString()); } final List<Path> toRemove = new ArrayList<>(); Files.walkFileTree(outputPath, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { path = path.toRealPath(); if (!keepPaths.contains(path.toString())) { toRemove.add(path); } return FileVisitResult.CONTINUE; } }); for (Path p : toRemove) { verboseLog("Deleting: " + p.toString()); Files.delete(p); } return toRemove; } private void verboseLog(String message) { if (verbose) { getLog().info(message); } else { getLog().debug(message); } } /** * Apply templates to <code>.ktype</code> files (single-argument). */ private List<OutputFile> processTemplates(List<TemplateFile> inputs) throws IOException { List<OutputFile> outputs = new ArrayList<>(); for (TemplateFile f : inputs) { String fileName = f.getFileName(); if (!fileName.contains("VType") && fileName.contains("KType")) { for (Type ktype : Type.values()) { TemplateOptions options = new TemplateOptions(ktype); options.templateFile = f.path; generate(f, outputs, options); } } if (fileName.contains("KTypeVType")) { for (Type ktype : Type.values()) { for (Type vtype : Type.values()) { TemplateOptions options = new TemplateOptions(ktype, vtype); options.templateFile = f.path; generate(f, outputs, options); } } } } return outputs; } private Stopwatch timeVelocity = Stopwatch.createUnstarted(); private Stopwatch timeIntrinsics = Stopwatch.createUnstarted(); private Stopwatch timeTypeClassRefs = Stopwatch.createUnstarted(); private Stopwatch timeComments = Stopwatch.createUnstarted(); /** * Apply templates. */ private void generate(TemplateFile input, List<OutputFile> outputs, TemplateOptions templateOptions) throws IOException { final String targetFileName = targetFileName(templatesPath.relativize(input.path).toString(), templateOptions); final OutputFile output = new OutputFile(outputPath.resolve(targetFileName).toAbsolutePath().normalize()); if (incremental && Files.exists(output.path) && Files.getLastModifiedTime(output.path).toMillis() >= Files.getLastModifiedTime(input.path).toMillis()) { // No need to re-render but mark as generated. output.upToDate = true; outputs.add(output); return; } String template = new String(Files.readAllBytes(input.path), StandardCharsets.UTF_8); timeVelocity.start(); template = filterVelocity(input, template, templateOptions); timeVelocity.stop(); // Check if template requested ignoring a given type combination. if (templateOptions.isIgnored()) { return; } getLog().debug("Processing: " + input.getFileName() + " => " + output.path); try { timeIntrinsics.start(); template = filterIntrinsics(template, templateOptions); timeIntrinsics.stop(); timeComments.start(); template = filterComments(template); timeComments.stop(); timeTypeClassRefs.start(); template = filterTypeClassRefs(template, templateOptions); template = filterStaticTokens(template, templateOptions); timeTypeClassRefs.stop(); } catch (RuntimeException e) { getLog().error("Error processing: " + input.getFileName() + " => " + output.path + ":\n\t" + e.getMessage()); throw e; } Files.createDirectories(output.path.getParent()); Files.write(output.path, template.getBytes(StandardCharsets.UTF_8)); outputs.add(output); } private String filterStaticTokens(String template, TemplateOptions templateOptions) { return template.replace(TemplateOptions.TEMPLATE_FILE_TOKEN, templateOptions.getTemplateFile()); } private String filterIntrinsics(String input, TemplateOptions templateOptions) { // TODO: this should be eventually moved to AST processor. Pattern p = Pattern.compile( "(Intrinsics.\\s*)" + "(<(?<generic>[^>]+)>\\s*)?" + "(?<method>[a-zA-Z0-9]+)", Pattern.MULTILINE | Pattern.DOTALL); StringBuilder sb = new StringBuilder(); while (true) { Matcher m = p.matcher(input); if (m.find()) { sb.append(input.substring(0, m.start())); String method = m.group("method"); int bracketCount = 0; int last = m.end() + 1; ArrayList<String> params = new ArrayList<>(); outer: for (int i = m.end(); i < input.length(); i++) { switch (input.charAt(i)) { case '(': bracketCount++; break; case ')': bracketCount--; if (bracketCount == 0) { if (last != i) { params.add(input.substring(last, i).trim()); } input = input.substring(i + 1); break outer; } break; case ',': if (bracketCount == 1) { params.add(input.substring(last, i)); last = i + 1; } break; } } getLog().debug("Intrinsic call: " + m.group() + "; method: " + method + ", generic: " + m.group("generic") + ", args: " + params); // Allow recursion of intrinsics into arguments. for (int i = 0; i < params.size(); i++) { params.set(i, filterIntrinsics(params.get(i), templateOptions)); } IntrinsicMethod im = intrinsics.get(method); if (im == null) { throw new RuntimeException(String.format(Locale.ROOT, "Unknown intrinsic method '%s' in call: %s", method, m.group())); } else { im.invoke(m, sb, templateOptions, m.group("generic"), params); } } else { sb.append(input); break; } } return sb.toString(); } private String filterComments(String input) { Pattern p = Pattern.compile("(/\\*!)|(!\\*/)", Pattern.MULTILINE | Pattern.DOTALL); return p.matcher(input).replaceAll(""); } private String filterTypeClassRefs(String input, TemplateOptions options) { try { SignatureProcessor signatureProcessor = new SignatureProcessor(input); return signatureProcessor.process(options); } catch (Exception e) { getLog().error("Signature processor failure: " + options.getTemplateFile(), e); throw new RuntimeException(e); } } /** * Apply velocity to the input. */ private String filterVelocity(TemplateFile f, String template, TemplateOptions options) { final VelocityContext ctx = new VelocityContext(); ctx.put("TemplateOptions", options); ctx.put("true", true); ctx.put("templateOnly", false); ctx.put("false", false); StringWriter sw = new StringWriter(); velocity.evaluate(ctx, sw, f.getFileName(), template); return sw.toString(); } private String targetFileName(String relativePath, TemplateOptions templateOptions) { if (templateOptions.hasVType()) { relativePath = relativePath.replace("KTypeVType", templateOptions.getKType().getBoxedType() + templateOptions.getVType().getBoxedType()); } return relativePath.replace("KType", templateOptions.getKType().getBoxedType()); } private List<Path> scanFilesMatching(Path dir, String matchPattern) throws IOException { final List<Path> paths = new ArrayList<>(); if (Files.isDirectory(dir)) { final PathMatcher matcher = dir.getFileSystem().getPathMatcher(matchPattern); Files.walkFileTree(dir, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { if (matcher.matches(path)) { paths.add(path); } return FileVisitResult.CONTINUE; } }); } return paths; } /** * Collect all template files from this and subdirectories. */ private List<TemplateFile> collectTemplateFiles(Path dir) throws IOException { final List<TemplateFile> paths = new ArrayList<>(); for (Path path : scanFilesMatching(dir, "glob:**.java")) { paths.add(new TemplateFile(path)); } return paths; } }