/*
* Copyright 2012 James Moger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.moxie.ant;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Jar;
import org.apache.tools.ant.taskdefs.Manifest;
import org.apache.tools.ant.taskdefs.Manifest.Attribute;
import org.apache.tools.ant.taskdefs.ManifestException;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Path.PathElement;
import org.apache.tools.ant.types.resources.FileResource;
import org.moxie.Build;
import org.moxie.MoxieException;
import org.moxie.MxLauncher;
import org.moxie.Scope;
import org.moxie.Toolkit;
import org.moxie.Toolkit.Key;
import org.moxie.console.Console;
import org.moxie.maxml.MaxmlMap;
import org.moxie.utils.FileUtils;
import org.moxie.utils.StringUtils;
public class MxGenJar extends GenJar {
Build build;
Console console;
LauncherSpec launcher;
ClassSpec mainclass;
boolean classResolution;
boolean fatjar;
boolean includeResources;
boolean excludePomFiles;
String includes;
boolean packageSources;
String resourceFolderPrefix;
String tag;
String classifier;
private boolean configured;
Boolean showtitle;
public MxGenJar() {
super();
setTaskName("mx:genjar");
}
/**
* Builds a <mainclass> element.
*
* @return A <mainclass> element.
*/
public ClassSpec createMainclass() {
if (mainclass == null) {
ClassSpec cs = new ClassSpec(getProject());
mainclass = cs;
jarSpecs.add(cs);
return cs;
}
throw new MoxieException("Can only specify one main class");
}
/**
* Builds a <launcher> element.
*
* @return A <launcher> element.
*/
public LauncherSpec createLauncher() {
if (launcher == null) {
LauncherSpec cs = new LauncherSpec(getProject());
launcher = cs;
jarSpecs.add(cs);
return cs;
}
throw new MoxieException("Can only specify one launcher class");
}
public boolean getFatjar() {
return fatjar;
}
public void setFatjar(boolean value) {
this.fatjar = value;
}
public boolean getExcludeclasspathjars() {
return excludeClasspathJars;
}
public void setExcludeclasspathjars(boolean value) {
this.excludeClasspathJars = value;
}
public boolean getExcludepomfiles() {
return excludePomFiles;
}
public void setExcludepomfiles(boolean value) {
this.excludePomFiles = value;
}
public boolean getIncludesresources() {
return includeResources;
}
public void setIncluderesources(boolean copy) {
this.includeResources = copy;
}
public String getIncludes() {
return includes;
}
public void setIncludes(String includes) {
this.includes = includes;
}
public void setResourceFolderPrefix(String resourceFolderPrefix) {
this.resourceFolderPrefix = resourceFolderPrefix;
}
public String getExcludes() {
return excludes;
}
public void setExcludes(String excludes) {
this.excludes = excludes;
}
public String getTag() {
return tag;
}
public void setTag(String tag) {
this.tag = tag;
}
public String getClassifier() {
return classifier;
}
public void setClassifier(String classifier) {
this.classifier = classifier;
}
public boolean getPackagesources() {
return packageSources;
}
public void setPackagesources(boolean sources) {
this.packageSources = sources;
}
public void setShowtitle(boolean value) {
this.showtitle = value;
}
public boolean isShowTitle() {
return showtitle == null || showtitle;
}
@Override
public void setProject(Project project) {
super.setProject(project);
Build build = (Build) getProject().getReference(Key.build.referenceId());
if (build != null) {
configure(build);
}
}
private void configure(Build build) {
configured = true;
MaxmlMap attributes = build.getConfig().getTaskAttributes(getTaskName());
if (attributes == null) {
build.getConsole().error(getTaskName() + " attributes are null!");
return;
}
AttributeReflector.setAttributes(getProject(), this, attributes);
}
@Override
public void execute() throws BuildException {
build = (Build) getProject().getReference(Key.build.referenceId());
console = build.getConsole();
if (!configured) {
// called from moxie.package
configure(build);
}
if (fatjar && excludeClasspathJars) {
throw new BuildException("Can not specify fatjar and excludeClasspathJars!");
}
// automatic manifest entries from Moxie metadata
configureManifest(mft);
if (mainclass == null) {
String mc = build.getConfig().getProjectConfig().getMainclass();
if (!StringUtils.isEmpty(mc)) {
ClassSpec cs = new ClassSpec(getProject());
mainclass = cs;
mainclass.setName(mc);
jarSpecs.add(cs);
}
}
if (mainclass != null) {
String mc = mainclass.getName().replace('/', '.');
if (mc.endsWith(".class")) {
mc = mc.substring(0, mc.length() - ".class".length());
}
if (launcher == null) {
// use specified mainclass
setManifest(mft, "Main-Class", mc);
} else {
// inject Moxie Launcher class
String mx = launcher.getName().replace('/', '.');
if (mx.endsWith(".class")) {
mx = mx.substring(0, mx.length() - ".class".length());
}
setManifest(mft, "Main-Class", mx);
setManifest(mft, "mxMain-Class", mc);
String paths = launcher.getPaths();
if (!StringUtils.isEmpty(paths)) {
setManifest(mft, "mxMain-Paths", paths);
}
}
}
// automatic classpath resolution, if not manually specified
if (classpath == null) {
Path cp = buildClasspath(build, Scope.compile, tag);
if (fatjar) {
// FatJar generation
classpath = createClasspath();
for (String path : cp.list()) {
if (path.toLowerCase().endsWith(".jar")) {
LibrarySpec lib = createLibrary();
lib.setJar(path);
} else {
PathElement element = classpath.createPathElement();
element.setPath(path);
}
}
} else {
// standard GenJar class dependency resolution
classpath = cp;
}
}
if (destFile == null) {
setDestfile(build.getBuildArtifact(classifier));
}
if (destFile.getParentFile() != null) {
destFile.getParentFile().mkdirs();
}
version = build.getPom().version;
File outputFolder = build.getConfig().getOutputDirectory(Scope.compile);
if (excludes == null) {
excludes = Toolkit.DEFAULT_RESOURCE_EXCLUDES;
}
// include resources from the project source folders
Resource resources = createResource();
if (!StringUtils.isEmpty(resourceFolderPrefix)) {
resources.setPrefix(resourceFolderPrefix);
}
for (File dir : build.getConfig().getSourceDirectories(Scope.compile, tag)) {
FileSet res = resources.createFileset();
res.setDir(dir);
res.setExcludes(excludes);
}
if (includeResources) {
// include resources from the project resource folders
for (File dir : build.getConfig().getResourceDirectories(Scope.compile, tag)) {
FileSet res = resources.createFileset();
res.setExcludes(excludes);
res.setDir(dir);
}
for (Build module : build.getSolver().getLinkedModules()) {
// include resources from module source folders
File dir = module.getConfig().getOutputDirectory(Scope.compile);
FileSet res = resources.createFileset();
res.setDir(dir);
res.setExcludes(excludes);
// include resources from the module resource folders
for (File resDir : module.getConfig().getResourceDirectories(Scope.compile)) {
FileSet resSet = resources.createFileset();
res.setExcludes(Toolkit.DEFAULT_RESOURCE_EXCLUDES);
resSet.setDir(resDir);
}
}
}
if (isShowTitle()) {
console.title(getClass(), destFile.getName());
}
console.debug(getTaskName() + " configuration");
// display specified mxgenjar attributes
MaxmlMap attributes = build.getConfig().getTaskAttributes(getTaskName());
AttributeReflector.logAttributes(this, attributes, console);
// optionally inject MxLauncher utility
if (launcher != null) {
if (launcher.getName().equals(MxLauncher.class.getName().replace('.', '/') + ".class")) {
// inject MxLauncher into the output folder of the project
for (String cn : Arrays.asList(MxLauncher.class.getName(), MxLauncher.class.getName() + "$1")) {
try {
String fn = cn.replace('.', '/') + ".class";
InputStream is = MxLauncher.class.getResourceAsStream("/" + fn);
if (is == null) {
continue;
}
build.getConsole().log("Injecting {0} into output folder", cn);
File file = new File(outputFolder, fn.replace('/', File.separatorChar));
if (file.exists()) {
file.delete();
}
file.getParentFile().mkdirs();
FileOutputStream os = new FileOutputStream(file, false);
byte [] buffer = new byte[4096];
int len = 0;
while ((len = is.read(buffer)) > 0) {
os.write(buffer, 0, len);
}
is.close();
os.flush();
os.close();
// add these files to the jarSpecs
ClassSpec cs = new ClassSpec(getProject());
cs.setName(cn);
jarSpecs.add(cs);
} catch (Exception e) {
build.getConsole().error(e, "Failed to inject {0} into {1}",
launcher.getName(), outputFolder);
}
}
}
}
long start = System.currentTimeMillis();
try {
super.execute();
} catch (ResolutionFailedException e) {
String msg;
if (tag == null) {
String template = "Unable to resolve: {0}\n\n{1} could not be located on the classpath.\n";
msg = MessageFormat.format(template, e.resolvingclass, e.missingclass, tag == null ? "classpath" : ("\"" + tag + "\" classpath"), tag);
} else {
String template = "Unable to resolve: {0}\n\n{1} could not be located on the \"{2}\" classpath.\nPlease add the \":{2}\" tag to the appropriate dependency in your Moxie descriptor file.\n";
msg = MessageFormat.format(template, e.resolvingclass, e.missingclass, tag);
}
throw new MoxieException(msg);
}
if (fatjar) {
// try to merge duplicate META-INF/services files
JarUtils.mergeMetaInfServices(console, destFile);
}
console.log(1, destFile.getAbsolutePath());
console.log(1, "{0} KB, generated in {1} ms", (destFile.length()/1024), System.currentTimeMillis() - start);
/*
* Build sources jar
*/
if (packageSources) {
String name = destFile.getName();
if (!StringUtils.isEmpty(classifier)) {
// replace the classifier with "sources"
name = name.replace(classifier, "sources");
} else {
// append -sources to the filename before the extension
name = name.substring(0, name.lastIndexOf('.')) + "-sources" + name.substring(name.lastIndexOf('.'));
}
File sourcesFile = new File(destFile.getParentFile(), name);
if (sourcesFile.exists()) {
sourcesFile.delete();
}
Jar jar = new Jar();
jar.setTaskName(getTaskName());
jar.setProject(getProject());
// set the destination file
jar.setDestFile(sourcesFile);
// use the resolved classes to determine included source files
List<FileResource> sourceFiles = new ArrayList<FileResource>();
Map<File, Set<String>> packageResources = new HashMap<File, Set<String>>();
if (resolvedLocal.size() == 0) {
console.warn(getTaskName() + " has not resolved any class files local to {0}", build.getPom().getManagementId());
}
List<File> folders = build.getConfig().getSourceDirectories(Scope.compile, tag);
for (String className : resolvedLocal) {
String sourceName = className.substring(0, className.length() - ".class".length()).replace('.', '/') + ".java";
console.debug(sourceName);
for (File folder : folders) {
File file = new File(folder, sourceName);
if (file.exists()) {
FileResource resource = new FileResource(getProject(), file);
resource.setBaseDir(folder);
sourceFiles.add(resource);
if (!packageResources.containsKey(folder)) {
// always include default package resources
packageResources.put(folder, new TreeSet<String>(Arrays.asList( "/*" )));
}
String packagePath = FileUtils.getRelativePath(folder, file.getParentFile());
packageResources.get(folder).add(packagePath + "/*");
console.debug(1, file.getAbsolutePath());
break;
}
}
}
// add the discovered source files for the resolved classes
jar.add(new FileResourceSet(sourceFiles));
// add the resolved package folders for resource files
for (Map.Entry<File, Set<String>> entry : packageResources.entrySet()) {
FileSet res = new FileSet();
res.setDir(entry.getKey());
res.setExcludes(excludes);
StringBuilder includes = new StringBuilder();
for (String packageName : entry.getValue()) {
includes.append(packageName + ",");
}
includes.setLength(includes.length() - 1);
res.setIncludes(includes.toString());
console.debug("adding resource fileset {0}", entry.getKey());
console.debug(1, "includes={0}", includes.toString());
jar.add(res);
}
if (includeResources) {
for (File dir : build.getConfig().getResourceDirectories(Scope.compile, tag)) {
FileSet res = resources.createFileset();
res.setDir(dir);
res.setExcludes(Toolkit.DEFAULT_RESOURCE_EXCLUDES);
jar.add(res);
}
}
// set the source jar manifest
try {
Manifest mft = new Manifest();
configureManifest(mft);
jar.addConfiguredManifest(mft);
} catch (ManifestException e) {
console.error(e);
}
start = System.currentTimeMillis();
jar.execute();
console.log(1, sourcesFile.getAbsolutePath());
console.log(1, "{0} KB, generated in {1} ms", (sourcesFile.length()/1024), System.currentTimeMillis() - start);
}
}
void configureManifest(Manifest manifest) {
// set manifest entries from Moxie metadata
Manifest mft = new Manifest();
setManifest(mft, "Created-By", "Moxie v" + Toolkit.getVersion());
setManifest(mft, "Build-Jdk", System.getProperty("java.version"));
setManifest(mft, "Build-Date", new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
setManifest(mft, "Implementation-Title", Key.name);
setManifest(mft, "Implementation-Vendor", Key.organization);
setManifest(mft, "Implementation-Vendor-Id", Key.groupId);
setManifest(mft, "Implementation-Vendor-URL", Key.url);
setManifest(mft, "Implementation-Version", Key.version);
setManifest(mft, "Bundle-Name", Key.name);
setManifest(mft, "Bundle-SymbolicName", Key.artifactId);
setManifest(mft, "Bundle-Version", Key.version);
setManifest(mft, "Bundle-Vendor", Key.organization);
setManifest(mft, "Maven-Url", Key.mavenUrl);
setManifest(mft, "Commit-Id", Key.commitId);
try {
manifest.merge(mft, true);
} catch (ManifestException e) {
console.error(e, "Failed to configure manifest!");
}
}
void setManifest(Manifest man, String key, Key prop) {
// try project property
String value = getProject().getProperty(prop.projectId());
if (value == null) {
return;
}
if (!StringUtils.isEmpty(value)) {
setManifest(man, key, value);
}
}
void setManifest(Manifest man, String key, String value) {
if (!StringUtils.isEmpty(value)) {
try {
man.addConfiguredAttribute(new Attribute(key, value));
} catch (ManifestException e) {
console.error(e, "Failed to set manifest attribute \"{0}\"!", key);
}
}
}
/**
* Add the Maven META-INF files.
*/
@Override
protected void writeJarEntries(JarOutputStream jos) {
if (excludePomFiles) {
return;
}
DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(jos));
Properties properties = new Properties();
properties.put(Key.groupId.name(), build.getPom().groupId);
properties.put(Key.artifactId.name(), build.getPom().artifactId);
properties.put(Key.version.name(), version);
try {
ZipEntry entry = new ZipEntry(MessageFormat.format("META-INF/maven/{0}/{1}/pom.properties", build.getPom().groupId, build.getPom().artifactId));
jos.putNextEntry(entry);
properties.store(dos, "Generated by Moxie");
dos.flush();
jos.closeEntry();
} catch (IOException e) {
console.error(e, "failed to write pom.properties!");
}
try {
ZipEntry entry = new ZipEntry(MessageFormat.format("META-INF/maven/{0}/{1}/pom.xml", build.getPom().groupId, build.getPom().artifactId));
jos.putNextEntry(entry);
dos.write(build.getPom().toXML(false).getBytes("UTF-8"));
dos.flush();
jos.closeEntry();
} catch (IOException e) {
console.error(e, "failed to write pom.xml!");
}
}
private Path buildClasspath(Build build, Scope scope, String tag) {
List<File> jars = build.getSolver().getClasspath(scope, tag);
Path cp = new Path(getProject());
// output folder
PathElement of = cp.createPathElement();
of.setLocation(build.getConfig().getOutputDirectory(scope));
if (!scope.isDefault()) {
of.setLocation(build.getConfig().getOutputDirectory(Scope.compile));
}
// add project dependencies
for (File folder : buildDependentProjectsClasspath(build)) {
PathElement element = cp.createPathElement();
element.setLocation(folder);
}
// jars
for (File jar : jars) {
PathElement element = cp.createPathElement();
element.setLocation(jar);
}
return cp;
}
private List<File> buildDependentProjectsClasspath(Build build) {
List<File> folders = new ArrayList<File>();
List<Build> libraryProjects = build.getSolver().getLinkedModules();
for (Build project : libraryProjects) {
File outputFolder = project.getConfig().getOutputDirectory(Scope.compile);
folders.add(outputFolder);
}
return folders;
}
}