diff --git a/WHATSNEW b/WHATSNEW index c5d7f58619..5a272b5c97 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -56,6 +56,9 @@ Fixed bugs: actual address could potentially be unreachable. This is now fixed and the resolved address is actually checked for reachability. + * Added version and mainClass attributes for modular jar files. + Bugzilla Report 62772 + Bugzilla Report 62789 Other changes: -------------- diff --git a/build.xml b/build.xml index 6f7fc08c35..0833d5521f 100644 --- a/build.xml +++ b/build.xml @@ -49,6 +49,7 @@ + @@ -180,7 +181,10 @@ --> - + + + + package ${package.name}; + +public class Test { + public static void main(String[] args) { + System.out.println("Successful."); + } +} + + module ${test-module.name} { + exports ${package.name}; +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/org/apache/tools/ant/taskdefs/Jar.java b/src/main/org/apache/tools/ant/taskdefs/Jar.java index 0a43fbd417..0d95b2b455 100644 --- a/src/main/org/apache/tools/ant/taskdefs/Jar.java +++ b/src/main/org/apache/tools/ant/taskdefs/Jar.java @@ -20,6 +20,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -41,6 +42,7 @@ import java.util.StringTokenizer; import java.util.TreeMap; import java.util.Vector; +import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -50,6 +52,7 @@ import org.apache.tools.ant.types.ArchiveFileSet; import org.apache.tools.ant.types.EnumeratedAttribute; import org.apache.tools.ant.types.FileSet; +import org.apache.tools.ant.types.ModuleVersion; import org.apache.tools.ant.types.Path; import org.apache.tools.ant.types.Resource; import org.apache.tools.ant.types.ResourceCollection; @@ -75,6 +78,10 @@ public class Jar extends Zip { /** The manifest file name. */ private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; + /** Pattern matching name of module descriptor entry. */ + private static final Pattern MODULE_INFO = + Pattern.compile("(?:META-INF/versions/[0-9]+/)?module-info\\.class"); + /** * List of all known SPI Services */ @@ -170,6 +177,21 @@ public class Jar extends Zip { */ private boolean flattenClassPaths = false; + /** + * Module version to embed in jar file, specified as a string + * (Java 9+ only). + */ + private String version; + + /** + * Module version to embed in jar file, specified as a child element + * (Java 9+ only). + */ + private ModuleVersion moduleVersion; + + /** Module entry point to embed in jar file (Java 9+ only). */ + private String mainClass; + /** * Extra fields needed to make Solaris recognize the archive as a jar file. * @@ -452,6 +474,55 @@ public void setFlattenAttributes(boolean b) { flattenClassPaths = b; } + /** + * Sets fully qualified class name of entry point of module, + * if jar file is a modular jar file. Ignored if Java version + * is less than 9, or jar file is not a modular jar file. + * + * @param className fully qualified name of main class, or {@code null} + * + * @since Ant 1.10.6 + */ + public void setMainClass(String className) { + this.mainClass = className; + } + + // CheckStyle:LineLength OFF - Link is too long. + /** + * Sets module version + * to embed in jar file. Ignored if Java version is less than 9, + * or jar file is not a modular jar file. + * + * @param version new module version to embed in jar file + * + * @since Ant 1.10.6 + */ + // CheckStyle:LineLength ON + public void setVersion(String version) { + this.version = version; + } + + /** + * Creates an uninitialized child element representing the version of + * the modular jar. + * + * @return new, unconfigured child element + * + * @since Ant 1.10.6 + * + * @see #setVersion(String) + */ + public ModuleVersion createVersion() { + if (moduleVersion != null) { + throw new BuildException( + "No more than one 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 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()); + } +}