element is allowed.",
+ getLocation());
+ }
+
+ moduleVersion = new ModuleVersion();
+ return moduleVersion;
+ }
+
/**
* Initialize the zip output stream.
* @param zOut the zip output stream
@@ -661,6 +732,18 @@ protected void zipFile(InputStream is, ZipOutputStream zOut, String vPath,
+ " files include a " + INDEX_NAME + " which will"
+ " be replaced by a newly generated one.",
Project.MSG_WARN);
+ } else if (MODULE_INFO.matcher(vPath).matches()) {
+ java.nio.file.Path newModuleInfo = copyAndSetModuleAttributes(is);
+
+ try (InputStream newModuleInfoStream =
+ new BufferedInputStream(
+ Files.newInputStream(newModuleInfo))) {
+
+ super.zipFile(newModuleInfoStream,
+ zOut, vPath, lastModified, fromArchive, mode);
+ } finally {
+ Files.delete(newModuleInfo);
+ }
} else {
if (index && !vPath.contains("/")) {
rootEntries.add(vPath);
@@ -774,7 +857,7 @@ protected ArchiveState getResourcesToAdd(ResourceCollection[] rcs,
return new ArchiveState(true, manifests);
}
- // need to handle manifest as a special check
+ // need to handle manifest and modular attributes as a special check
if (zipFile.exists()) {
// if it doesn't exist, it will get created anyway, don't
// bother with any up-to-date checks.
@@ -785,6 +868,13 @@ protected ArchiveState getResourcesToAdd(ResourceCollection[] rcs,
log("Updating jar since the current jar has"
+ " no manifest", Project.MSG_VERBOSE);
needsUpdate = true;
+ } else if (version != null || moduleVersion != null || mainClass != null) {
+ /*
+ * Task's modular attributes were not set during
+ * the initial creation of the jar, so we need to
+ * make sure we can process them the next time around.
+ */
+ needsUpdate = true;
} else {
Manifest mf = createManifest();
if (!mf.equals(originalManifest)) {
@@ -1118,6 +1208,82 @@ private Resource[][] grabManifests(ResourceCollection[] rcs) {
return manifests;
}
+ /**
+ * Updates a module-info.class descriptor's module-specific attributes
+ * from this task's attributes, and writes the new module-info to a
+ * temporary file.
+ *
+ * The argument is consumed, even if there were no changes made
+ * to its content. Either way, returned file can be opened and
+ * the InputStream can be read from, just as the original InputStream
+ * would have been.
+ *
+ * @param originalModuleInfo module-info to update
+ *
+ * @return new module-info file, with task's attributes applied
+ *
+ * @throws IOException if stream cannot be read, or new module-info
+ * cannot be written
+ */
+ private java.nio.file.Path copyAndSetModuleAttributes(InputStream originalModuleInfo)
+ throws IOException {
+
+ if (version != null && moduleVersion != null) {
+ throw new BuildException(
+ "version attribute and nested cannot both be specified.",
+ getLocation());
+ }
+
+ String versionStr;
+ if (moduleVersion != null) {
+ try {
+ versionStr = moduleVersion.toModuleVersionString();
+ } catch (IllegalStateException e) {
+ throw new BuildException(e, getLocation());
+ }
+ } else {
+ versionStr = version;
+ }
+
+ java.nio.file.Path newModuleInfoFile =
+ Files.createTempFile("module-info-", ".class");
+
+ if (versionStr != null) {
+ log("Setting modular jar's version to \"" + versionStr + "\"",
+ Project.MSG_VERBOSE);
+ }
+ if (mainClass != null) {
+ log("Setting modular jar's main class to " + mainClass,
+ Project.MSG_VERBOSE);
+ }
+
+ try {
+ // Call via reflection, to avoid compile-time dependency
+ // on Java 9+ classes.
+ // Equivalent to:
+ // JarAttributeUpdater.writeNewModuleInfo(
+ // originalModuleInfo, versionStr, mainClass, newModuleInfoFile);
+ Class> updaterClass = Class.forName(
+ "org.apache.tools.ant.util.jarattr.JarAttributeUpdater");
+ updaterClass.getMethod("writeNewModuleInfo",
+ InputStream.class,
+ String.class,
+ String.class,
+ java.nio.file.Path.class).invoke(
+ null,
+ originalModuleInfo,
+ versionStr, mainClass,
+ newModuleInfoFile);
+ } catch (ReflectiveOperationException e) {
+ throw new BuildException(
+ "Unable to set modular attributes of jar file"
+ + " (possibly because this Ant was compiled for Java 8)"
+ + ": " + e, e, getLocation());
+ }
+
+ return newModuleInfoFile;
+ }
+
private Charset getManifestCharset() {
if (manifestEncoding == null) {
return Charset.defaultCharset();
diff --git a/src/main/org/apache/tools/ant/util/jarattr/Attribute.java b/src/main/org/apache/tools/ant/util/jarattr/Attribute.java
new file mode 100644
index 0000000000..5e666683e2
--- /dev/null
+++ b/src/main/org/apache/tools/ant/util/jarattr/Attribute.java
@@ -0,0 +1,156 @@
+/*
+ * 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
+ *
+ * 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.apache.tools.ant.util.jarattr;
+
+import java.io.InputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import java.util.Arrays;
+import java.util.List;
+
+// CheckStyle:LineLength OFF - Link is too long.
+/**
+ * {@code attribute_info} data as defined in the "{@code class} File Format"
+ * section of the Java Virtual Machine Specification.
+ *
+ * @see {@code class File Format
+ */
+// CheckStyle:LineLength ON
+abstract class Attribute {
+ /** Index in constant pool of attribute name. */
+ private final int attributeNameIndex;
+
+ /** Length of attribute data in bytes. */
+ private final int attributeLength;
+
+ /**
+ * Initializes a new instance.
+ *
+ * @param attributeNameIndex index in constant pool of attribute's name
+ * @param length size of attribute data in bytes
+ */
+ Attribute(int attributeNameIndex,
+ int length) {
+ this.attributeNameIndex = attributeNameIndex;
+ this.attributeLength = length;
+ }
+
+ /**
+ * Returns the index in the constant pool of the Utf8 constant which
+ * contains this attribute's name.
+ *
+ * @return index in constant pool of this attribute's name
+ */
+ int attributeNameIndex() {
+ return attributeNameIndex;
+ }
+
+ /**
+ * Returns the size of this attribute's data in bytes, not including
+ * the name index and attribute length themselves.
+ *
+ * @return size of attribute's data in bytes
+ */
+ int attributeLength() {
+ return attributeLength;
+ }
+
+ /**
+ * Writes this attribute's data in class file format.
+ * The first two bytes are always the {@link #attributeNameIndex},
+ * and the next four bytes are always the {@link #attributeLength}.
+ *
+ * @param out stream to which attribute should be written
+ *
+ * @throws IOException if stream cannot be written to
+ */
+ abstract void writeTo(DataOutputStream out)
+ throws IOException;
+
+ /**
+ * Reads a concrete {@code Attribute} object from a
+ * {@code module-info.class} descriptor. The stream is presumed to
+ * point to the start of an {@code attribute_info} block.
+ *
+ * @param in module-info stream to read from
+ * @param constantPool fully read constant pool of module-info;
+ * never modified by this method
+ *
+ * @return new {@code Attribute} instance, never {@code null}
+ */
+ static Attribute readFrom(DataInputStream in,
+ List extends Constant> constantPool)
+ throws IOException {
+ int nameIndex = in.readUnsignedShort();
+ int len = in.readInt();
+
+ Constant c = constantPool.get(nameIndex - 1);
+ if (!(c instanceof UTF8Constant)) {
+ throw new ClassFormatException(
+ "Attribute's attribute_name_index (" + nameIndex + ")"
+ + " does not point to a CONSTANT_Utf8 in the constant pool");
+ }
+
+ UTF8Constant utf8 = (UTF8Constant) c;
+ String name = utf8.value();
+
+ if (name.equals(ModuleAttribute.NAME)) {
+ return ModuleAttribute.readFrom(in, nameIndex, len);
+ } else if (name.equals(MainClassAttribute.NAME)) {
+ int mainClassNameIndex = in.readUnsignedShort();
+ return new MainClassAttribute(nameIndex, len, mainClassNameIndex);
+ } else {
+ return new OtherAttribute(nameIndex, len, readNBytes(in, len));
+ }
+ }
+
+ // Needed since we may be compiling or running with Java 9 or 10.
+ /**
+ * Identical to Java 11's {@code InputStream.readNBytes}, which reads
+ * the specified number of bytes (or less if EOF is reached).
+ *
+ * @param in stream from which to read
+ * @param count number of bytes to read
+ *
+ * @return new byte array containing {@code count} bytes (or fewer,
+ * if EOF is encountered before that many bytes can be read)
+ */
+ static byte[] readNBytes(InputStream in,
+ int count)
+ throws IOException {
+
+ byte[] bytes = new byte[count];
+
+ int totalBytesRead = 0;
+ while (totalBytesRead < count) {
+ int bytesRead =
+ in.read(bytes, totalBytesRead, count - totalBytesRead);
+
+ if (bytesRead < 0) {
+ return Arrays.copyOf(bytes, totalBytesRead);
+ }
+
+ totalBytesRead += bytesRead;
+ }
+
+ return bytes;
+ }
+}
diff --git a/src/main/org/apache/tools/ant/util/jarattr/ClassFormatException.java b/src/main/org/apache/tools/ant/util/jarattr/ClassFormatException.java
new file mode 100644
index 0000000000..b52e8b0f1b
--- /dev/null
+++ b/src/main/org/apache/tools/ant/util/jarattr/ClassFormatException.java
@@ -0,0 +1,59 @@
+/*
+ * 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
+ *
+ * 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.apache.tools.ant.util.jarattr;
+
+import java.io.IOException;
+
+/**
+ * Indicates an unexpected datum or condition was encountered
+ * while reading a module-info.class stream.
+ */
+public class ClassFormatException
+extends IOException {
+ private static final long serialVersionUID = 1;
+
+ /**
+ * Creates an exception with the specified message.
+ *
+ * @param message new exception's message
+ */
+ ClassFormatException(String message) {
+ super(message);
+ }
+
+ /**
+ * Creates an exception caused by another exception.
+ *
+ * @param cause exception which is the reason this one is being thrown
+ */
+ ClassFormatException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Creates an exception with an explicit message and cause.
+ *
+ * @param message new exception's message
+ * @param cause exception which is the reason this one is being thrown
+ */
+ ClassFormatException(String message,
+ Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/org/apache/tools/ant/util/jarattr/Constant.java b/src/main/org/apache/tools/ant/util/jarattr/Constant.java
new file mode 100644
index 0000000000..f78b2694fe
--- /dev/null
+++ b/src/main/org/apache/tools/ant/util/jarattr/Constant.java
@@ -0,0 +1,147 @@
+/*
+ * 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
+ *
+ * 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.apache.tools.ant.util.jarattr;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+/**
+ * Represents a {@code cp_info} entry in a the constant pool of a
+ * module-info.class file.
+ */
+abstract class Constant {
+ /** Constant pool tag value for {@code CONSTANT_Utf8}. */
+ static final byte UTF8 = 1;
+ /** Constant pool tag value for {@code CONSTANT_Integer}. */
+ static final byte INTEGER = 3;
+ /** Constant pool tag value for {@code CONSTANT_Float}. */
+ static final byte FLOAT = 4;
+ /** Constant pool tag value for {@code CONSTANT_Long}. */
+ static final byte LONG = 5;
+ /** Constant pool tag value for {@code CONSTANT_Double}. */
+ static final byte DOUBLE = 6;
+ /** Constant pool tag value for {@code CONSTANT_Class}. */
+ static final byte CLASS = 7;
+ /** Constant pool tag value for {@code CONSTANT_String}. */
+ static final byte STRING = 8;
+ /** Constant pool tag value for {@code CONSTANT_Fieldref}. */
+ static final byte FIELDREF = 9;
+ /** Constant pool tag value for {@code CONSTANT_Methodref}. */
+ static final byte METHODREF = 10;
+ /** Constant pool tag value for {@code CONSTANT_InterfaceMethodref}. */
+ static final byte INTERFACEMETHODREF = 11;
+ /** Constant pool tag value for {@code CONSTANT_NameAndType}. */
+ static final byte NAMEANDTYPE = 12;
+ /** Constant pool tag value for {@code CONSTANT_MethodHandle}. */
+ static final byte METHODHANDLE = 15;
+ /** Constant pool tag value for {@code CONSTANT_MethodType}. */
+ static final byte METHODTYPE = 16;
+ /** Constant pool tag value for {@code CONSTANT_Dynamic}. */
+ static final byte DYNAMIC = 17;
+ /** Constant pool tag value for {@code CONSTANT_InvokeDynamic}. */
+ static final byte INVOKEDYNAMIC = 18;
+ /** Constant pool tag value for {@code CONSTANT_Module}. */
+ static final byte MODULE = 19;
+ /** Constant pool tag value for {@code CONSTANT_Package}. */
+ static final byte PACKAGE = 20;
+
+ /** Tag value of this constant pool entry. */
+ private final byte tag;
+
+ /**
+ * Initializes a new constant instance.
+ *
+ * @param tag constant pool tag value of new instance
+ */
+ Constant(byte tag) {
+ this.tag = tag;
+ }
+
+ /**
+ * Returns this constant's JVM-defined tag value.
+ *
+ * @return JVM tag number of this constant
+ *
+ * @see #UTF8
+ * @see #INTEGER
+ * @see #FLOAT
+ * @see #LONG
+ * @see #DOUBLE
+ * @see #CLASS
+ * @see #STRING
+ * @see #FIELDREF
+ * @see #METHODREF
+ * @see #INTERFACEMETHODREF
+ * @see #NAMEANDTYPE
+ * @see #METHODHANDLE
+ * @see #METHODTYPE
+ * @see #DYNAMIC
+ * @see #INVOKEDYNAMIC
+ * @see #MODULE
+ * @see #PACKAGE
+ */
+ byte tag() {
+ return tag;
+ }
+
+ /**
+ * Reads a {@code cp_info} block from a {@code module-info.class}
+ * stream. Upon return, the stream is positioned immediately after
+ * the {@code cp_info} block.
+ *
+ * @param in {@code module-info.class} stream, positioned at start
+ * of {@code cp_info} data
+ *
+ * @return new {@code Constant} instance
+ *
+ * @throws IOException if stream cannot be read
+ */
+ static Constant readFrom(DataInputStream in)
+ throws IOException {
+
+ byte tag = (byte) in.readUnsignedByte();
+
+ switch (tag) {
+ case UTF8:
+ return new UTF8Constant(tag, in.readUTF());
+ case CLASS:
+ case STRING:
+ case METHODTYPE:
+ case MODULE:
+ case PACKAGE:
+ return new IndexedConstant(tag, in.readUnsignedShort());
+ case LONG:
+ case DOUBLE:
+ return new OtherConstant(tag, Attribute.readNBytes(in, 8));
+ default:
+ return new OtherConstant(tag, Attribute.readNBytes(in, 4));
+ }
+ }
+
+ /**
+ * Saves this constant in {@code module-info.class} format.
+ *
+ * @param out destination to write to
+ *
+ * @throws IOException if stream cannot be written to
+ */
+ abstract void writeTo(DataOutputStream out)
+ throws IOException;
+}
diff --git a/src/main/org/apache/tools/ant/util/jarattr/IndexedConstant.java b/src/main/org/apache/tools/ant/util/jarattr/IndexedConstant.java
new file mode 100644
index 0000000000..a65547285a
--- /dev/null
+++ b/src/main/org/apache/tools/ant/util/jarattr/IndexedConstant.java
@@ -0,0 +1,73 @@
+/*
+ * 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
+ *
+ * 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.apache.tools.ant.util.jarattr;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+/**
+ * A constant consisting only of an index of another constant in the
+ * constant pool. For example, {@code CONSTANT_Class_info} consists of
+ * only a one-byte tag and a two-byte {@code name_index}.
+ */
+class IndexedConstant
+extends Constant {
+ /** Index in constant pool of data this constant refers to. */
+ private final int index;
+
+ /**
+ * Creates a new index based constant.
+ *
+ * @param tag constant pool tag value
+ * @param index index in constant pool of data this constant will refer to
+ */
+ IndexedConstant(byte tag,
+ int index) {
+ super(tag);
+ this.index = index;
+ }
+
+ /**
+ * Returns the index in the constant pool of the data to which
+ * this constant refers.
+ *
+ * @return constant pool index for this constant's data
+ */
+ int index() {
+ return index;
+ }
+
+ @Override
+ void writeTo(DataOutputStream out)
+ throws IOException {
+ out.writeByte(tag());
+ out.writeShort(index);
+ }
+
+ /**
+ * Returns a diagnostic string form of this constant instance.
+ *
+ * @return string form of this object
+ */
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "[tag=" + tag()
+ + ", index=" + index + "]";
+ }
+}
diff --git a/src/main/org/apache/tools/ant/util/jarattr/JarAttributeUpdater.java b/src/main/org/apache/tools/ant/util/jarattr/JarAttributeUpdater.java
new file mode 100644
index 0000000000..a036c2dcf9
--- /dev/null
+++ b/src/main/org/apache/tools/ant/util/jarattr/JarAttributeUpdater.java
@@ -0,0 +1,242 @@
+/*
+ * 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
+ *
+ * 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.apache.tools.ant.util.jarattr;
+
+import java.io.InputStream;
+import java.io.BufferedOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import java.nio.file.Path;
+import java.nio.file.Files;
+
+import java.util.Objects;
+import java.util.jar.JarFile;
+
+/**
+ * Updates a .jar file's modular attributes.
+ */
+public class JarAttributeUpdater {
+
+ /**
+ * Extracts {@code module-info.class} from a .jar file.
+ *
+ * @param jar jar file to read
+ *
+ * @return representation of .jar file's {@code module-info.class}
+ *
+ * @throws IOException if file cannot be read, is not a valid .jar file,
+ * or lacks a {@code module-info.class}
+ */
+ private static ModuleInfo readModuleInfo(Path jar)
+ throws IOException {
+
+ try (JarFile jarFile = new JarFile(jar.toFile())) {
+ return ModuleInfo.readFrom(jarFile);
+ }
+ }
+
+ /**
+ * Updates the module version in {@code module-info.class} data.
+ *
+ * @param moduleInfo data to update
+ * @param version new module version
+ *
+ * @return {@code true} if module's version was missing or different
+ * and was modified; {@code false} if it was already the same
+ * as the {@code version} argument and thus no change was made
+ */
+ public boolean setVersion(ModuleInfo moduleInfo,
+ String version) {
+
+ String oldVersion = moduleInfo.getVersion();
+ if (Objects.equals(version, oldVersion)) {
+ return false;
+ }
+
+ moduleInfo.setVersion(version);
+ return true;
+ }
+
+ /**
+ * Updates the module version in a .jar file's {@code module-info.class}.
+ *
+ * @param jar .jar file to update
+ * @param version new module version
+ *
+ * @return {@code true} if module's version was missing or different
+ * and was modified; {@code false} if it was already the same
+ * as the {@code version} argument and thus no change was made
+ *
+ * @throws IOException if file could not be read or written, or file
+ * is not a valid .jar file, or file lacks a
+ * {@code module-info.class}
+ */
+ public boolean setVersion(Path jar,
+ String version)
+ throws IOException {
+
+ ModuleInfo moduleInfo = readModuleInfo(jar);
+
+ if (setVersion(moduleInfo, version)) {
+ moduleInfo.writeInto(jar);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Updates the module main class entry point in
+ * {@code module-info.class} data.
+ *
+ * @param moduleInfo data to update
+ * @param mainClass fully qualified name of class that will be the
+ * new module main class entry point
+ *
+ * @return {@code true} if module's main class was missing or different
+ * and was modified; {@code false} if it was already the same
+ * as the {@code mainClass} argument and thus no change was made
+ */
+ public boolean setMainClass(ModuleInfo moduleInfo,
+ String mainClass) {
+
+ String oldMain = moduleInfo.getMainClass();
+ if (Objects.equals(mainClass, oldMain)) {
+ return false;
+ }
+
+ moduleInfo.setMainClass(mainClass);
+ return true;
+ }
+
+ /**
+ * Updates the module main class entry point in a .jar file's
+ * {@code module-info.class}.
+ *
+ * @param jar .jar file to update
+ * @param mainClass fully qualified name of class that will be the
+ * new module main class entry point
+ *
+ * @return {@code true} if module's main class was missing or different
+ * and was modified; {@code false} if it was already the same
+ * as the {@code mainClass} argument and thus no change was made
+ *
+ * @throws IOException if file could not be read or written, or file
+ * is not a valid .jar file, or file lacks a
+ * {@code module-info.class}
+ */
+ public boolean setMainClass(Path jar,
+ String mainClass)
+ throws IOException {
+
+ ModuleInfo moduleInfo = readModuleInfo(jar);
+
+ if (setMainClass(moduleInfo, mainClass)) {
+ moduleInfo.writeInto(jar);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Updates the module version and main class entry point in a .jar file's
+ * {@code module-info.class}.
+ *
+ * @param jar .jar file to update
+ * @param version new module version
+ * @param mainClass fully qualified name of class that will be the
+ * new module main class entry point
+ *
+ * @return {@code true} if module's version and/or main class were
+ * modified; {@code false} if they were already the same
+ * as the arguments and thus no change was made
+ *
+ * @throws IOException if file could not be read or written, or file
+ * is not a valid .jar file, or file lacks a
+ * {@code module-info.class}
+ */
+ public boolean updateJar(Path jar,
+ String version,
+ String mainClass)
+ throws IOException {
+
+ ModuleInfo moduleInfo = readModuleInfo(jar);
+
+ boolean versionChanged =
+ version != null && setVersion(moduleInfo, version);
+ boolean mainClassChanged =
+ mainClass != null && setMainClass(moduleInfo, mainClass);
+
+ if (versionChanged || mainClassChanged) {
+ moduleInfo.writeInto(jar);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Carries out the entire module-info update operation.
+ *
+ * This is meant to be called reflectively from the <jar> task,
+ * in order to avoid a compile-time dependency on Java 9+ code.
+ * Therefore, if this method's signature is changed, make sure the
+ * reflective call in the taskdefs.Jar class is updated to match it.
+ *
+ * @param originalModuleInfo source whose version and/or main class
+` * attributes will be modified
+ * @param versionStr new module version, or {@code null}
+ * @param mainClass fully qualfied name new main class of module,
+ * or {@code null}
+ * @param newModuleInfoLocation file to which new module-info
+ * will be written
+ *
+ * @throws IOException if original module-info cannot be read, or
+ * new module-info file cannot be written
+ * @throws RuntimeException if {@code originalModuleInfo} or
+ * {@code newModuleInfoLocation} is {@code null}
+ */
+ public static void writeNewModuleInfo(InputStream originalModuleInfo,
+ String versionStr,
+ String mainClass,
+ Path newModuleInfoLocation)
+ throws IOException {
+
+ ModuleInfo moduleInfo;
+ try (DataInputStream original =
+ new DataInputStream(originalModuleInfo)) {
+
+ moduleInfo = ModuleInfo.readFrom(original);
+ }
+
+ JarAttributeUpdater updater = new JarAttributeUpdater();
+ updater.setVersion(moduleInfo, versionStr);
+ updater.setMainClass(moduleInfo, mainClass);
+
+ try (DataOutputStream infoStream = new DataOutputStream(
+ new BufferedOutputStream(
+ Files.newOutputStream(newModuleInfoLocation)))) {
+
+ moduleInfo.writeTo(infoStream);
+ }
+ }
+}
diff --git a/src/main/org/apache/tools/ant/util/jarattr/MainClassAttribute.java b/src/main/org/apache/tools/ant/util/jarattr/MainClassAttribute.java
new file mode 100644
index 0000000000..2d38c5b97c
--- /dev/null
+++ b/src/main/org/apache/tools/ant/util/jarattr/MainClassAttribute.java
@@ -0,0 +1,107 @@
+/*
+ * 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
+ *
+ * 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.apache.tools.ant.util.jarattr;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+/**
+ * Class file attribute which describes a module's main class
+ * entry point.
+ */
+class MainClassAttribute
+extends Attribute {
+ /** Official name of this attribute, as per JVM specification. */
+ static final String NAME = "ModuleMainClass";
+
+ /** Bytes in attribute, not counting tag. */
+ private static final int SIZE = 2;
+
+ /**
+ * Index in constant pool of a {@code CONSTANT_Class} entry
+ * representing the main class of the module.
+ */
+ private int mainClassIndex;
+
+ /**
+ * Creates a new attribute instance.
+ *
+ * @param nameIndex index in constant pool of this attribute's name
+ * (a {@code CONSTANT_Utf8} which must contain the
+ * value of {@link #NAME}).
+ * @param mainClassNameIndex index in constant pool of the binary name
+ * of the main class (a {@code CONSTANT_Class} entry)
+ */
+ MainClassAttribute(int nameIndex,
+ int mainClassNameIndex) {
+ super(nameIndex, SIZE);
+ this.mainClassIndex = mainClassNameIndex;
+ }
+
+ /**
+ * Creates a new attribute instance with an explicitly specified length
+ * (which should be unnecessary, since the length must always be 2).
+ *
+ * @param nameIndex index in constant pool of this attribute's name
+ * (a {@code CONSTANT_Utf8} which must contain the
+ * value of {@link #NAME}).
+ * @param length length of attribute data (should always be 2)
+ * @param mainClassNameIndex index in constant pool of the binary name
+ * of the main class (a {@code CONSTANT_Class} entry)
+ */
+ MainClassAttribute(int nameIndex,
+ int length,
+ int mainClassNameIndex) {
+ super(nameIndex, length);
+ this.mainClassIndex = mainClassNameIndex;
+
+ if (length != SIZE) {
+ throw new IllegalArgumentException(
+ "Main class attribute must have a length 2.");
+ }
+ }
+
+ /**
+ * Returns index in constant pool of a {@code CONSTANT_Class} entry
+ * representing the main class of the containing module.
+ *
+ * @return index of class constant in constant pool, or zero if not set
+ */
+ int getClassIndex() {
+ return mainClassIndex;
+ }
+
+ /**
+ * Sets index in constant pool of a {@code CONSTANT_Class} entry
+ * representing the main class of the containing module.
+ *
+ * @param index index of class constant in constant pool
+ */
+ void setClassIndex(int index) {
+ this.mainClassIndex = index;
+ }
+
+ @Override
+ void writeTo(DataOutputStream out)
+ throws IOException {
+ out.writeShort(attributeNameIndex());
+ out.writeInt(attributeLength());
+ out.writeShort(mainClassIndex);
+ }
+}
diff --git a/src/main/org/apache/tools/ant/util/jarattr/ModuleAttribute.java b/src/main/org/apache/tools/ant/util/jarattr/ModuleAttribute.java
new file mode 100644
index 0000000000..4c53c068c0
--- /dev/null
+++ b/src/main/org/apache/tools/ant/util/jarattr/ModuleAttribute.java
@@ -0,0 +1,318 @@
+/*
+ * 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
+ *
+ * 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.apache.tools.ant.util.jarattr;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import java.util.Objects;
+
+/**
+ * Class file attribute which describes a module descriptor.
+ */
+class ModuleAttribute
+extends Attribute {
+ /** Official name of this attribute, as per JVM specification. */
+ static final String NAME = "Module";
+
+ /** Describes a required module. */
+ static class Requires {
+ /** Index in constant pool of {@code CONSTANT_Module}. */
+ private int requiresIndex; // 2 bytes
+
+ /** Dependency modifiers (transitive, static, etc.) */
+ private int requiresFlags; // 2 bytes
+
+ /**
+ * Index in constant pool of {@code CONSTANT_Utf8} containing
+ * required module's version at compile time.
+ */
+ private int requiresVersionIndex; // 2 bytes
+ }
+
+ /** Describes an exported package in a module. */
+ static class Exports {
+ /** Index in constant pool of {@code CONSTANT_Package}. */
+ private int exportsIndex;
+
+ /** Export modifiers. */
+ private int exportsFlags;
+
+ /**
+ * Array of 16-bit constant pool indexes for {@code CONSTANT_Module}s
+ * which are explicit targets of export.
+ */
+ private byte[] exportsToIndex; // 2 bytes each
+ }
+
+ /** Describes an opened package in a module. */
+ static class Opens {
+ /** Index in constant pool of {@code CONSTANT_Package}. */
+ private int opensIndex;
+
+ /** Open modifiers. */
+ private int opensFlags;
+
+ /**
+ * Array of 16-bit constant pool indexes for {@code CONSTANT_Module}s
+ * which are explicit targets of open.
+ */
+ private byte[] opensToIndex; // 2 bytes each
+ }
+
+ /**
+ * Describes an implementation of a service provider interface
+ * contained in a module.
+ */
+ static class Provides {
+ /**
+ * Index in constant pool of {@code CONSTANT_Class} representing
+ * service type.
+ */
+ private int providesIndex;
+
+ /**
+ * Array of 16-bit constant pool indexes for {@code CONSTANT_Class}es
+ * indicating implementations of service type.
+ */
+ private byte[] providesWithIndex; // 2 bytes each
+ }
+
+ /**
+ * Index in constant pool of {@code CONSTANT_Module} containing module's
+ * identifying information.
+ */
+ private final int moduleNameIndex;
+
+ /** Module declaration modifiers. */
+ private final int moduleFlags;
+
+ /**
+ * Index in constant pool of {@code CONSTANT_Utf8} containing module's
+ * version, or zero if there is no version.
+ */
+ private int moduleVersionIndex;
+
+ /** List of all required modules. */
+ private final Requires[] requires;
+
+ /** List of all exported packages. */
+ private final Exports[] exports;
+
+ /** List of all opened packages. */
+ private final Opens[] opens;
+
+ /**
+ * Array of 16-bit constant pool indexes for {@code CONSTANT_Class}es
+ * which are SPI classes used by module.
+ */
+ private final byte[] uses; // 2 bytes each
+
+ /** List of all implementations of service provider interfaces. */
+ private final Provides[] provides;
+
+ /**
+ * Creates an instance with the specified module information.
+ *
+ * @param nameIndex index in constant pool of attribute's name
+ * @param length size in bytes of attribute data
+ * @param moduleNameIndex index in constant pool of module's name
+ * @param flags module modifiers
+ * @param versionIndex index in constant pool of module's version, or zero
+ * @param requires list of required modules
+ * @param exports list of exported packages
+ * @param opens list of opened packages
+ * @param uses list of 16-bit constant pool indices of
+ * service provider interfaces needed by module
+ * @param provides list of concrete implements of
+ * service provider interfaces in module
+ */
+ ModuleAttribute(int nameIndex,
+ int length,
+ int moduleNameIndex,
+ int flags,
+ int versionIndex,
+ Requires[] requires,
+ Exports[] exports,
+ Opens[] opens,
+ byte[] uses,
+ Provides[] provides) {
+
+ super(nameIndex, length);
+
+ this.moduleNameIndex = moduleNameIndex;
+ this.moduleFlags = flags;
+ this.moduleVersionIndex = versionIndex;
+
+ this.requires = Objects.requireNonNull(requires,
+ "Requires list cannot be null");
+ this.exports = Objects.requireNonNull(exports,
+ "Exports list cannot be null");
+ this.opens = Objects.requireNonNull(opens,
+ "Opens list cannot be null");
+ this.provides = Objects.requireNonNull(provides,
+ "Provides list cannot be null");
+ this.uses = Objects.requireNonNull(uses,
+ "Uses cannot be null");
+ }
+
+ /**
+ * Returns the index in the constant pool of the {@code CONSTANT_Utf8}
+ * which holds the module's version string, or zero if there is no version.
+ *
+ * @return index in constant pool of version string, or zero
+ */
+ int getModuleVersionIndex() {
+ return moduleVersionIndex;
+ }
+
+ /**
+ * Sets the index in the constant pool of the {@code CONSTANT_Utf8}
+ * which holds the module's version string.
+ *
+ * @param index index in constant pool of Utf8 constant containing version
+ * string, or zero to signify there is no version defined
+ */
+ void setModuleVersionIndex(int index) {
+ this.moduleVersionIndex = index;
+ }
+
+ /**
+ * Reads a module attribute from an {@code attribute_info} block
+ * in a {@code module-info.class}.
+ *
+ * @param in {@code module-info.class} stream, pointing to attribute_info
+ * block containing module attribute data
+ * @param attributeNameIndex already-read constant pool index of attribute
+ * name (which should always be the value of {@link #NAME})
+ * @param attributeLength size of attribute data in bytes
+ *
+ * @return new {@code ModuleAttribute} object representing attribute_info
+ * data
+ *
+ * @throws IOException if stream cannot be read, or contains invalid data
+ */
+ static ModuleAttribute readFrom(DataInputStream in,
+ int attributeNameIndex,
+ int attributeLength)
+ throws IOException {
+ int moduleNameIndex = in.readUnsignedShort();
+ int moduleFlags = in.readUnsignedShort();
+ int moduleVersionIndex = in.readUnsignedShort();
+
+ int requiresCount = in.readUnsignedShort();
+ Requires[] requires = new Requires[requiresCount];
+ for (int i = 0; i < requiresCount; i++) {
+ requires[i] = new Requires();
+ requires[i].requiresIndex = in.readUnsignedShort();
+ requires[i].requiresFlags = in.readShort();
+ requires[i].requiresVersionIndex = in.readUnsignedShort();
+ }
+
+ int exportsCount = in.readUnsignedShort();
+ Exports[] exports = new Exports[exportsCount];
+ for (int i = 0; i < exportsCount; i++) {
+ exports[i] = new Exports();
+ exports[i].exportsIndex = in.readUnsignedShort();
+ exports[i].exportsFlags = in.readShort();
+ int exportsToCount = in.readUnsignedShort();
+ exports[i].exportsToIndex = readNBytes(in, exportsToCount * 2);
+ }
+
+ int opensCount = in.readUnsignedShort();
+ Opens[] opens = new Opens[opensCount];
+ for (int i = 0; i < opensCount; i++) {
+ opens[i] = new Opens();
+ opens[i].opensIndex = in.readUnsignedShort();
+ opens[i].opensFlags = in.readShort();
+ int opensToCount = in.readUnsignedShort();
+ opens[i].opensToIndex = readNBytes(in, opensToCount * 2);
+ }
+
+ int usesCount = in.readUnsignedShort();
+ byte[] uses = readNBytes(in, usesCount * 2);
+
+ int providesCount = in.readUnsignedShort();
+ Provides[] provides = new Provides[providesCount];
+ for (int i = 0; i < providesCount; i++) {
+ provides[i] = new Provides();
+ provides[i].providesIndex = in.readUnsignedShort();
+ int providesWithCount = in.readUnsignedShort();
+ provides[i].providesWithIndex =
+ readNBytes(in, providesWithCount * 2);
+ }
+
+ return new ModuleAttribute(
+ attributeNameIndex,
+ attributeLength,
+ moduleNameIndex,
+ moduleFlags,
+ moduleVersionIndex,
+ requires,
+ exports,
+ opens,
+ uses,
+ provides);
+ }
+
+ @Override
+ void writeTo(DataOutputStream out)
+ throws IOException {
+ out.writeShort(attributeNameIndex());
+ out.writeInt(attributeLength());
+
+ out.writeShort(moduleNameIndex);
+ out.writeShort(moduleFlags);
+ out.writeShort(moduleVersionIndex);
+
+ out.writeShort(requires.length);
+ for (Requires r : requires) {
+ out.writeShort(r.requiresIndex);
+ out.writeShort(r.requiresFlags);
+ out.writeShort(r.requiresVersionIndex);
+ }
+
+ out.writeShort(exports.length);
+ for (Exports e : exports) {
+ out.writeShort(e.exportsIndex);
+ out.writeShort(e.exportsFlags);
+ out.writeShort(e.exportsToIndex.length / 2);
+ out.write(e.exportsToIndex);
+ }
+
+ out.writeShort(opens.length);
+ for (Opens o : opens) {
+ out.writeShort(o.opensIndex);
+ out.writeShort(o.opensFlags);
+ out.writeShort(o.opensToIndex.length / 2);
+ out.write(o.opensToIndex);
+ }
+
+ out.writeShort(uses.length / 2);
+ out.write(uses);
+
+ out.writeShort(provides.length);
+ for (Provides p : provides) {
+ out.writeShort(p.providesIndex);
+ out.writeShort(p.providesWithIndex.length / 2);
+ out.write(p.providesWithIndex);
+ }
+ }
+}
diff --git a/src/main/org/apache/tools/ant/util/jarattr/ModuleInfo.java b/src/main/org/apache/tools/ant/util/jarattr/ModuleInfo.java
new file mode 100644
index 0000000000..6938637858
--- /dev/null
+++ b/src/main/org/apache/tools/ant/util/jarattr/ModuleInfo.java
@@ -0,0 +1,509 @@
+/*
+ * 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
+ *
+ * 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.apache.tools.ant.util.jarattr;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+
+import java.util.List;
+import java.util.ArrayList;
+
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarInputStream;
+import java.util.jar.JarOutputStream;
+
+/**
+ * Represents, reads, and writes all data in a {@code module-info.class}.
+ * Reading and writing of {@code module-info.class} is based on
+ * the
+ * Java Virtual Machine Specification for Java 11.
+ *
+ * As per the JVM specification, a constant pool index is always 1-based:
+ * index 1 is the first constant, 2 is the second, etc.
+ */
+public final class ModuleInfo {
+ /** Magic number signature bytes. */
+ private int magic;
+ /** Major version and minor version of class file. */
+ private int version;
+
+ /** Constants defined in class file. */
+ private List constantPool = List.of();
+
+ /** Class modifiers. */
+ private short accessFlags;
+
+ /**
+ * Index in constant pool of {@code CONSTANT_class} for this file's class.
+ */
+ private int thisClass; // 2-byte constant pool index
+
+ /**
+ * Index in constant pool of {@code CONSTANT_class} for the superclass
+ * of this file's class.
+ */
+ private int superClass; // 2-byte constant pool index
+
+ /**
+ * Class attributes, which in the case of module-info is module attributes.
+ */
+ private List attributes = List.of();
+
+ /**
+ * Creates instance with no fields initialized.
+ */
+ private ModuleInfo() {
+ // Deliberately empty.
+ }
+
+ /**
+ * Returns a string suitable for debugging, which includes module
+ * version and main class.
+ *
+ * @return debugging string
+ */
+ @Override
+ public String toString() {
+ return getClass().getName() + "["
+ + "version=" + getVersion()
+ + ", main class=" + getMainClass()
+ + "]";
+ }
+
+ /**
+ * Returns the String value of the {@code CONSTANT_Utf8} at the given
+ * index in the constant pool, or {@code null} if the index in zero.
+ *
+ * @param index 1-based index in constant pool
+ *
+ * @return string constant at given index, or {@code null}
+ */
+ private String getUTF8Constant(int index) {
+ if (index > 0) {
+ Constant c = constantPool.get(index - 1);
+ return ((UTF8Constant) c).value();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the String name of the {@code CONSTANT_Class} at the given
+ * index in the constant pool, or {@code null} if the index in zero.
+ *
+ * @param index 1-based index in constant pool
+ *
+ * @return name of class constant at given index, or {@code null}
+ */
+ private String getClassConstant(int index) {
+ if (index > 0) {
+ Constant c = constantPool.get(index - 1);
+ int classNameIndex = ((IndexedConstant) c).index();
+ return getUTF8Constant(classNameIndex);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the currently defined version in this module-info.
+ *
+ * @return current version as a string, or {@code null} if no version
+ * is defined
+ *
+ * @see #setVersion(String)
+ */
+ public String getVersion() {
+ for (Attribute a : attributes) {
+ if (a instanceof ModuleAttribute) {
+ ModuleAttribute m = (ModuleAttribute) a;
+ return getUTF8Constant(m.getModuleVersionIndex());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Updates the version defined in this module-info.
+ *
+ * @param version new version to encode into module-info
+ *
+ * @see #getVersion()
+ */
+ public void setVersion(String version) {
+ for (Attribute a : attributes) {
+ if (a instanceof ModuleAttribute) {
+ ModuleAttribute m = (ModuleAttribute) a;
+ if (version != null) {
+ m.setModuleVersionIndex(getOrAddUTF8Constant(version));
+ } else {
+ m.setModuleVersionIndex(0);
+ }
+ return;
+ }
+ }
+ throw new IllegalStateException(
+ "Class file has no '" + ModuleAttribute.NAME + "' attribute");
+ }
+
+ /**
+ * Returns this module-info's currently defined main class entry point.
+ *
+ * @return name of main class, or {@code null} if none is defined
+ *
+ * @see #setMainClass(String)
+ */
+ public String getMainClass() {
+ for (Attribute a : attributes) {
+ if (a instanceof MainClassAttribute) {
+ MainClassAttribute m = (MainClassAttribute) a;
+ return getClassConstant(m.getClassIndex());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Updates this module-info's main class entry point. The class name
+ * must be a class which exists in the module, or the Java runtime will
+ * refuse to load the module.
+ *
+ * @param className new main class entry point
+ *
+ * @see #getMainClass()
+ */
+ public void setMainClass(String className) {
+ if (className == null) {
+ attributes.removeIf(MainClassAttribute.class::isInstance);
+ return;
+ }
+
+ // Internally, class names use slashes in place of periods.
+ // See section 4.2.1 "Binary Class and Interface Names":
+ // https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.2.1
+ className = className.replace('.', '/');
+
+ int mainClassNameIndex = getOrAddClassConstant(className);
+
+ for (Attribute a : attributes) {
+ if (a instanceof MainClassAttribute) {
+ MainClassAttribute main = (MainClassAttribute) a;
+ main.setClassIndex(mainClassNameIndex);
+ return;
+ }
+ }
+
+ int nameIndex = getOrAddUTF8Constant(MainClassAttribute.NAME);
+ attributes.add(new MainClassAttribute(nameIndex, mainClassNameIndex));
+ }
+
+ /**
+ * Locates the {@code CONSTANT_Utf8} in this module-info's constant pool
+ * with the given string value. If none exists, it is created and added
+ * to the constant pool.
+ *
+ * @param value constant value to check for or add
+ *
+ * @return index in constant pool of specified value
+ */
+ private int getOrAddUTF8Constant(String value) {
+ int index = 0;
+ int constantCount = constantPool.size();
+ for (int i = 0; i < constantCount; i++) {
+ Constant c = constantPool.get(i);
+ if (c instanceof UTF8Constant
+ && value.equals(((UTF8Constant) c).value())) {
+
+ index = i + 1;
+ break;
+ }
+ }
+
+ if (index == 0) {
+ constantPool.add(new UTF8Constant(value));
+ index = constantPool.size();
+ }
+
+ return index;
+ }
+
+ /**
+ * Locates the {@code CONSTANT_Class} in this module-info's constant pool
+ * which has a class name matching the argument. Note that binary class
+ * names use "{@code /}" as a separator rather than "{@code .}".
+ * If no match class constant exists, it is created and added
+ * to the constant pool.
+ *
+ * @param value constant value to check for or add
+ *
+ * @return index in constant pool of specified value
+ */
+ private int getOrAddClassConstant(String className) {
+ int classNameIndex = getOrAddUTF8Constant(className);
+
+ int index = 0;
+ int constantCount = constantPool.size();
+ for (int i = 0; i < constantCount; i++) {
+ Constant c = constantPool.get(i);
+ if (c instanceof IndexedConstant) {
+ IndexedConstant ic = (IndexedConstant) c;
+ if (ic.tag() == Constant.CLASS
+ && ic.index() == classNameIndex) {
+
+ index = i + 1;
+ break;
+ }
+ }
+ }
+
+ if (index == 0) {
+ constantPool.add(
+ new IndexedConstant(Constant.CLASS, classNameIndex));
+ index = constantPool.size();
+ }
+
+ return index;
+ }
+
+ /**
+ * Reads from a streamed .jar file's {@code module-info.class}.
+ *
+ * @param source .jar file containing module-info.class to read
+ *
+ * @return object representing .jar file's {@code module-info.class} data
+ *
+ * @throws IOException if stream cannot be read or does not contain
+ * {@code module-info.class}
+ */
+ public static ModuleInfo readFrom(JarInputStream source)
+ throws IOException {
+ JarEntry entry;
+ while ((entry = source.getNextJarEntry()) != null) {
+ if ("module-info.class".equals(entry.getName())) {
+ return readFrom(new DataInputStream(source));
+ }
+ source.closeEntry();
+ }
+
+ throw new IOException("No module-info.class found");
+ }
+
+ /**
+ * Reads from a jar file's {@code module-info.class}, taking any
+ * multi-release configuration into account.
+ *
+ * @param source .jar file containing module-info.class to read
+ *
+ * @return object representing .jar file's {@code module-info.class} data
+ *
+ * @throws IOException if .jar file cannot be read or does not contain
+ * {@code module-info.class}
+ */
+ public static ModuleInfo readFrom(JarFile source)
+ throws IOException {
+
+ JarEntry moduleInfoClass = source.getJarEntry("module-info.class");
+
+ if (moduleInfoClass == null) {
+ throw new IOException("No module-info.class found");
+ }
+
+ try (DataInputStream stream = new DataInputStream(
+ new BufferedInputStream(
+ source.getInputStream(moduleInfoClass)))) {
+
+ return readFrom(stream);
+ }
+ }
+
+ /**
+ * Reads from {@code module-info.class}.
+ *
+ * @param source content of {@code module-info.class}
+ *
+ * @return object representing .jar file's {@code module-info.class} data
+ *
+ * @throws IOException if .jar file cannot be read or does not contain
+ * {@code module-info.class}
+ */
+ public static ModuleInfo readFrom(DataInputStream source)
+ throws IOException {
+ ModuleInfo info = new ModuleInfo();
+
+ info.magic = source.readInt();
+ info.version = source.readInt(); // major_version, minor_version
+
+ int constantPoolCount = source.readUnsignedShort();
+ info.constantPool = new ArrayList<>(constantPoolCount);
+ for (int i = 1; i < constantPoolCount; i++) {
+ info.constantPool.add(Constant.readFrom(source));
+ }
+
+ info.accessFlags = source.readShort();
+ info.thisClass = source.readUnsignedShort();
+ info.superClass = source.readUnsignedShort();
+
+ int interfacesCount = source.readUnsignedShort();
+ if (interfacesCount != 0) {
+ throw new ClassFormatException(
+ "interfaces_count in class file should be zero"
+ + ", but is " + interfacesCount);
+ }
+
+ int fieldsCount = source.readUnsignedShort();
+ if (fieldsCount != 0) {
+ throw new ClassFormatException(
+ "fields_count in class file should be zero"
+ + ", but is " + fieldsCount);
+ }
+
+ int methodsCount = source.readUnsignedShort();
+ if (methodsCount != 0) {
+ throw new ClassFormatException(
+ "methods_count in class file should be zero"
+ + ", but is " + methodsCount);
+ }
+
+ int attributesCount = source.readUnsignedShort();
+ if (attributesCount < 1) {
+ throw new ClassFormatException(
+ "attributes_count in class file should be positive"
+ + ", but is " + attributesCount);
+ }
+
+ info.attributes = new ArrayList<>(attributesCount);
+ for (int i = 0; i < attributesCount; i++) {
+ info.attributes.add(Attribute.readFrom(source, info.constantPool));
+ }
+
+ return info;
+ }
+
+ /**
+ * Writes a new {@code module-info.class}.
+ *
+ * @param dest destination to write {@code module-info.class} to
+ *
+ * @throws IOException if stream cannot be written
+ */
+ public void writeTo(DataOutputStream dest)
+ throws IOException {
+ dest.writeInt(magic);
+ dest.writeInt(version);
+
+ dest.writeShort(constantPool.size() + 1);
+ for (Constant c : constantPool) {
+ c.writeTo(dest);
+ }
+
+ dest.writeShort(accessFlags);
+ dest.writeShort(thisClass);
+ dest.writeShort(superClass);
+
+ dest.writeShort(0); // interfaces_count
+ dest.writeShort(0); // fields_count
+ dest.writeShort(0); // methods_count
+
+ dest.writeShort(attributes.size());
+ for (Attribute a : attributes) {
+ a.writeTo(dest);
+ }
+ }
+
+ /**
+ * Writes a new {@code module-info.class} in the specified .jar file.
+ *
+ * @param jar .jar file to update
+ *
+ * @throws IOException if .jar file cannot be read, or new file cannot be
+ * written
+ */
+ public void writeInto(Path jar)
+ throws IOException {
+ Path newJar = Files.createTempFile(null, ".jar");
+
+ try (JarInputStream in = new JarInputStream(
+ new BufferedInputStream(
+ Files.newInputStream(jar)));
+ JarOutputStream out = new JarOutputStream(
+ new BufferedOutputStream(
+ Files.newOutputStream(newJar)))) {
+
+ JarEntry entry;
+ while ((entry = in.getNextJarEntry()) != null) {
+ JarEntry newEntry = (JarEntry) entry.clone();
+
+ out.putNextEntry(newEntry);
+
+ String name = entry.getName();
+ if (name.equals("module-info.class")
+ || name.endsWith("/module-info.class")) {
+
+ DataOutputStream d = new DataOutputStream(out);
+ writeTo(d);
+ d.flush();
+ } else {
+ in.transferTo(out);
+ }
+
+ out.closeEntry();
+ in.closeEntry();
+ }
+ }
+
+ Files.move(newJar, jar, StandardCopyOption.REPLACE_EXISTING);
+ }
+
+ /**
+ * Tests this class by treating each argument as a file name,
+ * displaying the module-info for each such file, along with its
+ * class file attributes.
+ *
+ * @param args command line arguments
+ *
+ * @throws IOException if an file cannot be read
+ */
+ public static void main(String[] args)
+ throws IOException {
+ for (String arg : args) {
+ try (JarInputStream stream = new JarInputStream(
+ new BufferedInputStream(
+ Files.newInputStream(
+ Paths.get(arg))))) {
+ ModuleInfo info = readFrom(stream);
+ System.out.println(arg + ": " + info);
+
+ System.out.println("Attributes:");
+ for (Attribute a : info.attributes) {
+ System.out.println(" "
+ + info.getUTF8Constant(a.attributeNameIndex()));
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/org/apache/tools/ant/util/jarattr/OtherAttribute.java b/src/main/org/apache/tools/ant/util/jarattr/OtherAttribute.java
new file mode 100644
index 0000000000..0f0852bc10
--- /dev/null
+++ b/src/main/org/apache/tools/ant/util/jarattr/OtherAttribute.java
@@ -0,0 +1,69 @@
+/*
+ * 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
+ *
+ * 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.apache.tools.ant.util.jarattr;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import java.util.Objects;
+
+/**
+ * A class file attribute whose specific data is unimportant to this
+ * package, because it does not relate to module information. It will be
+ * preserved as is, and written back byte-for-byte when updating
+ * {@code module-info.class}.
+ */
+class OtherAttribute
+extends Attribute {
+ /** Attribute data. */
+ private final byte[] info;
+
+ /**
+ * Creates a new attribute instance.
+ *
+ * @param nameIndex index in constant pool of {@code Constant_Utf8}
+ * containing attribute's name
+ * @param length size in bytes of attribute data
+ * @param info raw attribute data, must have size equal to {@code length}
+ *
+ * @throws IllegalArgumentException if {@code info} has a length different
+ * from {@code length}
+ */
+ OtherAttribute(int nameIndex,
+ int length,
+ byte[] info) {
+ super(nameIndex, length);
+
+ this.info = Objects.requireNonNull(info, "info cannot be null");
+
+ if (info.length != length) {
+ throw new IllegalArgumentException(
+ "length (" + length + ") must be the same"
+ + " as byte array's length (" + info.length + ")");
+ }
+ }
+
+ @Override
+ void writeTo(DataOutputStream out)
+ throws IOException {
+ out.writeShort(attributeNameIndex());
+ out.writeInt(attributeLength());
+ out.write(info);
+ }
+}
diff --git a/src/main/org/apache/tools/ant/util/jarattr/OtherConstant.java b/src/main/org/apache/tools/ant/util/jarattr/OtherConstant.java
new file mode 100644
index 0000000000..17ade6ab6b
--- /dev/null
+++ b/src/main/org/apache/tools/ant/util/jarattr/OtherConstant.java
@@ -0,0 +1,64 @@
+/*
+ * 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
+ *
+ * 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.apache.tools.ant.util.jarattr;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+/**
+ * A constant pool entry of a type not relevant to the
+ * {@code module-info.class} manipulation done by this package.
+ * Constant data is stored as a raw byte array.
+ */
+class OtherConstant
+extends Constant {
+ /** Constant's raw data. */
+ private final byte[] info;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param tag {@code cp_info} tag value
+ * @param info raw data of constant
+ */
+ OtherConstant(byte tag,
+ byte[] info) {
+ super(tag);
+ this.info = info;
+ }
+
+ @Override
+ void writeTo(DataOutputStream out)
+ throws IOException {
+ out.writeByte(tag());
+ out.write(info);
+ }
+
+ /**
+ * Returns a diagnostic string containing this constant entry's
+ * tag and data size.
+ *
+ * @return string form of this object, suitable for debugging
+ */
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "[tag=" + tag()
+ + ", " + info.length + " bytes]";
+ }
+}
diff --git a/src/main/org/apache/tools/ant/util/jarattr/UTF8Constant.java b/src/main/org/apache/tools/ant/util/jarattr/UTF8Constant.java
new file mode 100644
index 0000000000..d9f1500b6d
--- /dev/null
+++ b/src/main/org/apache/tools/ant/util/jarattr/UTF8Constant.java
@@ -0,0 +1,84 @@
+/*
+ * 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
+ *
+ * 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.apache.tools.ant.util.jarattr;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+/**
+ * A {@code CONSTANT_Utf8} constant pool entry containing text.
+ */
+class UTF8Constant
+extends Constant {
+ /** {@code cp_info} tag value for all constants of this type. */
+ static final byte TAG = 1;
+
+ /** This constant's text value. */
+ private final String value;
+
+ /**
+ * Creates a new constant pool entry containing the specified text.
+ *
+ * @param value new constant's text value
+ */
+ UTF8Constant(String value) {
+ this(TAG, value);
+ }
+
+ /**
+ * Creates a new constant pool entry with an explicitly specified tag
+ * (though the tag should always be the value in {@link #TAG}).
+ *
+ * @param tag new constant's tag (should always be {@link #TAG})
+ * @param value new constant's text value
+ */
+ UTF8Constant(byte tag,
+ String value) {
+ super(tag);
+ this.value = value;
+ }
+
+ /**
+ * Returns the text of this constant.
+ *
+ * @return non-{@code null} string value of constant
+ */
+ String value() {
+ return value;
+ }
+
+ @Override
+ void writeTo(DataOutputStream out)
+ throws IOException {
+ out.writeByte(tag());
+ out.writeUTF(value);
+ }
+
+ /**
+ * Returns a diagnostic string containing this constant entry's
+ * tag and text data.
+ *
+ * @return string form of this object, suitable for debugging
+ */
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "[tag=" + tag()
+ + ", value=\"" + value + "\"]";
+ }
+}
diff --git a/src/main/org/apache/tools/ant/util/jarattr/package-info.java b/src/main/org/apache/tools/ant/util/jarattr/package-info.java
new file mode 100644
index 0000000000..623023501e
--- /dev/null
+++ b/src/main/org/apache/tools/ant/util/jarattr/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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
+ *
+ * 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.
+ *
+ */
+
+/**
+ * Updates attributes of a modular .jar file. These attributes cannot
+ * be altered via the {@code java.util.jar} package.
+ */
+package org.apache.tools.ant.util.jarattr;
diff --git a/src/tests/junit/org/apache/tools/ant/taskdefs/modules/JarModuleRelatedTests.java b/src/tests/junit/org/apache/tools/ant/taskdefs/modules/JarModuleRelatedTests.java
new file mode 100644
index 0000000000..c6e730a0a3
--- /dev/null
+++ b/src/tests/junit/org/apache/tools/ant/taskdefs/modules/JarModuleRelatedTests.java
@@ -0,0 +1,223 @@
+/*
+ * 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
+ *
+ * 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.apache.tools.ant.taskdefs.modules;
+
+import java.lang.module.ModuleDescriptor;
+import java.lang.module.ModuleFinder;
+import java.lang.module.ModuleReference;
+
+import java.nio.file.Paths;
+
+import java.util.Optional;
+
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.BuildFileRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import org.junit.Assert;
+
+/**
+ * Tests module-related capabilities of <jar> task.
+ */
+public class JarModuleRelatedTests {
+
+ @Rule
+ public final BuildFileRule buildRule = new BuildFileRule();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Before
+ public void setUp() {
+ buildRule.configureProject("src/etc/testcases/taskdefs/jar.xml");
+ buildRule.executeTarget("setUp");
+ }
+
+ private Optional getTestModule() {
+ String moduleName =
+ buildRule.getProject().getProperty("test-module.name");
+
+ String jarFileName = buildRule.getProject().getProperty("tmp.jar");
+ ModuleFinder finder = ModuleFinder.of(Paths.get(jarFileName));
+
+ return finder.find(moduleName);
+ }
+
+ private ModuleDescriptor getTestModuleDescriptor() {
+ Optional module = getTestModule();
+ Assert.assertTrue("Verifying that test jar is a modular jar.",
+ module.isPresent());
+
+ return module.get().descriptor();
+ }
+
+ @Test
+ public void testModularJar() {
+ buildRule.executeTarget("testModularJar");
+
+ Assert.assertTrue(
+ "Verifying that specifying neither version nor main class "
+ + "allows unmodified module-info to be included in jar.",
+ getTestModule().isPresent());
+ }
+
+ @Test
+ public void testModuleVersion() {
+ buildRule.executeTarget("testModuleVersion");
+
+ Optional version = getTestModuleDescriptor().rawVersion();
+ Assert.assertTrue("Checking that modular jar has a module version.",
+ version.isPresent());
+
+ Assert.assertEquals(
+ "Checking that modular jar has correct module version.",
+ "1.0-test+0001", version.get());
+ }
+
+ @Test
+ public void testNestedVersion() {
+ buildRule.executeTarget("testNestedVersion");
+
+ Optional version = getTestModuleDescriptor().rawVersion();
+ Assert.assertTrue("Checking that modular jar has a module version.",
+ version.isPresent());
+
+ Assert.assertEquals(
+ "Checking that modular jar has correct module version.",
+ "1.0-test+0001", version.get());
+ }
+
+ @Test
+ public void testNestedVersionVersionNumberOnly() {
+ buildRule.executeTarget("testNestedVersionNumberOnly");
+
+ Optional version = getTestModuleDescriptor().rawVersion();
+ Assert.assertTrue("Checking that modular jar has a module version.",
+ version.isPresent());
+
+ Assert.assertEquals(
+ "Checking that modular jar has correct module version.",
+ "1.0", version.get());
+ }
+
+ @Test
+ public void testNestedVersionNumberAndPreReleaseOnly() {
+ buildRule.executeTarget("testNestedVersionNumberAndPreReleaseOnly");
+
+ Optional version = getTestModuleDescriptor().rawVersion();
+ Assert.assertTrue("Checking that modular jar has a module version.",
+ version.isPresent());
+
+ Assert.assertEquals(
+ "Checking that modular jar has correct module version.",
+ "1.0-test", version.get());
+ }
+
+ @Test
+ public void testNestedVersionNumberAndBuildOnly() {
+ buildRule.executeTarget("testNestedVersionNumberAndBuildOnly");
+
+ Optional version = getTestModuleDescriptor().rawVersion();
+ Assert.assertTrue("Checking that modular jar has a module version.",
+ version.isPresent());
+
+ Assert.assertEquals(
+ "Checking that modular jar has correct module version.",
+ "1.0-+0001", version.get());
+ }
+
+ @Test
+ public void testNestedVersionMissingNumber() {
+ thrown.expect(BuildException.class);
+ thrown.expectMessage("number");
+
+ buildRule.executeTarget("testNestedVersionMissingNumber");
+
+ }
+
+ @Test
+ public void testNestedVersionInvalidNumber() {
+ thrown.expect(BuildException.class);
+ thrown.expectMessage("contain");
+
+ buildRule.executeTarget("testNestedVersionInvalidNumber");
+ }
+
+ @Test
+ public void testNestedVersionInvalidPreRelease() {
+ thrown.expect(BuildException.class);
+ thrown.expectMessage("contain");
+
+ buildRule.executeTarget("testNestedVersionInvalidPreRelease");
+ }
+
+ @Test
+ public void testNestedVersionAndAttribute() {
+ thrown.expect(BuildException.class);
+ thrown.expectMessage("both");
+
+ buildRule.executeTarget("testNestedVersionAndAttribute");
+ }
+
+ @Test
+ public void testMainClass() {
+ buildRule.executeTarget("testMainClass");
+
+ String packageName =
+ buildRule.getProject().getProperty("package.name");
+
+ Optional mainClass = getTestModuleDescriptor().mainClass();
+ Assert.assertTrue("Checking that modular jar has a main class.",
+ mainClass.isPresent());
+
+ Assert.assertEquals(
+ "Checking that modular jar has correct main class.",
+ packageName + ".Test", mainClass.get());
+ }
+
+ @Test
+ public void testModuleVersionAndMainClass() {
+ buildRule.executeTarget("testModuleVersionAndMainClass");
+
+ String packageName =
+ buildRule.getProject().getProperty("package.name");
+
+ ModuleDescriptor descriptor = getTestModuleDescriptor();
+
+ Optional version = getTestModuleDescriptor().rawVersion();
+ Assert.assertTrue("Checking that modular jar has a module version.",
+ version.isPresent());
+
+ Optional mainClass = descriptor.mainClass();
+ Assert.assertTrue("Checking that modular jar has a main class.",
+ mainClass.isPresent());
+
+ Assert.assertEquals(
+ "Checking that modular jar has correct module version.",
+ "1.0-test+0001", version.get());
+
+ Assert.assertEquals(
+ "Checking that modular jar has correct main class.",
+ packageName + ".Test", mainClass.get());
+ }
+}