001/******************************************************************************* 002 * This software is provided as a supplement to the authors' textbooks on digital 003 * image processing published by Springer-Verlag in various languages and editions. 004 * Permission to use and distribute this software is granted under the BSD 2-Clause 005 * "Simplified" License (see http://opensource.org/licenses/BSD-2-Clause). 006 * Copyright (c) 2006-2025 Wilhelm Burger, Mark J. Burge. All rights reserved. 007 * Visit https://imagingbook.com for additional details. 008 ******************************************************************************/ 009package imagingbook.core.resource; 010 011import java.io.File; 012import java.io.IOException; 013import java.net.URI; 014import java.net.URISyntaxException; 015import java.net.URL; 016import java.nio.file.FileSystem; 017import java.nio.file.FileSystemNotFoundException; 018import java.nio.file.FileSystems; 019import java.nio.file.Files; 020import java.nio.file.Path; 021import java.nio.file.Paths; 022import java.util.ArrayList; 023import java.util.Collections; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Objects; 027import java.util.stream.Stream; 028 029 030/** 031 * <p> 032 * This class defines static methods for accessing resources. What makes things somewhat complex is the requirement that 033 * we want to retrieve resources located in the file system or contained inside a JAR file. 034 * </p> 035 * <p> 036 * Here is a typical URI for a JAR-embedded file: 037 * {@code 038 * "jar:file:/C:/PROJEC~2/parent/IM1D84~1/ImageJ/jars/jarWithResources.jar!/jarWithResouces/resources/clown.jpg"} 039 * </p> 040 * 041 * @author WB 042 * @version 2022/11/01 043 */ 044public abstract class ResourceUtils { 045 046 private ResourceUtils() {} 047 048 /** 049 * Determines if the specified class was loaded from a JAR file or a .class file in the file system. 050 * 051 * @param clazz the class 052 * @return true if contained in a JAR file, false otherwise 053 */ 054 public static boolean isInsideJar(Class<?> clazz) { 055 URL url = clazz.getProtectionDomain().getCodeSource().getLocation(); 056 String path = url.getPath(); 057 File file = new File(path); 058 return file.isFile(); 059 } 060 061 062 /** 063 * Finds the URI for a resource relative to a specified class. The resource may be located in the file system or 064 * inside a JAR file. 065 * 066 * @param clazz the anchor class 067 * @param relPath the resource path relative to the anchor class (file or directory) 068 * @return the URI or {@code null} if the resource was not found 069 */ 070 public static URI getResourceUri(Class<?> clazz, String relPath) { 071 // System.out.println("ResourceUtils.getResourceUri(): relPath = " + relPath); 072 URI uri = null; 073 if (isInsideJar(clazz)) { 074 // System.out.println("ResourceUtils.getResourceUri(): inside JAR"); 075 String classPath = clazz.getProtectionDomain().getCodeSource().getLocation().getFile(); 076 // String packagePath = clazz.getPackage().getName().replace('.', File.separatorChar); 077 String packagePath = clazz.getPackage().getName().replace('.', '/'); 078 String compPath = "jar:file:" + classPath + "!/" + packagePath + "/" + relPath; 079 try { 080 uri = new URI(compPath); 081 } catch (URISyntaxException e) { 082 // throw new RuntimeException("getResourceURI: " + e.toString()); 083 } 084 } 085 else { // regular file path 086 // System.out.println("ResourceUtils.getResourceUri(): regular file"); 087 try { 088 URL url = clazz.getResource(relPath); 089 // System.out.println("ResourceUtils.getResourceUri(): url = " + url); 090 uri = clazz.getResource(relPath).toURI(); 091 } catch (Exception e) { 092 //do nothing, just return null - was: throw new RuntimeException("getResourceURI: " + e.toString()); 093 } 094 } 095 return uri; 096 } 097 098 099 100 /** 101 * <p> 102 * Finds the path to a resource relative to the location of some class. Example: Assume class C was loaded from file 103 * {@code someLocation/C.class} and there is a subfolder {@code someLocation/resources/} that contains an image file 104 * {@code lenna.jpg}. Then the complete path to this image is obtained by 105 * </p> 106 * <pre> 107 * Path path = getResourcePath(C.class, "resources/lenna.jpg"); 108 * </pre> 109 * 110 * @param clazz anchor class 111 * @param relPath the path of the resource to be found (relative to the location of the anchor class) 112 * @return the path to the specified resource 113 */ 114 public static Path getResourcePath(Class<?> clazz, String relPath) { 115 // 2016/06/03: modified to return proper path to resource inside a JAR file. 116 URI uri = getResourceUri(clazz, relPath); 117 if (uri != null) { 118 return uriToPath(uri); 119 } 120 else { 121 return null; 122 } 123 } 124 125 /** 126 * Converts an {@link URI} to a {@link Path} for locations that are either in the file system or inside a JAR file. 127 * 128 * @param uri the specified location 129 * @return the associated path 130 */ 131 public static Path uriToPath(URI uri) { 132 Path path = null; 133 String scheme = uri.getScheme(); 134 switch (scheme) { 135 case "jar": { // resource inside JAR file 136 FileSystem fs = null; 137 try { // check if this FileSystem already exists 138 fs = FileSystems.getFileSystem(uri); 139 } catch (FileSystemNotFoundException e) { 140 // that's OK to happen, the file system is not created automatically 141 } 142 143 if (fs == null) { // must not create the file system twice 144 try { 145 fs = FileSystems.newFileSystem(uri, Collections.<String, Object>emptyMap()); 146 } catch (IOException e) { 147 throw new RuntimeException("uriToPath: " + e.toString()); 148 } 149 } 150 151 String ssp = uri.getSchemeSpecificPart(); 152 int startIdx = ssp.lastIndexOf('!'); 153 String inJarPath = ssp.substring(startIdx + 1); // in-Jar path (after the last '!') 154 path = fs.getPath(inJarPath); 155 break; 156 } 157 case "file": { // resource in ordinary file system 158 path = Paths.get(uri); 159 break; 160 } 161 default: 162 throw new IllegalArgumentException("Cannot handle this URI type: " + scheme); 163 } 164 return path; 165 } 166 167 public static Path[] getResourcePaths(URI uri) { 168 return getResourcePaths(uriToPath(uri)); 169 } 170 171 /** 172 * Method to obtain the paths to all files in a directory specified by a {@link Path} (non-recursively). This should 173 * work in an ordinary file system as well as a (possibly nested) JAR file. 174 * 175 * @param path {@link Path} to a directory (may be contained in a JAR file) 176 * @return a possibly empty sequence of paths 177 */ 178 public static Path[] getResourcePaths(Path path) { 179 // with help from http://stackoverflow.com/questions/1429172/how-do-i-list-the-files-inside-a-jar-file, #10 180 if (!Files.isDirectory(path)) { 181 throw new IllegalArgumentException("path is not a directory: " + path.toString()); 182 } 183 184 List<Path> pathList = new ArrayList<Path>(); 185 Stream<Path> walk = null; 186 try { 187 walk = Files.walk(path, 1); 188 } catch (IOException e) { 189 e.printStackTrace(); 190 } 191 192 for (Iterator<Path> it = walk.iterator(); it.hasNext();){ 193 Path p = it.next(); 194 if (Files.isRegularFile(p) && Files.isReadable(p)) { 195 pathList.add(p); 196 } 197 } 198 walk.close(); 199 return pathList.toArray(new Path[0]); 200 } 201 202 /** 203 * Use this method to obtain the paths to all files in a directory located relative to the specified class 204 * (non-recursively). This should work in an ordinary file system as well as a (possibly nested) JAR file. 205 * 206 * @param clazz class whose source location defines the root 207 * @param relPath path relative to the root 208 * @return a possibly empty array of paths 209 */ 210 public static Path[] getResourcePaths(Class<?> clazz, String relPath) { 211 URI uri = getResourceUri(clazz, relPath); 212 if (uri == null) { 213 throw new RuntimeException("uri is null for resource class " + clazz.getSimpleName() 214 + " and relative path " + relPath); 215 } 216 return getResourcePaths(uri); 217 } 218 219 /** 220 * Use this method to obtain the names of all files in a directory located relative to the specified class 221 * (non-recursively). This should work in an ordinary file system as well as a (possibly nested) JAR file. 222 * 223 * @param clazz class whose source location specifies the root 224 * @param relDir directory relative to the root 225 * @return a possibly empty array of file names 226 */ 227 public static String[] getResourceFileNames(Class<?> clazz, String relDir) { 228 if (!relDir.endsWith("/")) { 229 relDir = relDir + "/"; // TODO: check for more elegant solution 230 } 231 Path[] paths = ResourceUtils.getResourcePaths(clazz, relDir); 232 List<String> names = new ArrayList<>(); 233 for (Path p : paths) { 234 names.add(p.getFileName().toString()); 235 } 236 return names.toArray(new String[0]); 237 } 238 239}