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