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