/*
* Copyright (C) 2011 Laurent Caillette
*
* This program 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.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.novelang.common.filefixture;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.MissingResourceException;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import org.novelang.logger.Logger;
import org.novelang.logger.LoggerFactory;
import org.novelang.outfit.loader.ClasspathResourceLoader;
/**
* Transforms the hierarchical representation of resources into real files.
*
* @author Laurent Caillette
*/
public final class ResourceSchema {
private static final InvocationHandler NULL_INVOCATION_HANDLER = new InvocationHandler() {
@Override
public Object invoke(
final Object o,
final Method method,
final Object[] objects
) throws Throwable {
throw new UnsupportedOperationException( "invoke" );
}
};
/**
* Factory method.
*/
public static Directory directory( final String name ) {
return new Directory( name ) ;
}
/**
* Factory method.
*/
public static Resource resource( final String name ) {
return new Resource( name ) ;
}
private static final ClasspathResourceLoader CLASSPATH_RESOURCE_LOADER =
new ClasspathResourceLoader() ;
private static final Logger LOGGER = LoggerFactory.getLogger( ResourceSchema.class );
private ResourceSchema() { }
// ==============
// Initialization
// ==============
public static void initialize( final Class declaration ) {
initialize( "", declaration ) ;
}
public static void initialize( final String resourcePrefix, final Class declaration ) {
Preconditions.checkNotNull( declaration ) ;
Preconditions.checkNotNull( resourcePrefix ) ;
try {
if( ! findDirectoryObject( declaration ).isInitialized() ) {
final Directory rootDirectory = makeDirectoryOfClass( declaration ) ;
checkUnderlyingResources( resourcePrefix, rootDirectory ) ;
}
} catch ( DeclarationException e ) {
throw new RuntimeException( e );
} catch ( IllegalAccessException e ) {
throw new RuntimeException( e );
} catch ( IOException e ) {
throw new RuntimeException( e );
}
}
private static void checkUnderlyingResources(
final String resourcePrefix,
final Directory directory
)
throws MissingResourceException, IOException
{
final String directoryPath = resourcePrefix + "/" + directory.getName() ;
directory.setAbsoluteResourceName( directoryPath ) ;
for( final Resource resource : directory.getResources() ) {
resource.setParent( directory ) ;
final String resourcePath = directoryPath + "/" + resource.getName() ;
final InputStream inputStream = ResourceSchema.class.getResourceAsStream( resourcePath ) ;
if( null == inputStream ) {
throw new MissingResourceException( resourcePath, ResourceSchema.class.getName(), "" ) ;
}
inputStream.close() ;
resource.setAbsoluteResourceName( resourcePath ) ;
LOGGER.debug( "Verified: ", resource.getAbsoluteResourceName() ) ;
}
for( final Directory subDirectory : directory.getSubdirectories() ) {
subDirectory.setParent( directory ) ;
checkUnderlyingResources( directoryPath, subDirectory ) ;
}
}
private static Directory makeDirectoryOfClass( final Class declaringClass )
throws DeclarationException, IllegalAccessException
{
final Directory directory = findDirectoryObject( declaringClass ) ;
directory.setDeclaringClass( declaringClass ) ;
directory.setResources( findResources( declaringClass ) ) ;
directory.setDirectories( findDirectories( declaringClass ) ) ;
return directory ;
}
private static List< Resource > findResources( final Class declaringClass )
throws IllegalAccessException, DeclarationException
{
final List< Resource > resources = Lists.newArrayList() ;
final Field[] fields = declaringClass.getDeclaredFields() ;
for( final Field field : fields ) {
checkAllowed( field ) ;
if( Resource.class.equals( field.getType() ) ) {
final Resource resource = ( Resource ) field.get( null ) ;
resource.setDeclaringClass( declaringClass ) ;
resources.add( resource ) ;
}
}
return Ordering.natural().sortedCopy( resources ) ;
}
private static List< Directory > findDirectories( final Class declaringClass )
throws IllegalAccessException, DeclarationException
{
final List< Directory > directories = Lists.newArrayList() ;
final Class[] interfaces = declaringClass.getDeclaredClasses() ;
for( final Class ynterface : interfaces ) {
checkAllowed( ynterface ) ;
final Directory directory = makeDirectoryOfClass( ynterface ) ;
directories.add( directory ) ;
}
return Ordering.natural().sortedCopy( directories ) ;
}
private static void checkAllowed( final Class ynterface ) throws DeclarationException {
if( ynterface.isAnonymousClass() ) {
throw new DeclarationException( "Misses requirements: " + ynterface ) ;
}
}
private static Directory findDirectoryObject( final Class ynterface )
throws DeclarationException, IllegalAccessException
{
final Field[] fields = ynterface.getDeclaredFields() ;
boolean found = false ;
Directory directory = null ;
for( final Field field : fields ) {
if( Directory.class.equals( field.getType() ) ) {
if( found ) {
throw new DeclarationException( "More than one field of " +
Directory.class.getSimpleName() + " in " + ynterface.getName() ) ;
} else {
field.setAccessible( true ) ;
final Object ynterfaceProxy = Proxy.newProxyInstance(
ynterface.getClassLoader(),
new Class< ? >[] { ynterface },
NULL_INVOCATION_HANDLER
) ;
directory = ( Directory )
field.get( ynterfaceProxy ) ;
found = true ;
}
}
}
if( found ) {
return directory ;
} else {
throw new DeclarationException( "Missing directory declaration" ) ;
}
}
private static void checkAllowed( final Field field ) throws DeclarationException {
final int modifiers = field.getModifiers() ;
if( Modifier.isAbstract( modifiers )
|| Modifier.isNative( modifiers )
|| Modifier.isPrivate( modifiers )
|| Modifier.isProtected( modifiers )
|| Modifier.isProtected( modifiers )
) {
throw new DeclarationException( "Field " + field + " has unsupported modifier" ) ;
}
if( Modifier.isFinal( modifiers )
&& Modifier.isPublic( modifiers )
&& Modifier.isStatic( modifiers )
) {
return ;
} else {
throw new DeclarationException( "Field " + field + " misses one modifier or more" ) ;
}
}
// =================
// Various utilities
// =================
/**
* Returns true if {@code maybeParent} is one of the parents of {@code maybeChild}, false
* otherwise.
*
* @param maybeParent a non-null object.
* @param maybeChild a non-null object.
*/
public static boolean isParentOf( final Directory maybeParent, final SchemaNode maybeChild ) {
Preconditions.checkNotNull( maybeParent ) ;
Preconditions.checkNotNull( maybeChild ) ;
return maybeParentOfOrSameAs( maybeParent, maybeChild.getParent() ) ;
}
/**
* Returns true if {@code maybeParent} is one of the parent of {@code maybeChild},
* or if it is the same object as {@code maybeChild}.
*
* @param maybeParent a non-null object.
* @param maybeChild a non-null object.
*/
public static boolean isParentOfOrSameAs(
final Directory maybeParent,
final SchemaNode maybeChild
) {
Preconditions.checkNotNull( maybeParent ) ;
Preconditions.checkNotNull( maybeChild ) ;
return maybeParentOfOrSameAs( maybeParent, maybeChild ) ;
}
public static Relativizer relativizer( final Directory newParent ) {
return new Relativizer( newParent ) ;
}
@Deprecated
public static String relativizeResourcePath( final Directory parent, final SchemaNode child ) {
Preconditions.checkNotNull( parent ) ;
Preconditions.checkNotNull( child ) ;
final String parentPath = parent.getAbsoluteResourceName();
final String childPath = child.getAbsoluteResourceName();
Preconditions.checkArgument(
childPath.startsWith( parentPath ),
"Parent path '%s' does not contain '%s'",
parentPath,
childPath
) ;
return childPath.substring( parentPath.length() ) ;
}
private static boolean maybeParentOfOrSameAs(
final Directory maybeParent,
final SchemaNode maybeChild
) {
if( null == maybeChild ) {
return false ;
} else if( maybeChild == maybeParent ) {
return true ;
} else {
return maybeParentOfOrSameAs( maybeParent, maybeChild.getParent() ) ;
}
}
}