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-2016 Wilhelm Burger, Mark J. Burge. All rights reserved. 
007 * Visit http://imagingbook.com for additional details.
008 *******************************************************************************/
009package imagingbook.lib.util;
010
011import java.io.File;
012import java.io.IOException;
013import java.io.InputStream;
014import java.net.URI;
015import java.net.URISyntaxException;
016import java.net.URL;
017import java.nio.file.FileSystem;
018import java.nio.file.FileSystemNotFoundException;
019import java.nio.file.FileSystems;
020import java.nio.file.Files;
021import java.nio.file.Path;
022import java.nio.file.Paths;
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.Iterator;
026import java.util.List;
027import java.util.stream.Stream;
028
029import ij.IJ;
030import ij.ImagePlus;
031import ij.io.Opener;
032
033/**
034 * This class defines static methods for accessing resources.
035 * What makes things somewhat complex is the requirement that
036 * we want to retrieve resources located in the file system or
037 * contained inside a JAR file.
038 *  
039 * A typical URI for a JAR-embedded file:
040 * "jar:file:/C:/PROJEC~2/parent/IM1D84~1/ImageJ/jars/jarWithResources.jar!/jarWithResouces/resources/clown.jpg"
041 *
042 * @author W. Burger
043 * @version 2016/06/04
044 *
045 */
046public class ResourceUtils {
047        
048        /**
049         * Determines if the specified class was loaded from
050         * a JAR file or a .class file in the file system.
051         * 
052         * @param clazz the class
053         * @return true if contained in a JAR file, false otherwise
054         */
055        public static boolean isInsideJar(Class<?> clazz) {
056                URL url = clazz.getProtectionDomain().getCodeSource().getLocation();
057                String path = url.getPath();
058                File file = new File(path);
059                return file.isFile();
060        }
061        
062        /**
063         * Finds the URI for a resource relative to a specified class.
064         * The resource may be located in the file system or
065         * inside a JAR file.
066         * 
067         * @param clazz the anchor class
068         * @param relPath the resource path relative to the anchor class
069         * @return the URI or {@code null} if the resource was not found
070         */
071        public static URI getResourceUri(Class<?> clazz, String  relPath) {
072                URI uri = null;
073                if (isInsideJar(clazz)) {
074                        String classPath = clazz.getProtectionDomain().getCodeSource().getLocation().getFile();
075                        //String packagePath = clazz.getPackage().getName().replace('.', File.separatorChar);
076                        String packagePath = clazz.getPackage().getName().replace('.', '/');
077                        String compPath = "jar:file:" + classPath + "!/" + packagePath + "/" + relPath;
078                        try {
079                                uri = new URI(compPath);
080                        } catch (URISyntaxException e) {
081                                throw new RuntimeException("getResourceURI: " + e.toString());
082                        }       
083                }
084                else {  // regular file path
085                        try {
086                                uri = clazz.getResource(relPath).toURI();
087                        } catch (Exception e) {
088                                throw new RuntimeException("getResourceURI: " + e.toString());
089                        }
090                }
091                return uri;
092        }
093        
094        /**
095         * Find the path to a resource relative to the location of class c.
096         * Example: Assume class C was loaded from file someLocation/C.class
097         * and there is a subfolder someLocation/resources/ that contains 
098         * an image 'lenna.jpg'. Then the absolute path to this image
099         * is obtained by 
100         * String path = getResourcePath(C.class, "resources/lenna.jpg");
101         * 
102         * 2016-06-03: modified to return proper path to resource inside 
103         * a JAR file.
104         * 
105         * @param clazz anchor class 
106         * @param relPath the path of the resource to be found (relative to the location of the anchor class)
107         * @return the path to the specified resource
108         */
109        public static Path getResourcePath(Class<?> clazz, String  relPath) {
110                URI uri = getResourceUri(clazz, relPath);
111                if (uri != null) {
112                        return uriToPath(uri);
113                }
114                else {
115                        return null;
116                }
117        }
118        
119        /**
120         * Converts an URI to a Path for locations that are either
121         * in the file system or inside a JAR file.
122         * 
123         * @param uri the specified location
124         * @return the associated path
125         */
126        public static Path uriToPath(URI uri) {
127                Path path = null;
128                String scheme = uri.getScheme();
129                switch (scheme) {
130                case "jar":     {       // resource inside JAR file
131                        FileSystem fs = null;
132                        try { // check if this FileSystem already exists 
133                                fs = FileSystems.getFileSystem(uri);
134                        } catch (FileSystemNotFoundException e) {
135                                // that's OK to happen, the file system is not created automatically
136                        }
137                        
138                        if (fs == null) {       // must not create the file system twice
139                                try {
140                                        fs = FileSystems.newFileSystem(uri, Collections.<String, Object>emptyMap());
141                                } catch (IOException e) {
142                                        throw new RuntimeException("uriToPath: " + e.toString());
143                                }
144                        }
145                        
146                        String ssp = uri.getSchemeSpecificPart();
147                        int startIdx = ssp.lastIndexOf('!');
148                        String inJarPath = ssp.substring(startIdx + 1);  // in-Jar path (after the last '!')
149                        path = fs.getPath(inJarPath);
150                        break;
151                }
152                case "file": {  // resource in ordinary file system
153                        path = Paths.get(uri);
154                        break;
155                }
156                default:
157                        throw new IllegalArgumentException("Cannot handle this URI type: " + scheme);
158                }
159                return path;
160        }
161        
162        
163        public static Path[] listResources(URI uri) {
164                return listResources(uriToPath(uri));
165        }
166        
167        
168        /**
169         * Method to obtain the paths to all files in a directory specified
170         * by a path. This should work in an ordinary file system
171         * as well as a (possibly nested) JAR file.
172         * 
173         * @param path path to a directory (may be contained in a JAR file) 
174         * @return a sequence of paths or {@code null} if the specified path 
175         * is not a directory
176         */
177        public static Path[] listResources(Path path) {
178                // with help from http://stackoverflow.com/questions/1429172/how-do-i-list-the-files-inside-a-jar-file, #10
179                if (!Files.isDirectory(path)) {
180                        throw new IllegalArgumentException("path is not a directory: " + path.toString());
181                }
182                
183                List<Path> pathList = new ArrayList<Path>();
184                Stream<Path> walk = null;
185                try {
186                        walk = Files.walk(path, 1);
187                } catch (IOException e) {
188                        e.printStackTrace();
189                }
190
191                for (Iterator<Path> it = walk.iterator(); it.hasNext();){
192                        Path p = it.next();
193                        if (Files.isRegularFile(p) && Files.isReadable(p)) {
194                                pathList.add(p);
195                        }
196                }
197                walk.close();
198                return pathList.toArray(new Path[0]);
199        }
200        
201        /**
202         * Use this method to obtain the paths to all files in a directory located
203         * relative to the specified class. This should work in an ordinary file system
204         * as well as a (possibly nested) JAR file.
205         * 
206         * @param clazz class whose source location specifies the root 
207         * @param relPath path relative to the root
208         * @return a sequence of paths or {@code null} if the specified path is not a directory
209         */
210        public static Path[] listResources(Class<?> clazz, String relPath) {
211                return listResources(getResourceUri(clazz, relPath));
212        }
213        
214        
215        /**
216         * Opens an image from the specified resource. 
217         * If the resource is contained inside a JAR file, it is first
218         * extracted to a temporary file and subsequently opened
219         * with ImageJ's {@code Opener} class. 
220         * 
221         * @param clazz the anchor class
222         * @param resDir the directory relative to the anchor class
223         * @param resName the (file) name of the image resource
224         * @return the opened image or {@code null} if not successful.
225         */
226        public static ImagePlus openImageFromResource(Class<?> clazz, String resDir, String resName) {
227                URI uri = getResourceUri(clazz, resDir + resName);
228                if (uri == null) {
229                        IJ.error("resource not found: " + clazz.getName() + " | " + resDir  + " | " + resName);
230                        return null;
231                }
232                
233                ImagePlus im = null;
234                
235                String scheme = uri.getScheme();
236                switch (scheme) {
237                case "file": {  // resource in ordinary file system
238                        Path path = Paths.get(uri);
239                        im = new Opener().openImage(path.toString());
240                        break;
241                }
242                case "jar": { // resource inside JAR
243                        // create a temporary file:
244                        String ext = FileUtils.getFileExtension(resName);
245                        File tmpFile = null;
246                        try {
247                                tmpFile = File.createTempFile("img", "." + ext);
248                                tmpFile.deleteOnExit();
249                        } 
250                        catch (IOException e) {
251                                throw new RuntimeException("Could not create temporary file");
252                        }
253                                                
254                        //IJ.log("copying to tmp file: " + tmpFile.getPath());
255                        String relPath = resDir + resName;
256                        InputStream inStrm = clazz.getResourceAsStream(relPath);
257                        
258                        try {
259                                FileUtils.copyToFile(inStrm, tmpFile);
260                        } catch (IOException e) {
261                                throw new RuntimeException("Could not copy stream to temporary file");
262                        }
263                        im = new Opener().openImage(tmpFile.getPath());
264                        if (im != null) {
265                                im.setTitle(resName);
266                        }
267                        tmpFile.delete();
268                        break;
269                }
270                default:
271                        throw new IllegalArgumentException("Cannot handle this resource type: " + scheme);
272                }
273                return im;
274        }
275        
276
277
278//      /**
279//       * Checks 'by name' of a particular resource exists.
280//       * 
281//       * @param classname name of the class, e.g. {@literal imagingbook.lib.util.FileUtils}
282//       * @param recourcePath path (relative to the location of the class) to the specified resource 
283//       * @return {@code true} if the specified resource was found, {@code false} otherwise
284//       */
285//      public static boolean checkResource(String classname, String recourcePath) {
286//              String logStr = "  checking resource " + classname + ":" + recourcePath + " ... ";
287//              try {
288//                      //if (Class.forName(classname).getResourceAsStream(recourcePath) != null) {
289//                      if (Class.forName(classname).getResource(recourcePath)!= null) {
290//                              IJ.log(logStr + "OK");
291//                              return true;
292//                      }
293//              } catch (Exception e) { }
294//              IJ.log(logStr + "ERROR");
295//              return false;
296//      }
297
298}