ComponentArchive.java
/*
* Copyright © 2025 Indiana University
* All rights reserved.
*
* BSD 3-Clause License
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* - Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package iu.type;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import edu.iu.type.IuComponent.Kind;
import edu.iu.type.base.TemporaryFile;
/**
* Reads a component archive and provides attributes necessary to realizing the
* component's instance.
*
* <p>
* An archive represents a single element in the component's path
* </p>
*
* @param path location of temporary file dedicated to this
* archive
* @param kind component kind detected via {@link ArchiveSource}
* @param version component version info read from the archive
* @param properties {@code META-INF/iu-type.properties} when
* {@link Kind#isModular()}, else
* {@code META-INF/iu.properties}
* @param nonEnclosedTypeNames all top-level classes, including those defined
* with package and protected encapsulation levels
* @param webResources all static web resources found in a
* {@link Kind#isWeb() web} archive, include those
* under WEB-INF/
* @param bundledDependencies archive sources for all dependencies bundled
* (i.e. WEB-INF/lib/*.jar) with the archive
*/
record ComponentArchive(Path path, Kind kind, ComponentVersion version, Properties properties,
Set<String> nonEnclosedTypeNames, Map<String, byte[]> webResources,
Collection<ArchiveSource> bundledDependencies) {
private static record ScannedAttributes(Kind kind, ComponentVersion version, Properties properties,
Set<String> nonEnclosedTypeNames, Map<String, byte[]> webResources,
Collection<ArchiveSource> bundledDependencies) {
}
private static ScannedAttributes scan(ArchiveSource source, ComponentTarget target) throws IOException {
final Set<String> nonEnclosedTypeNames = new LinkedHashSet<>();
final Map<String, byte[]> webResources = new LinkedHashMap<>();
final Map<String, ArchiveSource> bundledDependencies = new LinkedHashMap<>();
final Set<String> classPath = new HashSet<>();
source.classPath().forEach(classPath::add);
var isWeb = false;
var hasModuleDescriptor = false;
var hasNonWebTypes = false;
Properties pomProperties = null;
Properties typeProperties = null;
Properties iuProperties = null;
while (source.hasNext()) {
var componentEntry = source.next();
var name = componentEntry.name();
if (name.startsWith("META-INF/maven/")) {
if (name.endsWith("/pom.properties")) {
if (pomProperties != null)
throw new IllegalArgumentException("Component archive must not be a shaded (uber-)jar");
pomProperties = new Properties();
final var data = componentEntry.data();
pomProperties.load(new ByteArrayInputStream(data));
target.put(name, new ByteArrayInputStream(data));
}
continue;
}
// Detect web component first; later logic expects isWeb == true if any
// component entry begins with WEB-INF, including the current entry
if (!isWeb && name.startsWith("WEB-INF/")) {
if (hasNonWebTypes)
throw new IllegalArgumentException("Web archive must not define types outside WEB-INF/classes/");
if (!bundledDependencies.isEmpty())
throw new IllegalArgumentException("Web archive must not embed components outside WEB-INF/lib/");
if (typeProperties != null)
throw new IllegalArgumentException(
"Web archive must define META-INF/iu-type.properties as WEB-INF/classes/META-INF/iu-type.properties");
if (iuProperties != null)
throw new IllegalArgumentException(
"Web archive must define META-INF/iu.properties as WEB-INF/classes/META-INF/iu.properties");
isWeb = true;
}
if (name.endsWith(".jar")) {
if (isWeb && !name.startsWith("WEB-INF/lib/"))
throw new IllegalArgumentException("Web archive must not embed components outside WEB-INF/lib/");
if (classPath.remove(name) // allow embedded names from manifest Class-Path attribute
// also temporarily allow IU JEE 6 embedded class path entries
// these will be validated for the presence of META-INF/iu.properties
// after scanning the full archive
|| name.startsWith("WEB-INF/lib/") //
|| name.startsWith("META-INF/lib/") //
|| name.startsWith("META-INF/ejb/endorsed/") //
|| name.startsWith("META-INF/ejb/lib/")) {
var embeddedDependencyData = componentEntry.data();
var embeddedDependencyInput = new ByteArrayInputStream(embeddedDependencyData);
var embeddedDependency = new ArchiveSource(embeddedDependencyInput);
bundledDependencies.put(name, embeddedDependency);
continue;
}
}
if (name.equals("META-INF/application.xml") //
|| name.equals("META-INF/ra.xml") //
|| name.endsWith(".jar") //
|| name.endsWith(".war") //
|| name.endsWith(".rar") //
|| name.endsWith(".dll") //
|| name.endsWith(".so"))
throw new IllegalArgumentException(
"Component archive must not be an Enterprise Application (ear) or Resource Adapter Archive (rar) file");
if (name.equals("WEB-INF/classes/META-INF/iu-type.properties") //
|| name.equals("META-INF/iu-type.properties")) {
if (isWeb && name.startsWith("META-INF/"))
throw new IllegalArgumentException(
"Web archive must define META-INF/iu-type.properties as WEB-INF/classes/META-INF/iu-type.properties");
typeProperties = new Properties();
final var data = componentEntry.data();
typeProperties.load(new ByteArrayInputStream(data));
target.put(name, new ByteArrayInputStream(data));
continue;
}
if (name.equals("WEB-INF/classes/META-INF/iu.properties") //
|| name.equals("META-INF/iu.properties")) {
if (hasModuleDescriptor)
throw new IllegalArgumentException(
"Modular component archive must not include META-INF/iu.properties");
if (isWeb && name.startsWith("META-INF/"))
throw new IllegalArgumentException(
"Web archive must define META-INF/iu.properties as WEB-INF/classes/META-INF/iu.properties");
iuProperties = new Properties();
final var data = componentEntry.data();
iuProperties.load(new ByteArrayInputStream(data));
target.put(name, new ByteArrayInputStream(data));
continue;
}
// after this point, source resources are captured
if (name.endsWith(".class")) {
String resourceName;
if (isWeb)
if (name.startsWith("WEB-INF/classes/"))
resourceName = name.substring(16);
else
// this error will also be triggered if a WEB-INF/ entry is read after a class
throw new IllegalArgumentException( // is detected outside WEB-INF/classes/
"Web archive must not define types outside WEB-INF/classes/");
else {
hasNonWebTypes = true;
resourceName = name;
if (!webResources.isEmpty()) {
// This is not a web archive, so write all resources collected so far to
// the target archive and stop collecting web resource. Further entries
// with name starting with WEB-INF/ will trigger the same error as above
for (var resourceEntry : webResources.entrySet()) {
var resourceEntryName = resourceEntry.getKey();
if (resourceEntryName.charAt(resourceEntryName.length() - 1) != '/')
target.put(resourceEntryName, new ByteArrayInputStream(resourceEntry.getValue()));
}
webResources.clear();
}
}
if (resourceName.equals("module-info.class")) {
if (iuProperties != null)
throw new IllegalArgumentException(
"Modular component archive must not include META-INF/iu.properties");
hasModuleDescriptor = true;
} else if (!resourceName.endsWith("package-info.class") //
&& resourceName.indexOf('$') == -1) // check for '$' skips enclosed classes
nonEnclosedTypeNames.add(resourceName.substring(0, resourceName.length() - 6).replace('/', '.'));
componentEntry.read(in -> target.put(resourceName, in));
} else if (name.startsWith("WEB-INF/classes/")) {
var resourceName = name.substring(16);
if (resourceName.startsWith("META-INF/") && resourceName.charAt(resourceName.length() - 1) == '/')
continue;
componentEntry.read(in -> target.put(resourceName, in));
} else if ((name.startsWith("META-INF/") || name.startsWith("WEB-INF/"))
&& name.charAt(name.length() - 1) == '/')
continue;
else if (!hasNonWebTypes)
webResources.put(name, componentEntry.data());
else if (name.charAt(name.length() - 1) != '/')
componentEntry.read(in -> target.put(name, in));
}
if (!classPath.isEmpty())
throw new IllegalArgumentException(
"Component archive didn't include all bundled dependencies, missing " + classPath);
if (pomProperties == null)
throw new IllegalArgumentException("Component archive missing META-INF/maven/.../pom.properties");
var componentName = pomProperties.getProperty("artifactId");
if (componentName == null)
throw new IllegalArgumentException("Component archive must provide a name as artifactId in pom.properties");
var version = pomProperties.getProperty("version");
if (version == null)
throw new IllegalArgumentException("Component archive must provide a version in pom.properties");
final Kind kind;
final Properties properties;
if (isWeb) {
var webResourceIterator = webResources.entrySet().iterator();
while (webResourceIterator.hasNext())
if (webResourceIterator.next().getKey().startsWith("META-INF/"))
webResourceIterator.remove();
var isServlet6 = false;
for (var dependency : source.dependencies())
if (dependency.compareTo(ComponentVersion.SERVLET_6) >= 0) {
isServlet6 = true;
continue;
}
if (hasModuleDescriptor || isServlet6) {
kind = Kind.MODULAR_WAR;
properties = typeProperties;
} else {
kind = Kind.LEGACY_WAR;
properties = iuProperties;
}
} else if (hasModuleDescriptor) {
kind = Kind.MODULAR_JAR;
properties = typeProperties;
} else {
kind = Kind.LEGACY_JAR;
properties = iuProperties;
}
return new ScannedAttributes( //
kind, //
new ComponentVersion(componentName, version), //
properties, //
Collections.unmodifiableSet(nonEnclosedTypeNames), //
Collections.unmodifiableMap(webResources), //
Collections.unmodifiableCollection(bundledDependencies.values()));
}
/**
* Reads a component archive from its source.
*
* @param source {@link ArchiveSource}
* @return archive-level component attributes
* @throws IOException If an I/O error occurs reading from the source
*/
static ComponentArchive from(ArchiveSource source) throws IOException {
return TemporaryFile.init(path -> {
try (var target = new ComponentTarget(path)) {
var scannedAttributes = scan(source, target);
return new ComponentArchive(path, //
scannedAttributes.kind, //
scannedAttributes.version, //
scannedAttributes.properties, //
scannedAttributes.nonEnclosedTypeNames, //
scannedAttributes.webResources, //
scannedAttributes.bundledDependencies //
);
}
});
}
}