TemporaryFile.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 edu.iu.type.base;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Queue;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import edu.iu.IuException;
import edu.iu.IuStream;
import edu.iu.UnsafeFunction;
import edu.iu.UnsafeRunnable;
/**
* Initializes a temporary file with fail-safe delete when initialization fails.
*/
public final class TemporaryFile {
private static final ThreadLocal<Queue<Path>> PENDING = new ThreadLocal<>();
/**
* Tracks temp files created by an initialization task, and either deletes all
* created if an error is thrown, or returns a thunk that may be used to deletes
* all files on teardown.
*
* @param task initialization task
* @return destroy thunk
* @throws IOException from {@link UnsafeRunnable}
*/
public static UnsafeRunnable init(UnsafeRunnable task) throws IOException {
final Queue<Path> tempFilesToRestore = PENDING.get();
final Queue<Path> tempFiles = new ArrayDeque<>();
final UnsafeRunnable teardown = () -> {
Throwable e = null;
for (final var path : tempFiles)
e = IuException.suppress(e, () -> Files.deleteIfExists(path));
if (e != null)
throw IuException.checked(e, IOException.class);
};
try {
PENDING.set(tempFiles);
task.run();
return teardown;
} catch (Throwable e) {
IuException.suppress(e, teardown);
throw IuException.checked(e, IOException.class);
} finally {
if (tempFilesToRestore == null)
PENDING.remove();
else
PENDING.set(tempFilesToRestore);
}
}
/**
* Initializes a temp file.
*
* @param <T> initialization result
* @param tempFileInitializer initialization function
* @return result of initialization; <em>must</em> contain
* implementation-specific logic to delete the temp file as part of
* destroying the initialized resource
* @throws IOException If an I/O error is thrown from the initializer
*/
public static <T> T init(UnsafeFunction<Path, T> tempFileInitializer) throws IOException {
class Box {
T initialized;
}
final var box = new Box();
UnsafeRunnable init = () -> {
Path temp = Files.createTempFile("iu-type-", ".jar");
PENDING.get().offer(temp);
box.initialized = tempFileInitializer.apply(temp);
};
if (PENDING.get() == null)
init(init);
else
IuException.checked(IOException.class, init);
return box.initialized;
}
/**
* Copies data from an input stream and outputs to a temporary file.
*
* @param in supplies data
* @return {@link Path} to a temporary file with a copy of the data
*/
public static Path of(InputStream in) {
return IuException.unchecked(() -> init(temporaryFile -> {
try (final var out = Files.newOutputStream(temporaryFile)) {
IuStream.copy(in, out);
}
return temporaryFile;
}));
}
/**
* Copies data from an input stream and outputs to a temporary file.
*
* @param url supplies data; <em>must</em> refer to a file or jar entry from a
* local filesystem. The method <em>should</em> only be used with
* values from {@link ClassLoader#getResource(String)} or
* {@link ClassLoader#getResources(String)}
* @return {@link Path} to the canonical file indicated by the URL, or to a
* temporary file with a copy of the Jar file entry
*/
public static Path of(URL url) {
return with(url, o -> {
if (o instanceof Path)
return (Path) o;
else
return of((InputStream) o);
});
}
/**
* Reads a mixed class/module path for loading a component from a bundled
* classpath resource.
*
* <p>
* This package format expected by this method is a specific instance of
* Enterprise Archive (EAR) useful for bootstrapping a resource adapter or
* application client module as a iu-java-type compatible component. In the
* sample Maven config below, the project that includes the embedding pom
* segment uses this method to convert the embedded component EAR to a path.
* </p>
*
* <pre>
* final var path = TemporaryFile.of(getClass().getClassLoader().getResource("META-INF/client/my-bundle.jar");
* </pre>
*
* <h4>bundle pom</h4>
*
* <pre>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>bundle-distribution</id>
<goals>
<goal>single</goal>
</goals>
<phase>package</phase>
<configuration>
<descriptors>
<descriptor>src/assembly/bundle.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
* </pre>
*
* <h4>src/assembly/bundle.xml</h4>
*
* <pre>
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://maven.apache.org/ASSEMBLY/2.2.0 http://maven.apache.org/xsd/assembly-2.2.0.xsd">
<id>bundle</id>
<includeBaseDirectory>false</includeBaseDirectory>
<formats>
<format>jar</format>
</formats>
<dependencySets>
<dependencySet>
<includes>
<include>${project.groupId}:${project.artifactId}</include>
</includes>
</dependencySet>
<dependencySet>
<scope>runtime</scope>
<outputDirectory>lib/</outputDirectory>
<excludes>
<exclude>${project.groupId}:${project.artifactId}</exclude>
</excludes>
</dependencySet>
</dependencySets>
</assembly>
* </pre>
*
* <h4>embedding pom</h4>
*
* <pre>
<build>
<plugins>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>import-bundle</id>
<phase>prepare-package</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>${project.groupId}</groupId>
<artifactId>my-client</artifactId>
<version>${project.version}</version>
<classifier>bundle</classifier>
</artifactItem>
</artifactItems>
<stripVersion>true</stripVersion>
<outputDirectory>
${project.build.outputDirectory}/META-INF/client</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
* </pre>
*
* @param bundleResource File or Jar {@link URL} indicating location of the
* bundled EAR resource i.e., from
* {@link ClassLoader#getResource(String)}
* @return class/module path suitable for use with
* {@link ModularClassLoader#ModularClassLoader(boolean, Iterable, ModuleLayer, ClassLoader, java.util.function.Consumer)}.
*
* @see <a href=
* "https://jakarta.ee/specifications/platform/10/jakarta-platform-spec-10.0#a2948">JEE
* 10 8.2.1</a>
*/
public static Iterable<Path> readBundle(URL bundleResource) {
final Deque<Path> libs = new ArrayDeque<>();
with(bundleResource, o -> {
try (final var in = (o instanceof Path) //
? Files.newInputStream((Path) o) //
: (InputStream) o; //
final var bundleJar = new JarInputStream(in)) {
JarEntry entry;
while ((entry = bundleJar.getNextJarEntry()) != null) {
final var name = entry.getName();
if (name.endsWith(".jar")) {
final var lib = name.startsWith("lib/");
final var bundledLib = TemporaryFile.init(path -> {
try (final var out = Files.newOutputStream(path)) {
IuStream.copy(bundleJar, out);
}
return path;
});
if (lib)
libs.offer(bundledLib);
else
libs.offerFirst(bundledLib);
}
}
bundleJar.closeEntry();
}
return null;
});
return libs;
}
private static <T> T with(URL url, UnsafeFunction<Object, T> then) {
return IuException.unchecked(() -> {
final var uri = url.toURI();
final var scheme = uri.getScheme();
if ("file".equals(scheme))
return then.apply(Path.of(uri).toRealPath());
if (!"jar".equals(scheme))
throw new IllegalArgumentException();
final var jarSpec = uri.getSchemeSpecificPart();
final var bangSlash = jarSpec.indexOf("!/");
final var jarUri = URI.create(jarSpec.substring(0, bangSlash));
if (!"file".equals(jarUri.getScheme()))
throw new IllegalArgumentException();
final var entryName = jarSpec.substring(bangSlash + 2);
try (final var jarFile = Files.newInputStream(Path.of(jarUri).toRealPath());
final var jar = new JarInputStream(jarFile)) {
JarEntry entry;
while ((entry = jar.getNextJarEntry()) != null)
if (entry.getName().equals(entryName))
return then.apply(jar);
}
throw new IllegalArgumentException();
});
}
private TemporaryFile() {
}
}