/*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* 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 Lesser General Public License for more details.
*
* Copyright (c) 2006 - 2013 Pentaho Corporation and Contributors. All rights reserved.
*/
package org.pentaho.reporting.libraries.repository;
import org.pentaho.reporting.libraries.base.util.IOUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.StringTokenizer;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* A collection of repository related helper methods that make it easier to work with repositories.
*
* @author Thomas Morgner
*/
public class RepositoryUtilities {
/**
* Private constructor to prevent object creation.
*/
private RepositoryUtilities() {
}
/**
* Returns the content entity for the given path name.
*
* @param repository the repository from where to retrieve the content entity.
* @param name the path name as array of name-segments.
* @return the entity at the position, never null.
* @throws ContentIOException if the path did not point to a valid content entity.
* @see RepositoryUtilities#splitPath(String, String)
*/
public static ContentEntity getEntity( final Repository repository, final String[] name )
throws ContentIOException {
if ( repository == null ) {
throw new NullPointerException( "Repository given must not be null." );
}
if ( name == null ) {
throw new NullPointerException( "Path-Name array must not be null." );
}
final int length = name.length;
if ( length == 0 ) {
return repository.getRoot();
}
ContentLocation node = repository.getRoot();
for ( int i = 0; i < length - 1; i++ ) {
final String nameItem = name[ i ];
final ContentEntity entry = node.getEntry( nameItem );
if ( entry instanceof ContentLocation == false ) {
// its ok, if we hit the last item
throw new ContentIOException( "No such item: " + nameItem + " in " + node.getContentId() );
}
node = (ContentLocation) entry;
}
return node.getEntry( name[ length - 1 ] );
}
/**
* Checks whether a given pathname points to a valid content entity.
*
* @param repository the repository from where to retrieve the content entity.
* @param name the path name as array of name-segments.
* @return true, if the entity exists, false otherwise.
* @throws ContentIOException if an unexpected repository error occured.
* @see RepositoryUtilities#splitPath(String, String)
*/
public static boolean isExistsEntity( final Repository repository, final String[] name )
throws ContentIOException {
if ( repository == null ) {
throw new NullPointerException( "Repository given must not be null." );
}
if ( name == null ) {
throw new NullPointerException( "Path-Name array must not be null." );
}
final int length = name.length;
if ( length == 0 ) {
return true;
}
ContentLocation node = repository.getRoot();
for ( int i = 0; i < length - 1; i++ ) {
final String nameItem = name[ i ];
if ( node.exists( nameItem ) == false ) {
// if there is no such path segment, indicate non-existence
return false;
}
final ContentEntity entry = node.getEntry( nameItem );
if ( entry instanceof ContentLocation == false ) {
// if the inner path segment is a leaf, indicate non-existence
return false;
}
node = (ContentLocation) entry;
}
// finally check for the last item...
return node.exists( name[ length - 1 ] );
}
/**
* Tries to create a content item with the given path-name in the repository. This call will succeed if and only if
* all but the last segment of the name point to Content-Locations and if the content-item does not yet exist.
*
* @param repository the repository in which a new entity should be created.
* @param name the name of the new entity as path name.
* @return the newly created content-item.
* @throws ContentIOException if an repository error occured or if the path was not valid.
*/
public static ContentItem createItem( final Repository repository, final String[] name )
throws ContentIOException {
if ( repository == null ) {
throw new NullPointerException( "Repository given must not be null." );
}
if ( name == null ) {
throw new NullPointerException( "Path-Name array must not be null." );
}
final int length = name.length;
if ( length == 0 ) {
throw new IllegalArgumentException( "Empty name not permitted." );
}
ContentLocation node = repository.getRoot();
for ( int i = 0; i < length - 1; i++ ) {
final String nameItem = name[ i ];
if ( node.exists( nameItem ) == false ) {
// create it
node = node.createLocation( nameItem );
} else {
final ContentEntity entry = node.getEntry( nameItem );
if ( entry instanceof ContentLocation == false ) {
// its ok, if we hit the last item
throw new ContentIOException( "No such item." );
}
node = (ContentLocation) entry;
}
}
return node.createItem( name[ length - 1 ] );
}
/**
* Tries to create a content location with the given path-name in the repository. This call will succeed if and only
* if all but the last segment of the name point to Content-Locations and if the content-entity does not yet exist.
*
* @param repository the repository in which a new entity should be created.
* @param name the name of the new entity as path name.
* @return the newly created content-location.
* @throws ContentIOException if an repository error occured or if the path was not valid.
*/
public static ContentLocation createLocation( final Repository repository, final String[] name )
throws ContentIOException {
if ( repository == null ) {
throw new NullPointerException( "Repository given must not be null." );
}
if ( name == null ) {
throw new NullPointerException( "Path-Name array must not be null." );
}
final int length = name.length;
if ( length == 0 ) {
throw new IllegalArgumentException( "Empty name not permitted." );
}
ContentLocation node = repository.getRoot();
for ( int i = 0; i < length - 1; i++ ) {
final String nameItem = name[ i ];
if ( node.exists( nameItem ) == false ) {
// create it
node = node.createLocation( nameItem );
} else {
final ContentEntity entry = node.getEntry( nameItem );
if ( entry instanceof ContentLocation == false ) {
// its ok, if we hit the last item
throw new ContentIOException( "No such item." );
}
node = (ContentLocation) entry;
}
}
return node.createLocation( name[ length - 1 ] );
}
/**
* Splits a string on the given separator. Multiple occurences of the separator are unified into a single separator.
*
* @param name the path name.
* @param separator the separator on which to split.
* @return the name as array of atomar path elements.
*/
public static String[] splitPath( final String name, final String separator ) {
if ( name == null ) {
throw new NullPointerException( "Path-Name must not be null." );
}
if ( separator == null ) {
throw new NullPointerException( "Separator must not be null." );
}
final StringTokenizer strtok = new StringTokenizer( name, separator, false );
final int tokenCount = strtok.countTokens();
final String[] retval = new String[ tokenCount ];
int i = 0;
boolean emptyTokenRemoved = false;
while ( strtok.hasMoreTokens() ) {
final String token = strtok.nextToken();
retval[ i ] = token;
if ( "".equals( token ) == false ) {
i += 1;
} else {
emptyTokenRemoved = true;
}
}
if ( emptyTokenRemoved == false ) {
return retval;
}
final String[] reducedArray = new String[ i ];
System.arraycopy( retval, 0, reducedArray, 0, i );
return reducedArray;
}
/**
* Splits a string on the given separator. Multiple occurences of the separator result in empty strings as path
* elements in the returned array.
*
* @param name the path name.
* @param separator the separator on which to split.
* @return the name as array of atomar path elements.
*/
public static String[] split( final String name, final String separator ) {
if ( name == null ) {
throw new NullPointerException( "Path-Name must not be null." );
}
if ( separator == null ) {
throw new NullPointerException( "Separator must not be null." );
}
final StringTokenizer strtok = new StringTokenizer( name, separator, false );
final int tokenCount = strtok.countTokens();
final String[] retval = new String[ tokenCount ];
int i = 0;
while ( strtok.hasMoreTokens() ) {
final String token = strtok.nextToken();
retval[ i ] = token;
i += 1;
}
return retval;
}
/**
* Builds a absolute pathname for the given entity.
*
* @param entity the entity for which the pathname should be computed.
* @return the absolute path.
*/
public static String[] buildNameArray( ContentEntity entity ) {
if ( entity == null ) {
throw new NullPointerException( "Entity given must not be null." );
}
final ArrayList collector = new ArrayList( 20 );
while ( entity != null ) {
final ContentLocation parent = entity.getParent();
if ( parent != null ) {
// this filters out the root ..
collector.add( 0, entity.getName() );
}
entity = parent;
}
return (String[]) collector.toArray( new String[ collector.size() ] );
}
/**
* Builds a string of an absolute pathname for the given entity and using the given separator to separate filename
* segments..
*
* @param entity the entity for which the pathname should be computed.
* @param separator the filename separator.
* @return the absolute path.
*/
public static String buildName( ContentEntity entity, final String separator ) {
if ( entity == null ) {
throw new NullPointerException( "ContentEntity must not be null." );
}
if ( separator == null ) {
throw new NullPointerException( "Separator must not be null." );
}
int size = 0;
final ArrayList collector = new ArrayList();
while ( entity != null ) {
final ContentLocation parent = entity.getParent();
if ( parent != null ) {
// this filters out the root ..
final String name = entity.getName();
if ( name.length() == 0 ) {
throw new IllegalStateException( "ContentLocation with an empty name" );
}
if ( isInvalidPathName( name ) ) {
throw new IllegalStateException( "ContentLocation with an illegal name: " + name );
}
collector.add( name );
size += 1;
size += name.length();
}
entity = parent;
}
final StringBuffer builder = new StringBuffer( size );
final int maxIdx = collector.size() - 1;
for ( int i = maxIdx; i >= 0; i-- ) {
final String s = (String) collector.get( i );
if ( i != maxIdx ) {
builder.append( separator );
}
builder.append( s );
}
return builder.toString();
}
/**
* Checks whether the given entity name is valid for filesystems. This method rejects filenames that either contain a
* slash ('/') or backslash ('\') which both are commonly used path-separators and it rejects filenames that contain
* only dots (as the dot names are used as directory traversal names).
*
* @param name the filename that should be tested. This name must be a single name section, not a full path.
* @return true, if the pathname is valid, false otherwise.
*/
public static boolean isInvalidPathName( String name ) {
if ( name == null ) {
throw new NullPointerException( "Name must not be null." );
}
boolean onlyDots = true;
for ( int i = 0; i < name.length(); i++ ) {
final char c = name.charAt( i );
if ( onlyDots && c != '.' ) {
onlyDots = false;
}
if ( c == '\\' || c == '/' ) {
return true;
}
}
return onlyDots;
}
/**
* Writes the given repository as ZIP-File into the given output stream.
*
* @param outputStream the output stream that should receive the zipfile.
* @param repository the repository that should be written.
* @throws IOException if an IO error prevents the writing of the file.
* @throws ContentIOException if a repository related IO error occurs.
*/
public static void writeAsZip( final OutputStream outputStream,
final Repository repository ) throws IOException, ContentIOException {
final ZipOutputStream zipout = new ZipOutputStream( outputStream );
writeToZipStream( zipout, repository );
zipout.finish();
zipout.flush();
}
/**
* Writes the given repository to the given ZIP-output stream.
*
* @param zipOutputStream the output stream that represents the ZipFile to be generated.
* @param repository the repository that should be written.
* @throws IOException if an IO error prevents the writing of the file.
* @throws ContentIOException if a repository related IO error occurs.
*/
public static void writeToZipStream( final ZipOutputStream zipOutputStream,
final Repository repository ) throws IOException, ContentIOException {
writeLocation( repository.getRoot(), zipOutputStream );
}
/**
* Recursively writes the given contentlocation and all content-items into the given Zip output stream.
*
* @param outputStream the output stream that should receive the zipfile.
* @param location the content location that should be written.
* @throws IOException if an IO error prevents the writing of the file.
* @throws ContentIOException if a repository related IO error occurs.
*/
private static void writeLocation( final ContentLocation location,
final ZipOutputStream outputStream ) throws IOException, ContentIOException {
final ContentEntity[] contentEntities = location.listContents();
for ( int i = 0; i < contentEntities.length; i++ ) {
final ContentEntity entity = contentEntities[ i ];
final String fullName = RepositoryUtilities.buildName( entity, "/" );
if ( entity instanceof ContentLocation ) {
final ContentLocation childlocation = (ContentLocation) entity;
final ZipEntry dirEntry = new ZipEntry( fullName + '/' );
final Object comment =
entity.getAttribute( LibRepositoryBoot.ZIP_DOMAIN, LibRepositoryBoot.ZIP_COMMENT_ATTRIBUTE );
if ( comment != null ) {
dirEntry.setComment( String.valueOf( comment ) );
}
final Object version = entity.getAttribute
( LibRepositoryBoot.REPOSITORY_DOMAIN, LibRepositoryBoot.VERSION_ATTRIBUTE );
if ( version instanceof Date ) {
final Date date = (Date) version;
dirEntry.setTime( date.getTime() );
}
outputStream.putNextEntry( dirEntry );
writeLocation( childlocation, outputStream );
} else if ( entity instanceof ContentItem ) {
final ContentItem item = (ContentItem) entity;
final ZipEntry itemEntry = new ZipEntry( fullName );
final Object comment =
entity.getAttribute( LibRepositoryBoot.ZIP_DOMAIN, LibRepositoryBoot.ZIP_COMMENT_ATTRIBUTE );
if ( comment != null ) {
itemEntry.setComment( String.valueOf( comment ) );
}
final Object version = entity.getAttribute
( LibRepositoryBoot.REPOSITORY_DOMAIN, LibRepositoryBoot.VERSION_ATTRIBUTE );
if ( version instanceof Date ) {
final Date date = (Date) version;
itemEntry.setTime( date.getTime() );
}
// need the CRC and the size if method is "stored".
final Object crc32 = entity.getAttribute
( LibRepositoryBoot.ZIP_DOMAIN, LibRepositoryBoot.ZIP_CRC32_ATTRIBUTE );
final Object size = entity.getAttribute
( LibRepositoryBoot.REPOSITORY_DOMAIN, LibRepositoryBoot.SIZE_ATTRIBUTE );
if ( crc32 instanceof Long && size instanceof Long ) {
final Long crc32Long = (Long) crc32;
final Long sizeLong = (Long) size;
itemEntry.setSize( sizeLong.longValue() );
itemEntry.setCrc( crc32Long.longValue() );
final int method = getZipMethod( item );
final int compression = getZipLevel( item );
outputStream.setMethod( method );
outputStream.setLevel( compression );
}
outputStream.putNextEntry( itemEntry );
final InputStream inputStream = item.getInputStream();
try {
IOUtils.getInstance().copyStreams( inputStream, outputStream );
} finally {
inputStream.close();
}
outputStream.closeEntry();
}
}
}
/**
* Computes the declared Zip-Compression level for the given content-item. If the content-items attributes do not
* contain a definition, the default compression is used instead.
*
* @param item the content item for which the compression factor should be computed.
* @return the compression level.
*/
public static int getZipLevel( final ContentItem item ) {
final Object method =
item.getAttribute( LibRepositoryBoot.ZIP_DOMAIN, LibRepositoryBoot.ZIP_COMPRESSION_ATTRIBUTE );
if ( method instanceof Number == false ) {
return Deflater.DEFAULT_COMPRESSION;
}
final Number n = (Number) method;
final int level = n.intValue();
if ( level < 0 || level > 9 ) {
return Deflater.DEFAULT_COMPRESSION;
}
return level;
}
/**
* Computes the declared Zip-Compression mode for the given content-item. If the content-items attributes do not
* contain a valid definition, the default compression is used instead.
*
* @param item the content item for which the compression mode should be computed.
* @return the compression mode, either ZipOutputStream.DEFLATED or ZipOutputStream.STORED.
*/
public static int getZipMethod( final ContentItem item ) {
final Object method =
item.getAttribute( LibRepositoryBoot.ZIP_DOMAIN, LibRepositoryBoot.ZIP_METHOD_ATTRIBUTE );
if ( method instanceof Number == false ) {
return ZipOutputStream.DEFLATED;
}
final Number n = (Number) method;
final int level = n.intValue();
if ( level != ZipOutputStream.STORED ) {
return ZipOutputStream.DEFLATED;
}
return ZipOutputStream.STORED;
}
}