/* * This file is part of Applied Energistics 2. * Copyright (c) 2013 - 2015, AlgorithmX2, All rights reserved. * * Applied Energistics 2 is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Applied Energistics 2 is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Applied Energistics 2. If not, see . */ package appeng.recipes.loader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLDecoder; import java.util.Collection; import java.util.Enumeration; import java.util.HashSet; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nonnull; import org.apache.commons.io.FileUtils; import com.google.common.base.Preconditions; /** * copies recipes in jars onto file system includes the readme, needs to be modified if other files needs to be handled * * @author thatsIch * @version rv3 - 11.05.2015 * @since rv3 11.05.2015 */ public class RecipeResourceCopier { /** * Most expected size of recipes found */ private static final int INITIAL_RESOURCE_CAPACITY = 20; private static final Pattern DOT_COMPILE_PATTERN = Pattern.compile( ".", Pattern.LITERAL ); private static final String FILE_PROTOCOL = "file"; private static final String CLASS_EXTENSION = ".class"; private static final String JAR_PROTOCOL = "jar"; private static final String UTF_8_ENCODING = "UTF-8"; /** * copy source in the jar */ private final String root; /** * @param root source root folder of the recipes inside the jar. * * @throws NullPointerException if root is null */ public RecipeResourceCopier( @Nonnull final String root ) { Preconditions.checkNotNull( root ); this.root = root; } /** * copies recipes found in the root to destination. * * @param identifier only copy files which end with the identifier * @param destination destination folder to which the recipes are copied to * * @throws URISyntaxException {@see #getResourceListing} * @throws IOException {@see #getResourceListing} and if copying the detected resource to file is not possible * @throws NullPointerException if either parameter is null * @throws IllegalArgumentException if destination is not a directory */ public void copyTo( @Nonnull final String identifier, @Nonnull final File destination ) throws URISyntaxException, IOException { Preconditions.checkNotNull( destination ); Preconditions.checkArgument( destination.isDirectory() ); this.copyTo( identifier, destination, this.root ); } /** * @param destination destination folder to which the recipes are copied to * @param directory the folder to copy. * * @throws URISyntaxException {@see #getResourceListing} * @throws IOException {@see #getResourceListing} and if copying the detected resource to file is not possible * @see {RecipeResourceCopier#copyTo(File)} */ private void copyTo( @Nonnull final String identifier, @Nonnull final File destination, @Nonnull final String directory ) throws URISyntaxException, IOException { assert identifier != null; assert destination != null; assert directory != null; final Class copierClass = this.getClass(); final String[] listing = this.getResourceListing( copierClass, directory ); for( final String list : listing ) { if( list.endsWith( identifier ) ) { // generate folder before the file is copied so no empty folders will be generated FileUtils.forceMkdir( destination ); this.copyFile( destination, directory, list ); } else if( !list.contains( "." ) ) { final File subDirectory = new File( destination, list ); this.copyTo( identifier, subDirectory, directory + list + "/" ); } } } /** * Copies a single file inside a folder to the destination. * * @param destination folder to which the file is copied to * @param directory the directory containing the file * @param fileName the file to copy * * @throws IOException if copying the file is not possible */ private void copyFile( @Nonnull final File destination, @Nonnull final String directory, @Nonnull final String fileName ) throws IOException { assert destination != null; assert directory != null; assert fileName != null; final Class copierClass = this.getClass(); final InputStream inStream = copierClass.getResourceAsStream( '/' + directory + fileName ); final File outFile = new File( destination, fileName ); if( !outFile.exists() && inStream != null ) { FileUtils.copyInputStreamToFile( inStream, outFile ); inStream.close(); } } /** * List directory contents for a resource folder. Not recursive. This is basically a brute-force implementation. * Works for regular files and also JARs. * * @param clazz Any java class that lives in the same place as the resources you want. * @param path Should end with "/", but not start with one. * * @return Just the name of each member item, not the full paths. * * @throws URISyntaxException if it is a file path and the URL can not be converted to URI * @throws IOException if jar path can not be decoded * @throws UnsupportedOperationException if it is neither in jar nor in file path */ @Nonnull private String[] getResourceListing( @Nonnull final Class clazz, @Nonnull final String path ) throws URISyntaxException, IOException { assert clazz != null; assert path != null; final ClassLoader classLoader = clazz.getClassLoader(); if( classLoader == null ) { throw new IllegalStateException( "ClassLoader was not found. It was probably loaded at a inappropriate time" ); } URL dirURL = classLoader.getResource( path ); if( dirURL != null ) { final String protocol = dirURL.getProtocol(); if( protocol.equals( FILE_PROTOCOL ) ) { // A file path: easy enough final URI uriOfURL = dirURL.toURI(); final File fileOfURI = new File( uriOfURL ); final String[] filesAndDirectoriesOfURI = fileOfURI.list(); if( filesAndDirectoriesOfURI == null ) { throw new IllegalStateException( "Files and Directories were illegal. Either an abstract pathname does not denote a directory, or an I/O error occured." ); } else { return filesAndDirectoriesOfURI; } } } if( dirURL == null ) { /* * In case of a jar file, we can't actually find a directory. * Have to assume the same jar as clazz. */ final String className = clazz.getName(); final Matcher matcher = DOT_COMPILE_PATTERN.matcher( className ); final String me = matcher.replaceAll( "/" ) + CLASS_EXTENSION; dirURL = classLoader.getResource( me ); } if( dirURL != null ) { final String protocol = dirURL.getProtocol(); if( protocol.equals( JAR_PROTOCOL ) ) { /* A JAR path */ final String dirPath = dirURL.getPath(); final String jarPath = dirPath.substring( 5, dirPath.indexOf( '!' ) ); // strip out only // the JAR file final JarFile jar = new JarFile( URLDecoder.decode( jarPath, UTF_8_ENCODING ) ); try { final Enumeration entries = jar.entries(); // gives ALL entries in jar final Collection result = new HashSet( INITIAL_RESOURCE_CAPACITY ); // avoid duplicates // in case it is a // subdirectory while( entries.hasMoreElements() ) { final JarEntry entry = entries.nextElement(); final String entryFullName = entry.getName(); if( entryFullName.startsWith( path ) ) { // filter according to the path String entryName = entryFullName.substring( path.length() ); final int checkSubDir = entryName.indexOf( '/' ); if( checkSubDir >= 0 ) { // if it is a subdirectory, we just return the directory name entryName = entryName.substring( 0, checkSubDir ); } result.add( entryName ); } } return result.toArray( new String[result.size()] ); } finally { jar.close(); } } } throw new UnsupportedOperationException( "Cannot list files for URL " + dirURL ); } }