blob: e9cdb103a80c555cc8fcef41aa17d3605404d44a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
*
* https://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.
*
*/
/**
* jlink.java links together multiple .jar files Original code by Patrick
* Beard. Modifications to work with ANT by Matthew Kuperus Heun.
*
*/
package org.apache.tools.ant.taskdefs.optional.jlink;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.List;
import java.util.Vector;
import java.util.zip.CRC32;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
// CheckStyle:TypeNameCheck OFF - bc
/**
* jlink links together multiple .jar files.
*/
public class jlink {
private static final int BUFFER_SIZE = 8192;
private static final int VECTOR_INIT_SIZE = 10;
private String outfile = null;
private List<String> mergefiles = new Vector<>(VECTOR_INIT_SIZE);
private List<String> addfiles = new Vector<>(VECTOR_INIT_SIZE);
private boolean compression = false;
// CheckStyle:VisibilityModifier OFF - bc
byte[] buffer = new byte[BUFFER_SIZE];
// CheckStyle:VisibilityModifier OFF - bc
/** The file that will be created by this instance of jlink.
* @param outfile the file to create.
*/
public void setOutfile(String outfile) {
if (outfile == null) {
return;
}
this.outfile = outfile;
}
/**
* Adds a file to be merged into the output.
* @param fileToMerge the file to merge into the output.
*/
public void addMergeFile(String fileToMerge) {
if (fileToMerge == null) {
return;
}
mergefiles.add(fileToMerge);
}
/** Adds a file to be added into the output.
* @param fileToAdd the file to add to the output.
*/
public void addAddFile(String fileToAdd) {
if (fileToAdd == null) {
return;
}
addfiles.add(fileToAdd);
}
/**
* Adds several files to be merged into the output.
* @param filesToMerge an array of files to merge into the output.
*/
public void addMergeFiles(String... filesToMerge) {
if (filesToMerge == null) {
return;
}
for (String element : filesToMerge) {
addMergeFile(element);
}
}
/**
* Adds several files to be added into the output.
* @param filesToAdd an array of files to add to the output.
*/
public void addAddFiles(String... filesToAdd) {
if (filesToAdd == null) {
return;
}
for (String element : filesToAdd) {
addAddFile(element);
}
}
/**
* Determines whether output will be compressed.
* @param compress if true use compression.
*/
public void setCompression(boolean compress) {
this.compression = compress;
}
/**
* Performs the linking of files. Addfiles are added to the output as-is.
* For example, a jar file is added to the output as a jar file. However,
* mergefiles are first examined for their type. If it is a jar or zip
* file, the contents will be extracted from the mergefile and entered
* into the output. If a zip or jar file is encountered in a subdirectory
* it will be added, not merged. If a directory is encountered, it becomes
* the root entry of all the files below it. Thus, you can provide
* multiple, disjoint directories, as addfiles: they will all be added in
* a rational manner to outfile.
* @throws Exception on error.
*/
public void link() throws Exception { //NOSONAR
try (ZipOutputStream output =
new ZipOutputStream(Files.newOutputStream(Paths.get(outfile)))) {
if (compression) {
output.setMethod(ZipOutputStream.DEFLATED);
output.setLevel(Deflater.DEFAULT_COMPRESSION);
} else {
output.setMethod(ZipOutputStream.STORED);
}
for (String path : mergefiles) {
File f = new File(path);
if (f.getName().endsWith(".jar")
|| f.getName().endsWith(".zip")) {
//Do the merge
mergeZipJarContents(output, f);
} else {
//Add this file to the addfiles Vector and add it
//later at the top level of the output file.
addAddFile(path);
}
}
for (String name : addfiles) {
File f = new File(name);
if (f.isDirectory()) {
addDirContents(output, f, f.getName() + '/', compression);
} else {
addFile(output, f, "", compression);
}
}
}
}
/**
* The command line entry point for jlink.
* @param args an array of arguments
*/
public static void main(String[] args) {
// jlink output input1 ... inputN
if (args.length < 2) {
System.out.println("usage: jlink output input1 ... inputN");
System.exit(1);
}
jlink linker = new jlink();
linker.setOutfile(args[0]);
// To maintain compatibility with the command-line version,
// we will only add files to be merged.
for (int i = 1; i < args.length; i++) {
linker.addMergeFile(args[i]);
}
try {
linker.link();
} catch (Exception ex) {
System.err.print(ex.getMessage());
}
}
/*
* Actually performs the merging of f into the output.
* f should be a zip or jar file.
*/
private void mergeZipJarContents(ZipOutputStream output, File f) throws IOException {
//Check to see that the file with name "name" exists.
if (!f.exists()) {
return;
}
try (ZipFile zipf = new ZipFile(f)) {
Enumeration<? extends ZipEntry> entries = zipf.entries();
while (entries.hasMoreElements()) {
ZipEntry inputEntry = entries.nextElement();
//Ignore manifest entries. They're bound to cause conflicts between
//files that are being merged. User should supply their own
//manifest file when doing the merge.
String inputEntryName = inputEntry.getName();
int index = inputEntryName.indexOf("META-INF");
if (index < 0) {
//META-INF not found in the name of the entry. Go ahead and process it.
try {
output.putNextEntry(processEntry(zipf, inputEntry));
} catch (ZipException ex) {
//If we get here, it could be because we are trying to put a
//directory entry that already exists.
//For example, we're trying to write "com", but a previous
//entry from another mergefile was called "com".
//In that case, just ignore the error and go on to the
//next entry.
if (ex.getMessage().contains("duplicate")) {
//It was the duplicate entry.
continue;
}
// I hate to admit it, but we don't know what happened
// here. Throw the Exception.
throw ex;
}
try (InputStream in = zipf.getInputStream(inputEntry)) {
int len = buffer.length;
int count = -1;
while ((count = in.read(buffer, 0, len)) > 0) {
output.write(buffer, 0, count);
}
output.closeEntry();
}
}
}
}
}
/*
* Adds contents of a directory to the output.
*/
private void addDirContents(ZipOutputStream output, File dir, String prefix,
boolean compress) throws IOException {
String[] names = dir.list();
if (names == null || names.length == 0) {
return;
}
for (String name : dir.list()) {
File file = new File(dir, name);
if (file.isDirectory()) {
addDirContents(output, file, prefix + name + '/', compress);
} else {
addFile(output, file, prefix, compress);
}
}
}
/*
* Gets the name of an entry in the file. This is the real name
* which for a class is the name of the package with the class
* name appended.
*/
private String getEntryName(File file, String prefix) {
String name = file.getName();
if (!name.endsWith(".class")) {
// see if the file is in fact a .class file, and determine its actual name.
try (InputStream input = Files.newInputStream(file.toPath())) {
String className = ClassNameReader.getClassName(input);
if (className != null) {
return className.replace('.', '/') + ".class";
}
} catch (IOException ioe) {
//do nothing
}
}
System.out.printf(
"From %1$s and prefix %2$s, creating entry %2$s%3$s%n",
file.getPath(), prefix, name);
return prefix + name;
}
/*
* Adds a file to the output stream.
*/
private void addFile(ZipOutputStream output, File file, String prefix,
boolean compress) throws IOException {
//Make sure file exists
if (!file.exists()) {
return;
}
ZipEntry entry = new ZipEntry(getEntryName(file, prefix));
entry.setTime(file.lastModified());
entry.setSize(file.length());
if (!compress) {
entry.setCrc(calcChecksum(file));
}
addToOutputStream(output, Files.newInputStream(file.toPath()), entry);
}
/*
* A convenience method that several other methods might call.
*/
private void addToOutputStream(ZipOutputStream output, InputStream input,
ZipEntry ze) throws IOException {
try {
output.putNextEntry(ze);
} catch (ZipException zipEx) {
//This entry already exists. So, go with the first one.
input.close();
return;
}
int numBytes;
while ((numBytes = input.read(buffer)) > 0) {
output.write(buffer, 0, numBytes);
}
output.closeEntry();
input.close();
}
/*
* A method that does the work on a given entry in a mergefile.
* The big deal is to set the right parameters in the ZipEntry
* on the output stream.
*/
private ZipEntry processEntry(ZipFile zip, ZipEntry inputEntry) {
/*
First, some notes.
On MRJ 2.2.2, getting the size, compressed size, and CRC32 from the
ZipInputStream does not work for compressed (deflated) files. Those calls return -1.
For uncompressed (stored) files, those calls do work.
However, using ZipFile.getEntries() works for both compressed and
uncompressed files.
Now, from some simple testing I did, it seems that the value of CRC-32 is
independent of the compression setting. So, it should be easy to pass this
information on to the output entry.
*/
String name = inputEntry.getName();
if (!inputEntry.isDirectory() && !name.endsWith(".class")) {
try (InputStream input = zip.getInputStream(zip.getEntry(name))) {
String className = ClassNameReader.getClassName(input);
if (className != null) {
name = className.replace('.', '/') + ".class";
}
} catch (IOException ioe) {
//do nothing
}
}
ZipEntry outputEntry = new ZipEntry(name);
outputEntry.setTime(inputEntry.getTime());
outputEntry.setExtra(inputEntry.getExtra());
outputEntry.setComment(inputEntry.getComment());
outputEntry.setTime(inputEntry.getTime());
if (compression) {
outputEntry.setMethod(ZipEntry.DEFLATED);
//Note, don't need to specify size or crc for compressed files.
} else {
outputEntry.setMethod(ZipEntry.STORED);
outputEntry.setCrc(inputEntry.getCrc());
outputEntry.setSize(inputEntry.getSize());
}
return outputEntry;
}
/*
* Necessary in the case where you add a entry that
* is not compressed.
*/
private long calcChecksum(File f) throws IOException {
return calcChecksum(
new BufferedInputStream(Files.newInputStream(f.toPath())));
}
/*
* Necessary in the case where you add a entry that
* is not compressed.
*/
private long calcChecksum(InputStream in) throws IOException {
CRC32 crc = new CRC32();
int len = buffer.length;
int count;
while ((count = in.read(buffer, 0, len)) > 0) {
crc.update(buffer, 0, count);
}
in.close();
return crc.getValue();
}
}