/*
* 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 <http://www.gnu.org/licenses/lgpl>.
*/
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 com.google.common.base.Preconditions;
import org.apache.commons.io.FileUtils;
/**
* 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 <tt>null</tt>
*/
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 <tt>null</tt>
* @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<? extends RecipeResourceCopier> 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<? extends RecipeResourceCopier> 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<JarEntry> entries = jar.entries(); // gives ALL entries in jar
final Collection<String> result = new HashSet<String>( 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 );
}
}