package net.jangaroo.jooc.mvnplugin; import net.jangaroo.utils.BOMStripperInputStream; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.repository.ArtifactRepository; import org.apache.maven.model.Dependency; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProjectBuilder; import org.apache.maven.project.ProjectBuildingException; import org.codehaus.plexus.archiver.zip.ZipEntry; import org.codehaus.plexus.archiver.zip.ZipFile; import org.codehaus.plexus.util.IOUtil; import java.io.BufferedReader; 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.OutputStreamWriter; import java.io.Writer; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * An abstract goal to build a Jangaroo application, either for testing or for an actual application. * It aggregates all needed resources into a given Web app directory: * <ul> * <li>extract all dependent jangaroo artifacts</li> * <li>optionally add Jangaroo compiler output from the current module</li> * <li>concatenate <artifactId>.js from all dependent jangaroo artifacts into jangaroo-application.js in the correct order</li> * </ul> * * @requiresDependencyResolution runtime */ public abstract class PackageApplicationMojo extends AbstractMojo { /** * The maven project. * * @parameter expression="${project}" * @required * @readonly */ protected MavenProject project; /** * @component */ @SuppressWarnings("UnusedDeclaration") private MavenProjectBuilder mavenProjectBuilder; /** * @parameter expression="${localRepository}" * @required */ @SuppressWarnings("UnusedDeclaration") private ArtifactRepository localRepository; /** * @parameter expression="${project.remoteArtifactRepositories}" * @required */ @SuppressWarnings("UnusedDeclaration") private List remoteRepositories; public abstract File getPackageSourceDirectory(); /** * Create the Jangaroo Web app in the given Web app directory. * @param webappDirectory the directory where to build the Jangaroo Web app. * @throws org.apache.maven.plugin.MojoExecutionException if anything goes wrong */ protected void createWebapp(File webappDirectory) throws MojoExecutionException { if (webappDirectory.mkdirs()) { getLog().debug("created webapp directory " + webappDirectory); } try { concatModuleScripts(new File(webappDirectory, "joo")); } catch (IOException e) { throw new MojoExecutionException("Failed to create jangaroo-application[-all].js", e); } catch (ProjectBuildingException e) { throw new MojoExecutionException("Failed to create jangaroo-application[-all].js", e); } } /** * Linearizes the acyclic, directed graph of all dependent artifacts to a list * where every item just depends on items that are contained in the list before itself. * * @return linearized dependency list of artifacts */ private List<Artifact> getLinearizedDependencies() throws ProjectBuildingException { final Map<String, Artifact> internalId2Artifact = artifactByInternalId(); List<String> depthFirstArtifactIds = new ArrayList<String>(); Set<String> openArtifacts = new HashSet<String>(internalId2Artifact.keySet()); while (!openArtifacts.isEmpty()) { depthFirst(internalId2Artifact, depthFirstArtifactIds, openArtifacts, openArtifacts.iterator().next()); } getLog().debug("linearized dependencies: " + depthFirstArtifactIds); // back from internal IDs to Artifacts: List<Artifact> depthFirstArtifacts = new ArrayList<Artifact>(depthFirstArtifactIds.size()); for (String depthFirstArtifactId : depthFirstArtifactIds) { depthFirstArtifacts.add(internalId2Artifact.get(depthFirstArtifactId)); } return depthFirstArtifacts; } private void depthFirst(Map<String, Artifact> internalId2Artifact, List<String> depthFirstArtifactIds, Set<String> openArtifacts, String artifactId) throws ProjectBuildingException { if (openArtifacts.remove(artifactId)) { // first, my dependencies: List<String> dependencies = getDependencies(internalId2Artifact.get(artifactId)); for (String dependency : dependencies) { depthFirst(internalId2Artifact, depthFirstArtifactIds, openArtifacts, dependency); } // then, my artifact depthFirstArtifactIds.add(artifactId); } } private static String getInternalId(Dependency dep) { return dep.getGroupId() + ":" + dep.getArtifactId(); } private static String getInternalId(Artifact art) { return art.getGroupId() + ":" + art.getArtifactId(); } private static final String JOO_FLUSH_STYLE_SHEETS = "\njoo.flushStyleSheets();\n"; private void concatModuleScripts(File scriptDirectory) throws IOException, ProjectBuildingException { Writer jangarooApplicationWriter = createJangarooModulesFile(scriptDirectory, "jangaroo-application.js"); Writer jangarooApplicationAllWriter = createJangarooModulesFile(scriptDirectory, "jangaroo-application-all.js"); try { jangarooApplicationWriter.write("// This file loads all collected JavaScript code from dependent Jangaroo modules.\n\n"); jangarooApplicationAllWriter.write("// This file contains all collected JavaScript code from dependent Jangaroo modules.\n\n"); for (Artifact artifact : getLinearizedDependencies()) { includeJangarooModuleScript(scriptDirectory, artifact, jangarooApplicationWriter, jangarooApplicationAllWriter); } writeThisJangarooModuleScript(scriptDirectory, jangarooApplicationWriter, jangarooApplicationAllWriter); jangarooApplicationWriter.write(JOO_FLUSH_STYLE_SHEETS); jangarooApplicationAllWriter.write(JOO_FLUSH_STYLE_SHEETS); } finally { try { jangarooApplicationWriter.close(); jangarooApplicationAllWriter.close(); } catch (IOException e) { getLog().warn("IOException on close ignored.", e); } } } protected void writeThisJangarooModuleScript(File scriptDirectory, Writer jangarooApplicationWriter, Writer jangarooApplicationAllWriter) throws IOException { ModuleSource jooModuleSource = null; File packageSourceDirectory = getPackageSourceDirectory(); if (packageSourceDirectory != null) { File jangarooModuleFile = new File(packageSourceDirectory, computeModuleJsFileName(project.getArtifactId())); if (jangarooModuleFile.exists()) { jooModuleSource = new FileModuleSource(jangarooModuleFile); } } writeJangarooModuleScript(scriptDirectory, project.getArtifact(), jooModuleSource, jangarooApplicationWriter, jangarooApplicationAllWriter); } private static String computeModuleJsFileName(String artifactId) { return "META-INF/resources/joo/" + artifactId + ".module.js"; } private Writer createJangarooModulesFile(File scriptDirectory, String fileName) throws IOException { //noinspection ResultOfMethodCallIgnored if (scriptDirectory.mkdirs()) { getLog().debug("created script output directory " + scriptDirectory); } File f = new File(scriptDirectory, fileName); getLog().info("Creating Jangaroo application script '" + f.getAbsolutePath() + "'."); return new OutputStreamWriter(new FileOutputStream(f), "UTF-8"); } private void includeJangarooModuleScript(File scriptDirectory, Artifact artifact, Writer jangarooApplicationWriter, Writer jangarooApplicationAllWriter) throws IOException { ZipFile zipFile = new ZipFile(artifact.getFile()); ZipEntry zipEntry = zipFile.getEntry(computeModuleJsFileName(artifact.getArtifactId())); ModuleSource jooModuleSource = zipEntry != null ? new ZipEntryModuleSource(zipFile, zipEntry) : null; writeJangarooModuleScript(scriptDirectory, artifact, jooModuleSource, jangarooApplicationWriter, jangarooApplicationAllWriter); } private static final Pattern LOAD_SCRIPT_CODE_PATTERN = Pattern.compile("\\s*joo\\.loadScript\\(['\"]([^'\"]+)['\"]\\s*[,)].*"); private static final Pattern LOAD_MODULE_CODE_PATTERN = Pattern.compile("\\s*joo\\.loadModule\\(['\"]([^'\"]+)['\"]\\s*,\\s*['\"]([^'\"]+)['\"].*"); private static String fromArtifactMessage(Artifact artifact) { return fromArtifactMessage(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion()); } private static String fromArtifactMessage(String groupId, String artifactId, String version) { return "// FROM " + fullArtifactName(groupId, artifactId, version) + ":\n"; } private static String fullArtifactName(Artifact artifact) { return fullArtifactName(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion()); } private static String fullArtifactName(String groupId, String artifactId, String version) { return String.format("%s:%s:%s", groupId, artifactId, version); } private void writeJangarooModuleScript(File scriptDirectory, Artifact artifact, ModuleSource jooModuleSource, Writer jangarooApplicationWriter, Writer jangarooApplicationAllWriter) throws IOException { String fullArtifactName = fullArtifactName(artifact); if (jooModuleSource == null) { getLog().debug("No " + artifact.getArtifactId() + ".module.js in " + fullArtifactName + "."); writeModule(scriptDirectory, artifact, jangarooApplicationWriter, jangarooApplicationAllWriter); } else { getLog().info("Appending " + artifact.getArtifactId() + ".module.js from " + fullArtifactName); String fromMessage = fromArtifactMessage(artifact); jangarooApplicationWriter.write(fromMessage); appendFromInputStream(jangarooApplicationWriter, jooModuleSource.getInputStream()); jangarooApplicationAllWriter.write(fromMessage); writeModuleWithInlineScripts(scriptDirectory, jooModuleSource, jangarooApplicationAllWriter); } } private void writeModuleWithInlineScripts(File scriptDirectory, ModuleSource jooModuleSource, Writer jangarooApplicationAllWriter) throws IOException { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(jooModuleSource.getInputStream(), "UTF-8")); while (true) { String line = bufferedReader.readLine(); if (line == null) { break; } String scriptFilename = null; Matcher loadModuleMatcher = LOAD_MODULE_CODE_PATTERN.matcher(line); if (loadModuleMatcher.matches()) { String groupId = loadModuleMatcher.group(1); String artifactId = loadModuleMatcher.group(2); scriptFilename = "joo/" + groupId + "." + artifactId + ".classes.js"; getLog().debug(" found loadModule: " + groupId + " / " + artifactId); } else { Matcher loadScriptMatcher = LOAD_SCRIPT_CODE_PATTERN.matcher(line); if (loadScriptMatcher.matches()) { scriptFilename = loadScriptMatcher.group(1); getLog().debug(" found loadScript: " + scriptFilename); } } File scriptFile = null; if (scriptFilename != null) { scriptFile = new File(scriptDirectory.getParent(), scriptFilename); if (!scriptFile.exists()) { scriptFile = null; } } if (scriptFile == null) { jangarooApplicationAllWriter.write(line + '\n'); } else { appendFile(jangarooApplicationAllWriter, scriptFile); } } } private void writeModule(File scriptDirectory, Artifact artifact, Writer jangarooApplicationWriter, Writer jangarooApplicationAllWriter) throws IOException { writeModule(scriptDirectory, artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion(), jangarooApplicationWriter, jangarooApplicationAllWriter); } protected void writeModule(File scriptDirectory, String groupId, String artifactId, String version, Writer jangarooApplicationWriter, Writer jangarooApplicationAllWriter) throws IOException { File classesJsFile = new File(scriptDirectory, groupId + "." + artifactId + ".classes.js"); if (classesJsFile.exists()) { getLog().debug("Creating joo.loadModule(...) code for / appending .classes.js of " + fullArtifactName(groupId, artifactId, version) + "."); jangarooApplicationWriter.write(fromArtifactMessage(groupId, artifactId, version)); jangarooApplicationWriter.write("joo.loadModule(\"" + groupId + "\",\"" + artifactId + "\");\n"); jangarooApplicationAllWriter.write(fromArtifactMessage(groupId, artifactId, version)); appendFile(jangarooApplicationAllWriter, classesJsFile); } else { getLog().debug("No file " + classesJsFile.getAbsolutePath() + " in module " + fullArtifactName(groupId, artifactId, version) +"."); } } private void appendFile(Writer writer, File file) throws IOException { appendFromInputStream(writer, new BOMStripperInputStream(new FileInputStream(file))); } private void appendFromInputStream(Writer writer, InputStream inputStream) throws IOException { IOUtil.copy(inputStream, writer, "UTF-8"); writer.write('\n'); // file might not end with new-line, better insert one } private Map<String, Artifact> artifactByInternalId() { final Map<String, Artifact> internalId2Artifact = new HashMap<String, Artifact>(); for (Artifact artifact : getArtifacts()) { if ("jar".equals(artifact.getType())) { String internalId = getInternalId(artifact); internalId2Artifact.put(internalId, artifact); } } return internalId2Artifact; } private List<String> getDependencies(Artifact artifact) throws ProjectBuildingException { MavenProject mp = mavenProjectBuilder.buildFromRepository(artifact, remoteRepositories, localRepository, true); List<String> deps = new LinkedList<String>(); for (Dependency dep : getDependencies(mp)) { if ("jar".equals(dep.getType()) && (dep.getScope().equals("compile") || dep.getScope().equals("runtime"))) { deps.add(getInternalId(dep)); } } return deps; } @SuppressWarnings({ "unchecked" }) private static List<Dependency> getDependencies(MavenProject mp) { return (List<Dependency>) mp.getDependencies(); } @SuppressWarnings({ "unchecked" }) protected Set<Artifact> getArtifacts() { return (Set<Artifact>)project.getArtifacts(); } private static interface ModuleSource { InputStream getInputStream() throws IOException; } private static class FileModuleSource implements ModuleSource { private final File file; private FileModuleSource(File file) { this.file = file; } @Override public InputStream getInputStream() throws IOException { return new BOMStripperInputStream(new FileInputStream(file)); } } private static class ZipEntryModuleSource implements ModuleSource { private final ZipFile zipFile; private final ZipEntry zipEntry; private ZipEntryModuleSource(ZipFile zipFile, ZipEntry zipEntry) { this.zipFile = zipFile; this.zipEntry = zipEntry; } @Override public InputStream getInputStream() throws IOException { return new BOMStripperInputStream(zipFile.getInputStream(zipEntry)); } } }