| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with this |
| * work for additional information regarding copyright ownership. The ASF |
| * licenses this file to You under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package org.apache.sling.feature.io; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.net.JarURLConnection; |
| import java.net.MalformedURLException; |
| import java.net.URISyntaxException; |
| import java.net.URL; |
| import java.nio.file.Files; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.List; |
| import java.util.jar.JarFile; |
| |
| public class IOUtils { |
| |
| /** The extension for a reference file. */ |
| public static final String EXTENSION_REF_FILE = ".ref"; |
| |
| /** The extension for a feature file. */ |
| public static final String EXTENSION_FEATURE_FILE = ".json"; |
| |
| /** The default directory to search for features. */ |
| public static final String DEFAULT_DIRECTORY = "features"; |
| |
| /** The default name of the feature file. */ |
| public static final String DEFAULT_FEATURE_FILE = "feature" + EXTENSION_FEATURE_FILE; |
| |
| /** |
| * Parse a feature reference file |
| * @param file The file |
| * @return The referenced features |
| * @throws IOException If reading fails |
| */ |
| public static List<String> parseFeatureRefFile(final File file) |
| throws IOException { |
| final List<String> result = new ArrayList<>(); |
| final List<String> lines = Files.readAllLines(file.toPath()); |
| for(String line : lines) { |
| line = line.trim(); |
| if ( !line.isEmpty() && !line.startsWith("#") ) { |
| if ( line.indexOf(':') == -1 ) { |
| result.add(new File(line).getAbsolutePath()); |
| } else { |
| result.add(line); |
| } |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * Get the list of feature files. |
| * If the provided list of files is {@code null} or an empty array, the default is used. |
| * The default checks for the following places, the first one found is used. If none is |
| * found an empty list is returned. |
| * <ol> |
| * <li>A directory named {@link #DEFAULT_DIRECTORY} in the current directory |
| * <li>A file named {@link #DEFAULT_FEATURE_FILE} in the current directory |
| * <li>A directory named {@link #DEFAULT_DIRECTORY} in the home directory |
| * <li>A file named {@link #DEFAULT_FEATURE_FILE} in the home directory |
| * </ol> |
| * |
| * The list of files is processed one after the other. If it is relative, it is |
| * first tried to be resolved against the current directory and then against the |
| * home directory. |
| * If an entry denotes a directory, all children ending in {@link #EXTENSION_FEATURE_FILE} or |
| * {@link #EXTENSION_REF_FILE} of that directory are read. |
| * If a file ends in {@link #EXTENSION_REF_FILE} the contents is read and every line not |
| * starting with the hash sign is considered a reference to a feature artifact. |
| * |
| * @param homeDirectory If relative files should be resolved, this is the directory to use |
| * @param files Optional list of files. If none is provided, a default is used. |
| * @return The list of files. |
| * @throws IOException If an error occurs. |
| */ |
| public static List<String> getFeatureFiles(final File homeDirectory, final String... files) |
| throws IOException { |
| String[] featureFiles = files; |
| if ( featureFiles == null || featureFiles.length == 0 ) { |
| // Default value - check feature directory otherwise features file |
| final File[] candidates = new File[] { |
| new File(homeDirectory, DEFAULT_DIRECTORY), |
| new File(homeDirectory, DEFAULT_FEATURE_FILE), |
| new File(DEFAULT_DIRECTORY), |
| new File(DEFAULT_FEATURE_FILE) |
| }; |
| File f = null; |
| for(final File c : candidates) { |
| if ( c.exists() ) { |
| f = c; |
| break; |
| } |
| } |
| // nothing found, we default to the first candidate and fail later |
| if ( f == null ) { |
| f = candidates[0]; |
| } |
| |
| featureFiles = new String[] {f.getAbsolutePath()}; |
| } |
| |
| final List<String> paths = new ArrayList<>(); |
| for(final String name : featureFiles) { |
| // check for absolute |
| if ( name.indexOf(':') > 1 ) { |
| paths.add(name); |
| } else { |
| // file or relative |
| File f = null; |
| final File test = new File(name); |
| if ( test.isAbsolute() ) { |
| f = test; |
| } else { |
| final File[] candidates = { |
| new File(homeDirectory, name), |
| new File(homeDirectory, DEFAULT_DIRECTORY + File.separatorChar + name), |
| new File(name), |
| new File(DEFAULT_DIRECTORY + File.separatorChar + name), |
| }; |
| for(final File c : candidates) { |
| if ( c.exists() && c.isFile() ) { |
| f = c; |
| break; |
| } |
| } |
| } |
| |
| if ( f != null && f.exists() ) { |
| if ( f.isFile() ) { |
| processFile(paths, f); |
| } else { |
| processDir(paths, f); |
| } |
| } else { |
| // we simply add the path and fail later on |
| paths.add(new File(name).getAbsolutePath()); |
| } |
| } |
| } |
| |
| Collections.sort(paths, FEATURE_PATH_COMP); |
| return paths; |
| } |
| |
| /** |
| * Get a File from a local URL (if possible) |
| * |
| * @param url a local url (like a file: url or a jar:file: url |
| * @param cache if an attempt should be made to download the content of the url locally |
| * if it can not be presented as a file directly |
| * @param tmpDir the tmpDir to use (null for default) |
| * @return the file the url points to (or null if none) - or a tmp file if cache is true and the url could be cached |
| * @throws IOException |
| */ |
| public static File getFileFromURL(URL url, boolean cache, File tmpDir) throws IOException { |
| File result; |
| if (url.getProtocol().equals("file")) { |
| try { |
| result = new File(url.toURI()); |
| } catch (URISyntaxException e) { |
| result = new File(url.getPath()); |
| } |
| } else if (url.getProtocol().equals("jar")) { |
| String innerURL = url.getPath(); |
| if (innerURL.endsWith("!/") && innerURL.indexOf("!/") == innerURL.lastIndexOf("!/")) { |
| innerURL = innerURL.substring(0, innerURL.indexOf("!/")); |
| try { |
| result = getFileFromURL(new URL(innerURL), cache, tmpDir); |
| } catch (IOException ex) { |
| result = null; |
| } |
| } |
| else { |
| result = null; |
| } |
| } |
| else { |
| result = null; |
| } |
| |
| if ((result == null || !result.exists()) && cache) { |
| File tmp = File.createTempFile("jar", ".jar", tmpDir); |
| try (InputStream input = url.openStream(); OutputStream output = new FileOutputStream(tmp)) { |
| byte[] buffer =new byte[64 * 1024]; |
| for (int i = input.read(buffer); i != -1;i = input.read(buffer)) { |
| output.write(buffer, 0, i); |
| } |
| } |
| result = tmp; |
| } |
| return result; |
| } |
| |
| /** |
| * Get a JarFile from a local URL (if possible) |
| * |
| * @param url a local url (like a file: url or a jar:file: url |
| * @param cache if an attempt should be made to download the content of the url locally |
| * if it can not be presented as a jarfile directly |
| * @param tmpDir the tmpDir to use (null for default) |
| * |
| * @return the jarfile the url points to |
| * @throws IOException if the url can't be represented as a jarfile |
| */ |
| public static JarFile getJarFileFromURL(URL url, boolean cache, File tmpDir) throws IOException { |
| try { |
| URL targetURL = url; |
| if (!url.getProtocol().equals("jar")) { |
| targetURL = new URL("jar:" + toURLString(url) + "!/"); |
| } |
| else if (!url.getPath().endsWith("!/")) { |
| targetURL = new URL(toURLString(url) + "!/"); |
| } |
| return ((JarURLConnection) targetURL.openConnection()).getJarFile(); |
| } catch (IOException ex) { |
| File file = getFileFromURL(url, cache, tmpDir); |
| if (file != null) { |
| return new JarFile(file); |
| } |
| else { |
| throw ex; |
| } |
| } |
| } |
| |
| private static String toURLString(URL url) { |
| try { |
| return url.toURI().toURL().toString(); |
| } catch (URISyntaxException | MalformedURLException e) { |
| return url.toString(); |
| } |
| } |
| static final Comparator<String> FEATURE_PATH_COMP = new Comparator<String>() { |
| @Override |
| public int compare(final String o1, final String o2) { |
| // windows path conversion |
| final String key1 = o1.replace(File.separatorChar, '/'); |
| final String key2 = o2.replace(File.separatorChar, '/'); |
| |
| final int lastSlash1 = key1.lastIndexOf('/'); |
| final int lastSlash2 = key2.lastIndexOf('/'); |
| if ( lastSlash1 == -1 || lastSlash2 == -1 ) { |
| return o1.compareTo(o2); |
| } |
| final String path1 = key1.substring(0, lastSlash1 + 1); |
| final String path2 = key2.substring(0, lastSlash2 + 1); |
| if ( path1.equals(path2) ) { |
| return o1.compareTo(o2); |
| } |
| if ( path1.startsWith(path2) ) { |
| return 1; |
| } else if ( path2.startsWith(path1) ) { |
| return -1; |
| } |
| return o1.compareTo(o2); |
| } |
| }; |
| |
| private static void processDir(final List<String> paths, final File dir) |
| throws IOException { |
| for(final File f : dir.listFiles()) { |
| if ( f.isFile() && !f.getName().startsWith(".")) { |
| // check if file is a reference |
| if ( f.getName().endsWith(EXTENSION_REF_FILE) || f.getName().endsWith(EXTENSION_FEATURE_FILE) ) { |
| processFile(paths, f); |
| } |
| } |
| } |
| } |
| |
| private static void processFile(final List<String> paths, final File f) |
| throws IOException { |
| if ( f.getName().endsWith(EXTENSION_REF_FILE) ) { |
| paths.addAll(parseFeatureRefFile(f)); |
| } else { |
| paths.add(f.getAbsolutePath()); |
| } |
| } |
| } |