blob: f2750181939444985deaecab34a81d13f3f4fcc9 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.xml;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.io.InputStream;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleDescriptor.Requires;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.junit.jupiter.api.Test;
/**
* Guards the generated OSGi and JPMS descriptors against a regression that makes any of the library's optional dependencies mandatory.
*
* <p>Both descriptors are generated from bytecode by tools whose output can shift across versions: bnd derives the {@code Import-Package} header, and jdeps
* (via moditect) derives {@code module-info}. Rather than launch an OSGi framework or a separate module layer, this test reads the two descriptors straight
* out of the built jar and asserts the same invariant against each: the only mandatory dependency may be on the platform itself. Every other dependency, such
* as Saxon HE or Xerces, must be optional, so a deployment that does not provide it still resolves.</p>
*
* <p>"Platform" means the packages an OSGi system bundle always exports, or the {@code java.*} modules the JDK always supplies. Those are the one set of
* dependencies that does not need to be optional.</p>
*
* <p>This test reads {@code module-info} through {@link ModuleDescriptor}, a Java 9 API the project's release-8 test sources cannot reference. It therefore
* lives in {@code src/test/java11} and is compiled and run only under the {@code java11-tests} profile, which activates on JDK 11 or later.</p>
*/
class DescriptorIT {
/**
* Package prefixes an OSGi system bundle always exports, so an import of them need not be optional. {@code java} and {@code javax} cover the JRE; the two
* {@code org} entries cover the DOM and SAX APIs the JRE also ships.
*/
private static final List<String> PLATFORM_PACKAGE_PREFIXES = List.of("java", "javax", "org.w3c", "org.xml.sax");
/**
* Opens the built artifact named by the failsafe {@code buildJar} system property.
*/
private static JarFile openBuildJar() throws IOException {
final String jar = System.getProperty("buildJar");
assertNotNull(jar, "System property 'buildJar' must point at the built artifact");
return new JarFile(jar);
}
@Test
void everyModuleRequiresIsPlatformOrOptional() throws IOException {
try (JarFile jar = openBuildJar()) {
final JarEntry entry = findModuleInfo(jar);
assertNotNull(entry, "Built jar must contain a module descriptor (root or under META-INF/versions/)");
final ModuleDescriptor descriptor;
try (InputStream in = jar.getInputStream(entry)) {
descriptor = ModuleDescriptor.read(in);
}
for (final Requires requires : descriptor.requires()) {
final boolean platform = requires.name().startsWith("java.");
final boolean optional = requires.modifiers().contains(Requires.Modifier.STATIC);
assertTrue(platform || optional,
"Non-platform module '" + requires.name() + "' must be an optional (static) requires, was: " + requires);
}
}
}
@Test
void everyBundleImportIsPlatformOrOptional() throws IOException {
try (JarFile jar = openBuildJar()) {
final Manifest manifest = jar.getManifest();
assertNotNull(manifest, "Built jar must contain a manifest");
final String importPackage = manifest.getMainAttributes().getValue("Import-Package");
assertNotNull(importPackage, "Bundle must declare an Import-Package header");
for (final String clause : splitClauses(importPackage)) {
final String pkg = packageName(clause);
final boolean platform = isPlatformPackage(pkg);
final boolean optional = clause.replace(" ", "").contains("resolution:=optional");
assertTrue(platform || optional,
"Import of non-platform package '" + pkg + "' must be optional, was: " + clause.trim());
}
}
}
/**
* Locates the module descriptor in the jar. A Java 8 library carries it as a Multi-Release entry under {@code META-INF/versions/<N>/module-info.class}
* rather than at the root, so this accepts either location.
*/
private static JarEntry findModuleInfo(final JarFile jar) {
final Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
final JarEntry entry = entries.nextElement();
final String name = entry.getName();
if (name.equals("module-info.class") || (name.startsWith("META-INF/versions/") && name.endsWith("/module-info.class"))) {
return entry;
}
}
return null;
}
/**
* Returns whether {@code pkg} is exported by an OSGi system bundle, i.e. it sits under one of {@link #PLATFORM_PACKAGE_PREFIXES}.
*/
private static boolean isPlatformPackage(final String pkg) {
for (final String prefix : PLATFORM_PACKAGE_PREFIXES) {
if (pkg.equals(prefix) || pkg.startsWith(prefix + ".")) {
return true;
}
}
return false;
}
/**
* Splits an OSGi header into its clauses on commas, ignoring commas inside double-quoted attribute values such as {@code version="[1.0,2.0)"}.
*/
private static List<String> splitClauses(final String header) {
final List<String> clauses = new ArrayList<>();
boolean quoted = false;
int start = 0;
for (int i = 0; i < header.length(); i++) {
final char c = header.charAt(i);
if (c == '"') {
quoted = !quoted;
} else if (c == ',' && !quoted) {
clauses.add(header.substring(start, i));
start = i + 1;
}
}
clauses.add(header.substring(start));
return clauses;
}
/**
* Returns the package name of a clause, i.e. everything before its first {@code ;} attribute separator.
*/
private static String packageName(final String clause) {
final int semicolon = clause.indexOf(';');
return (semicolon < 0 ? clause : clause.substring(0, semicolon)).trim();
}
}