001/*
002 * Copyright 2021 Global Biodiversity Information Facility (GBIF)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.gbif.utils.file;
017
018import org.gbif.utils.PreconditionUtils;
019
020import java.io.File;
021import java.io.IOException;
022import java.net.URISyntaxException;
023import java.net.URL;
024import java.net.URLDecoder;
025import java.nio.file.Files;
026import java.nio.file.Paths;
027import java.util.Enumeration;
028import java.util.HashSet;
029import java.util.Set;
030import java.util.jar.JarEntry;
031import java.util.jar.JarFile;
032
033import org.apache.commons.lang3.ObjectUtils;
034import org.apache.commons.lang3.StringUtils;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038/**
039 * Utils class dealing with classpath resources.
040 */
041public final class ResourcesUtil {
042
043  private static final Logger LOG = LoggerFactory.getLogger(ResourcesUtil.class);
044
045  /**
046   * Static utils class.
047   */
048  private ResourcesUtil() {
049  }
050
051  /**
052   * Copies classpath resources to real files.
053   *
054   * @param folder                 to copy resource files into
055   * @param ignoreMissingResources if true ignores missing resources, throws IOException otherwise
056   * @param classpathPrefix        common prefix added to all classpath resources which is not used for the result file
057   *                               path.
058   * @param classpathResources     list of classpath resources to be copied into folder
059   */
060  public static void copy(File folder, String classpathPrefix, boolean ignoreMissingResources,
061    String... classpathResources) throws IOException {
062    for (String classpathResource : classpathResources) {
063      String res = classpathPrefix + classpathResource;
064      URL url;
065      try {
066        url = getResource(res);
067        if (url == null) {
068          throw new IllegalArgumentException("Classpath resource " + res + " not existing");
069        }
070      } catch (IllegalArgumentException e) {
071        if (ignoreMissingResources) {
072          LOG.debug("Resource {} not found", res);
073          continue;
074        }
075        throw new IOException(e);
076      }
077
078      File f = new File(folder, classpathResource);
079      FileUtils.createParentDirs(f);
080
081      try {
082        Files.copy(Paths.get(url.toURI()), f.toPath());
083      } catch (URISyntaxException e) {
084        throw new IOException(e);
085      }
086    }
087  }
088
089  /**
090   * Returns a {@code URL} pointing to {@code resourceName} if the resource is found using the
091   * {@linkplain Thread#getContextClassLoader() context class loader}. In simple environments, the
092   * context class loader will find resources from the class path. In environments where different
093   * threads can have different class loaders, for example app servers, the context class loader
094   * will typically have been set to an appropriate loader for the current thread.
095   *
096   * <p>In the unusual case where the context class loader is null, the class loader that loaded
097   * this class will be used instead.
098   *
099   * <p>From Guava.
100   *
101   * @throws IllegalArgumentException if the resource is not found
102   */
103  public static URL getResource(String resourceName) {
104    ClassLoader loader =
105        ObjectUtils.firstNonNull(Thread.currentThread().getContextClassLoader(), ResourcesUtil.class.getClassLoader());
106    URL url = loader.getResource(resourceName);
107    PreconditionUtils.checkArgument(url != null, "resource " + resourceName + " not found.");
108    return url;
109  }
110
111    /**
112     * List directory contents for a resource folder. Not recursive.
113     * Works for regular files and also JARs.
114     *
115     * Based on code from Greg Briggs, slightly modified.
116     *
117     * @param clazz Any java class that lives in the same place as the resources you want.
118     * @param path Should end with "/", but not start with one.
119     * @return Just the name of each member item, not the full paths. Empty array in case folder cannot be found
120     * @throws IOException
121     */
122    public static String[] list(Class clazz, String path) throws IOException {
123        if (!path.endsWith("/")) {
124            path = path + "/";
125        }
126        URL dirURL = clazz.getClassLoader().getResource(path);
127        if (dirURL != null && dirURL.getProtocol().equals("file")) {
128        /* A file path: easy enough */
129            try {
130                return new File(dirURL.toURI()).list();
131            } catch (URISyntaxException e) {
132                throw new IOException("Bad URI. Cannot list files for path " + path + " in class " + clazz, e);
133            }
134        }
135
136        if (dirURL == null) {
137        /*
138         * In case of a jar file, we can't actually find a directory.
139         * Have to assume the same jar as clazz.
140         */
141            String me = clazz.getName().replace(".", "/")+".class";
142            dirURL = clazz.getClassLoader().getResource(me);
143        }
144
145        if (dirURL.getProtocol().equals("jar")) {
146        /* A JAR path */
147            String jarPath = dirURL.getPath().substring(5, dirURL.getPath().indexOf("!")); //strip out only the JAR file
148            JarFile jar = new JarFile(URLDecoder.decode(jarPath, FileUtils.UTF8));
149            Enumeration<JarEntry> entries = jar.entries(); //gives ALL entries in jar
150            Set<String> result = new HashSet<>(); //avoid duplicates in case it is a subdirectory
151            while(entries.hasMoreElements()) {
152                String name = entries.nextElement().getName();
153                if (name.startsWith(path)) { //filter according to the path
154                    String entry = name.substring(path.length());
155                    if (!StringUtils.isBlank(entry)) {
156                        int checkSubdir = entry.indexOf("/");
157                        if (checkSubdir >= 0) {
158                            // if it is a subdirectory, we just return the directory name
159                            entry = entry.substring(0, checkSubdir);
160                        }
161                        result.add(entry);
162                    }
163                }
164            }
165            return result.toArray(new String[result.size()]);
166        }
167
168        return new String[]{};
169    }
170
171}