Resources.java

// Generated by delombok at Mon Nov 18 07:27:48 UTC 2024
package de.larssh.utils.io;

import static de.larssh.utils.SystemUtils.DEFAULT_FILE_NAME_SEPARATOR;
import static de.larssh.utils.SystemUtils.DEFAULT_FILE_NAME_SEPARATOR_CHAR;
import static de.larssh.utils.SystemUtils.FILE_EXTENSION_SEPARATOR_CHAR;
import static java.util.Collections.emptyMap;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URL;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.Optional;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import de.larssh.utils.Nullables;
import de.larssh.utils.collection.Enumerations;
import de.larssh.utils.text.Patterns;
import de.larssh.utils.text.Strings;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
 * This class contains helper methods for loading and handling resources.
 *
 * <p>
 * The commonly used methods to load resources might differ when running from
 * JAR instead of running from local file system. These methods make sure
 * behavior is the same and code works in both situations.
 */
public final class Resources {
	/**
	 * File extension for Java class files
	 */
	private static final String FILE_EXTENSION_CLASS = "class";
	/**
	 * Pattern to check for a leading previous folder indicator
	 */
	private static final Pattern PATTERN_CHECK_LEASING_PREVIOUS_FOLDER = Pattern.compile("^/?\\.\\.(/|$)");
	/**
	 * Pattern to find and remove duplicate slashes and current folder indicators
	 */
	private static final Pattern PATTERN_FIX_CURRENT_FOLDER = Pattern.compile("/(\\.?/)+");
	/**
	 * Pattern to find the path to a JAR inside an URL string with JAR protocol
	 */
	private static final Pattern PATTERN_JAR_FROM_URL = Pattern.compile("(?i)^jar:(?<pathToJar>.*)![^!]*$");

	/**
	 * Normalizes and checks {@code resource} as path for accessing resources
	 * safely.
	 *
	 * @param resource the path to normalize and check
	 * @return the normalized path
	 */
	@SuppressWarnings({"checkstyle:SuppressWarnings", "resource"})
	private static String checkAndFixResourcePath(final Path resource) {
		String path = resource.normalize().toString();
		// Normalize the file systems separator
		path = path.replace(resource.getFileSystem().getSeparator(), DEFAULT_FILE_NAME_SEPARATOR);
		// Remove duplicate slashes and current folder indicators
		path = Strings.replaceAll(path, PATTERN_FIX_CURRENT_FOLDER, DEFAULT_FILE_NAME_SEPARATOR);
		// Check for empty path
		if (path.isEmpty()) {
			throw new ResourcePathException("The resource path must not be empty.");
		}
		// Check for root path
		if (DEFAULT_FILE_NAME_SEPARATOR.equals(path)) {
			throw new ResourcePathException("The resource path must not point to root.");
		}
		// Check for leading previous folder indicator
		if (Strings.find(path, PATTERN_CHECK_LEASING_PREVIOUS_FOLDER)) {
			throw new ResourcePathException("The resource path \"%s\" must not point to a location prior root.", resource.toString());
		}
		return path;
	}

	/**
	 * Converts {@code url} to a Path object creating a
	 * {@link java.nio.file.FileSystem} if not yet existing.
	 *
	 * @param url URL string
	 * @return path
	 */
	@SuppressWarnings({"checkstyle:SuppressWarnings", "PMD.PreserveStackTrace", "resource"})
	private static Path createPath(final String url) {
		final URI uri = URI.create(url);
		try {
			return Paths.get(uri);
		} catch (final FileSystemNotFoundException fileSystemNotFoundException) {
			try {
				FileSystems.newFileSystem(uri, emptyMap());
			} catch (final IOException e) {
				e.addSuppressed(fileSystemNotFoundException);
				throw new UncheckedIOException(e);
			}
			return Paths.get(uri);
		}
	}

	/**
	 * Validates if {@code path} ends with {@code end} case sensitively.
	 *
	 * <p>
	 * In comparison to {@link Path#endsWith(Path)} this method works purely string
	 * based instead of taking the paths {@link java.nio.file.FileSystem} into
	 * account.
	 *
	 * @param path the full path with actual character casing
	 * @param end  the path end as given by the developer
	 * @return {@code true} if {@code path} ends with {@code end} case sensitively,
	 *         else {@code false}
	 */
	@SuppressFBWarnings(value = "EXS_EXCEPTION_SOFTENING_NO_CONSTRAINTS", justification = "should not happen on regular usage")
	private static boolean endsPathWithCaseSensitive(final Path path, final Path end) {
		Path canonicalPath;
		try {
			canonicalPath = path.toFile().getCanonicalFile().toPath();
		} catch (@SuppressWarnings("unused") final UnsupportedOperationException e) {
			canonicalPath = path;
		} catch (final IOException e) {
			// If an I/O error occurs, which is possible because the construction of the
			// canonical pathname may require file system queries
			throw new UncheckedIOException(e);
		}
		final int pathNames = canonicalPath.getNameCount();
		final int endNames = end.getNameCount();
		if (pathNames < endNames) {
			return false;
		}
		for (int index = endNames - 1; index >= 0; index -= 1) {
			if (!canonicalPath.getName(pathNames - endNames + index).toString().equals(end.getName(index).toString())) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Returns the class loader which was used to load {@code clazz} or the system
	 * class loader if {@code clazz} was loaded by the system class loader.
	 *
	 * @param clazz the class
	 * @return either the classes class loader or the system class loader
	 */
	@SuppressWarnings("PMD.UseProperClassLoader")
	private static ClassLoader getClassLoader(final Class<?> clazz) {
		return Nullables.orElseGet(clazz.getClassLoader(), ClassLoader::getSystemClassLoader);
	}

	/**
	 * Determine the class file that {@code clazz} is defined in. Classes with no
	 * own class file result in {@link Optional#empty()}.
	 *
	 * <p>
	 * This method might load (without closing) a {@link java.nio.file.FileSystem}
	 * to handle a JAR archive.
	 *
	 * <p>
	 * Attention: <a href=
	 * "https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8131067">https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8131067</a>
	 *
	 * @param clazz class
	 * @return path to the the class file
	 */
	public static Optional<Path> getPathToClass(final Class<?> clazz) {
		return getUrlToClass(clazz).map(URL::toString).map(Resources::createPath);
	}

	/**
	 * Determine the JAR file that {@code clazz} is loaded from.
	 *
	 * <p>
	 * Classes outside of a JAR or with no own class file result in
	 * {@link Optional#empty()}.
	 *
	 * @param clazz class
	 * @return path to the the JAR file
	 */
	public static Optional<Path> getPathToJar(final Class<?> clazz) {
		return getUrlToClass(clazz).map(URL::toString).flatMap(url -> Patterns.matches(PATTERN_JAR_FROM_URL, url)).map(matcher -> matcher.group("pathToJar")).map(Resources::createPath);
	}

	/**
	 * Finds the path to {@code resource} using the class loader's resource lookup
	 * algorithm.
	 *
	 * <p>
	 * The commonly used method {@link ClassLoader#getResource(String)} might differ
	 * when running from JAR instead of running from local file system. This method
	 * makes sure behavior is the same and code works in both situations.
	 *
	 * <p>
	 * Calling this might load the {@link java.nio.file.FileSystem} that is
	 * registered for handling JAR files. If no file or folder is found
	 * {@link Optional#empty()} is returned.
	 *
	 * @param classLoader class loader to use for resource lookup
	 * @param resource    path to the resource to find
	 * @return the path to the resource
	 */
	public static Optional<Path> getResource(final ClassLoader classLoader, final Path resource) {
		final String fixedPath = checkAndFixResourcePath(resource);
		return Optional.ofNullable(classLoader.getResource(fixedPath)).map(URL::toString).map(Resources::createPath).filter(path -> endsPathWithCaseSensitive(path, Paths.get(fixedPath)));
	}

	/**
	 * Finds the path to {@code resource} relative to {@code clazz}.
	 *
	 * <p>
	 * The commonly used method {@link Class#getResource(String)} might differ when
	 * running from JAR instead of running from local file system. This method makes
	 * sure behavior is the same and code works in both situations.
	 *
	 * <p>
	 * Calling this might load the {@link java.nio.file.FileSystem} that is
	 * registered for handling JAR files. If no file or folder is found
	 * {@link Optional#empty()} is returned.
	 *
	 * @param clazz    class to use for resource lookup
	 * @param resource relative path to the resource to find
	 * @return the path to the resource
	 */
	@SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "processing as described in JavaDoc")
	public static Optional<Path> getResourceRelativeTo(final Class<?> clazz, final Path resource) {
		return getResourceStringToClass(clazz).map(Paths::get).map(Path::getParent).map(path -> path.resolve(resource)).flatMap(absoluteResource -> getResource(getClassLoader(clazz), absoluteResource));
	}

	/**
	 * Finds the paths to {@code resource} using the class loader's resource lookup
	 * algorithm. Instead of {@link #getResource(ClassLoader, Path)} this method
	 * returns not only the first matching resource, but also the shadowed resources
	 * based on the class loaders hierarchy.
	 *
	 * <p>
	 * The commonly used method {@link ClassLoader#getResources(String)} might
	 * differ when running from JAR instead of running from local file system. This
	 * method makes sure behavior is the same and code works in both situations.
	 *
	 * <p>
	 * Calling this might load the {@link java.nio.file.FileSystem} that is
	 * registered for handling JAR files. If no file or folder is found
	 * {@link Optional#empty()} is returned.
	 *
	 * @param classLoader class loader to use for resource lookup
	 * @param resource    path to the resource to find
	 * @return the paths of the found resources
	 */
	@SuppressFBWarnings(value = "EXS_EXCEPTION_SOFTENING_NO_CONSTRAINTS", justification = "converting to unchecked IOException")
	public static Stream<Path> getResources(final ClassLoader classLoader, final Path resource) {
		final String fixedPath = checkAndFixResourcePath(resource);
		final Enumeration<URL> enumeration;
		try {
			enumeration = classLoader.getResources(fixedPath);
		} catch (final IOException e) {
			throw new UncheckedIOException(e);
		}
		return Enumerations.stream(enumeration).map(URL::toString).map(Resources::createPath).filter(path -> endsPathWithCaseSensitive(path, Paths.get(fixedPath)));
	}

	/**
	 * Finds the paths to {@code resource} relative to {@code clazz}. Instead of
	 * {@link #getResourceRelativeTo(Class, Path)} this method returns not only the
	 * first matching resource, but also the shadowed resources based on the class
	 * loaders hierarchy.
	 *
	 * <p>
	 * Calling this might load the {@link java.nio.file.FileSystem} that is
	 * registered for handling JAR files. If no file or folder is found
	 * {@link Optional#empty()} is returned.
	 *
	 * @param clazz    class to use for resource lookup
	 * @param resource path to the resource to find
	 * @return the paths of the found resources
	 */
	@SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "processing as described in JavaDoc")
	public static Stream<Path> getResourcesRelativeTo(final Class<?> clazz, final Path resource) {
		return getResourceStringToClass(clazz).map(Paths::get).map(Path::getParent).map(path -> path.resolve(resource)).map(absoluteResource -> getResources(getClassLoader(clazz), absoluteResource)).orElseGet(Stream::empty);
	}

	/**
	 * Determine the resource string to the class file that {@code clazz} is defined
	 * in. Classes with probably no own class file result in
	 * {@link Optional#empty()}.
	 *
	 * @param clazz the class
	 * @return resource string to the class file
	 */
	private static Optional<String> getResourceStringToClass(final Class<?> clazz) {
		final String className = clazz.getName();
		return className.indexOf(FILE_EXTENSION_SEPARATOR_CHAR) == -1 ? Optional.empty() : Optional.of(className.replace(FILE_EXTENSION_SEPARATOR_CHAR, DEFAULT_FILE_NAME_SEPARATOR_CHAR) + FILE_EXTENSION_SEPARATOR_CHAR + FILE_EXTENSION_CLASS);
	}

	/**
	 * Determine the class file that {@code clazz} is defined in.
	 *
	 * <p>
	 * Classes with no own class file result in {@link Optional#empty()}.
	 *
	 * @param clazz class
	 * @return URL to the the class file
	 */
	private static Optional<URL> getUrlToClass(final Class<?> clazz) {
		return getResourceStringToClass(clazz).map(resource -> getClassLoader(clazz).getResource(resource));
	}

	/**
	 * Reads the {@link Manifest} of the JAR file that {@code clazz} has been loaded
	 * from.
	 *
	 * <p>
	 * Classes outside of a JAR or with no own class file or JARs with no manifest
	 * result in {@link Optional#empty()}.
	 *
	 * @param clazz class
	 * @return manifest
	 * @throws IOException if an I/O error occurs
	 */
	public static Optional<Manifest> readManifest(final Class<?> clazz) throws IOException {
		final Optional<Path> path = getPathToJar(clazz);
		if (!path.isPresent()) {
			return Optional.empty();
		}
		try (JarInputStream inputStream = new JarInputStream(Files.newInputStream(path.get()))) {
			return Optional.ofNullable(inputStream.getManifest());
		}
	}

	@java.lang.SuppressWarnings("all")
	@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
	@lombok.Generated
	private Resources() {
		throw new java.lang.UnsupportedOperationException("This is a utility class and cannot be instantiated");
	}
}