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}