/*! * 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) 2002-2013 Pentaho Corporation.. All rights reserved. */ package org.pentaho.reporting.libraries.base.util; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.Writer; import java.net.URL; import java.sql.Blob; import java.sql.Clob; import java.sql.SQLException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.StringTokenizer; /** * The IOUtils provide some IO related helper methods. * * @author Thomas Morgner. */ public class IOUtils { /** * the singleton instance of the utility package. */ private static IOUtils instance; private static final Log logger = LogFactory.getLog( IOUtils.class ); /** * DefaultConstructor. */ private IOUtils() { } /** * Gets the singleton instance of the utility package. * * @return the singleton instance. */ public static synchronized IOUtils getInstance() { if ( instance == null ) { instance = new IOUtils(); } return instance; } /** * Checks, whether the URL uses a file based protocol. * * @param url the url. * @return true, if the url is file based. */ private boolean isFileStyleProtocol( final URL url ) { if ( url == null ) { throw new NullPointerException(); } final String protocol = url.getProtocol(); if ( "http".equals( protocol ) ) { return true; } if ( "https".equals( protocol ) ) { return true; } if ( "ftp".equals( protocol ) ) { return true; } if ( "file".equals( protocol ) ) { return true; } if ( "jar".equals( protocol ) ) { return true; } return false; } /** * Parses the given name and returns the name elements as List of Strings. * * @param name the name, that should be parsed. * @return the parsed name. */ private List<String> parseName( final String name ) { final ArrayList<String> list = new ArrayList<String>(); final StringTokenizer strTok = new StringTokenizer( name, "/" ); while ( strTok.hasMoreElements() ) { final String s = (String) strTok.nextElement(); if ( s.length() != 0 ) { list.add( s ); } } return list; } /** * Transforms the name list back into a single string, separated with "/". * * @param name the name list. * @param query the (optional) query for the URL. * @return the constructed name. */ private String formatName( final List name, final String query ) { final StringBuilder b = new StringBuilder( 128 ); final Iterator it = name.iterator(); while ( it.hasNext() ) { b.append( it.next() ); if ( it.hasNext() ) { b.append( '/' ); } } if ( query != null ) { b.append( '?' ); b.append( query ); } return b.toString(); } /** * Compares both name lists, and returns the last common index shared between the two lists. * * @param baseName the name created using the base url. * @param urlName the target url name. * @return the number of shared elements. */ private int startsWithUntil( final List baseName, final List urlName ) { final int minIdx = Math.min( urlName.size(), baseName.size() ); for ( int i = 0; i < minIdx; i++ ) { final String baseToken = (String) baseName.get( i ); final String urlToken = (String) urlName.get( i ); if ( !baseToken.equals( urlToken ) ) { return i; } } return minIdx; } /** * Checks, whether the URL points to the same service. A service is equal if the protocol, host and port are equal. * * @param url a url * @param baseUrl an other url, that should be compared. * @return true, if the urls point to the same host and port and use the same protocol, false otherwise. */ private boolean isSameService( final URL url, final URL baseUrl ) { if ( !url.getProtocol().equals( baseUrl.getProtocol() ) ) { return false; } if ( !url.getHost().equals( baseUrl.getHost() ) ) { return false; } if ( url.getPort() != baseUrl.getPort() ) { return false; } return true; } /** * Creates a relative url by stripping the common parts of the the url. If the baseFile denotes a directory, it must * end with a slash. * * @param targetFile the to be stripped url * @param baseFile the base url, to which the <code>url</code> is relative to. * @return the relative url, or the url unchanged, if there is no relation beween both URLs. */ public String createRelativePath( final String targetFile, final String baseFile ) { if ( targetFile == null ) { throw new NullPointerException( "targetFile must not be null." ); } if ( baseFile == null ) { throw new NullPointerException( "baseFile must not be null." ); } // If the URL contains a query, ignore that URL; do not // attemp to modify it... final List baseName = parseName( baseFile ); if ( baseName.isEmpty() ) { return targetFile; } final List<String> urlName = parseName( targetFile ); if ( urlName.isEmpty() ) { return targetFile; } if ( ( baseFile.length() > 0 && baseFile.charAt( baseFile.length() - 1 ) == '/' ) == false ) { // remove trailing slashes and ensure that the last element in baseName points to a directory baseName.remove( baseName.size() - 1 ); } // if both urls are identical, then return the plain file name... if ( baseFile.equals( targetFile ) ) { return urlName.get( urlName.size() - 1 ); } int commonIndex = startsWithUntil( urlName, baseName ); if ( commonIndex == 0 ) { return targetFile; } if ( commonIndex == urlName.size() ) { // correct the base index if there is some weird mapping // detected, // fi. the file url is fully included in the base url: // // base: /file/test/funnybase // file: /file/test // // this could be a valid configuration whereever virtual // mappings are allowed. commonIndex -= 1; } final ArrayList<String> retval = new ArrayList<String>(); if ( ( baseName.size() + 1 ) != urlName.size() ) { final int levels = baseName.size() - commonIndex; for ( int i = 0; i < levels; i++ ) { retval.add( ".." ); } } retval.addAll( urlName.subList( commonIndex, urlName.size() ) ); return formatName( retval, null ); } /** * Creates a relative url by stripping the common parts of the the url. If the base-URL denotes a directory, it must * end with a slash. * * @param url the to be stripped url * @param baseURL the base url, to which the <code>url</code> is relative to. * @return the relative url, or the url unchanged, if there is no relation beween both URLs. */ public String createRelativeURL( final URL url, final URL baseURL ) { if ( url == null ) { throw new NullPointerException( "content url must not be null." ); } if ( baseURL == null ) { throw new NullPointerException( "baseURL must not be null." ); } if ( isFileStyleProtocol( url ) && isSameService( url, baseURL ) ) { // If the URL contains a query, ignore that URL; do not // attemp to modify it... final List<String> urlName = parseName( getPath( url ) ); final List<String> baseName = parseName( getPath( baseURL ) ); final String query = getQuery( url ); if ( !isPath( baseURL ) ) { baseName.remove( baseName.size() - 1 ); } // if both urls are identical, then return the plain file name... if ( String.valueOf( url ).equals( String.valueOf( baseURL ) ) ) { return urlName.get( urlName.size() - 1 ); } int commonIndex = startsWithUntil( urlName, baseName ); if ( commonIndex == 0 ) { return url.toExternalForm(); } if ( commonIndex == urlName.size() ) { // correct the base index if there is some weird mapping // detected, // fi. the file url is fully included in the base url: // // base: /file/test/funnybase // file: /file/test // // this could be a valid configuration whereever virtual // mappings are allowed. commonIndex -= 1; } final ArrayList<String> retval = new ArrayList<String>(); if ( baseName.size() != urlName.size() ) { final int levels = baseName.size() - commonIndex; for ( int i = 0; i < levels; i++ ) { retval.add( ".." ); } } retval.addAll( urlName.subList( commonIndex, urlName.size() ) ); return formatName( retval, query ); } return url.toExternalForm(); } /** * Returns <code>true</code> if the URL represents a path, and <code>false</code> otherwise. * * @param baseURL the URL. * @return A boolean. */ private boolean isPath( final URL baseURL ) { final String path = getPath( baseURL ); if ( path.length() > 0 && path.charAt( path.length() - 1 ) == '/' ) { return true; } else if ( "file".equals( baseURL.getProtocol() ) ) { final File f = new File( path ); try { if ( f.isDirectory() ) { return true; } } catch ( SecurityException se ) { // ignored ... } } return false; } /** * Implements the JDK 1.3 method URL.getPath(). The path is defined as URL.getFile() minus the (optional) query. * * @param url the URL * @return the path */ private String getQuery( final URL url ) { final String file = url.getFile(); final int queryIndex = file.indexOf( '?' ); if ( queryIndex == -1 ) { return null; } return file.substring( queryIndex + 1 ); } /** * Implements the JDK 1.3 method URL.getPath(). The path is defined as URL.getFile() minus the (optional) query. * * @param url the URL * @return the path */ private String getPath( final URL url ) { final String file = url.getFile(); final int queryIndex = file.indexOf( '?' ); if ( queryIndex == -1 ) { return file; } return file.substring( 0, queryIndex ); } /** * Copies the InputStream into the OutputStream, until the end of the stream has been reached. This method uses a * buffer of 4096 kbyte. * * @param in the inputstream from which to read. * @param out the outputstream where the data is written to. * @throws java.io.IOException if a IOError occurs. */ public void copyStreams( final InputStream in, final OutputStream out ) throws IOException { copyStreams( in, out, 4096 ); } /** * Copies the InputStream into the OutputStream, until the end of the stream has been reached. * * @param in the inputstream from which to read. * @param out the outputstream where the data is written to. * @param buffersize the buffer size. * @throws java.io.IOException if a IOError occurs. */ public void copyStreams( final InputStream in, final OutputStream out, final int buffersize ) throws IOException { // create a 4kbyte buffer to read the file final byte[] bytes = new byte[ buffersize ]; // the input stream does not supply accurate available() data // the zip entry does not know the size of the data int bytesRead = in.read( bytes ); while ( bytesRead > -1 ) { out.write( bytes, 0, bytesRead ); bytesRead = in.read( bytes ); } } /** * Copies the contents of the Reader into the Writer, until the end of the stream has been reached. This method uses a * buffer of 4096 kbyte. * * @param in the reader from which to read. * @param out the writer where the data is written to. * @throws java.io.IOException if a IOError occurs. */ public void copyWriter( final Reader in, final Writer out ) throws IOException { copyWriter( in, out, 4096 ); } /** * Copies the contents of the Reader into the Writer, until the end of the stream has been reached. * * @param in the reader from which to read. * @param out the writer where the data is written to. * @param buffersize the buffer size. * @throws java.io.IOException if a IOError occurs. */ public void copyWriter( final Reader in, final Writer out, final int buffersize ) throws IOException { // create a 4kbyte buffer to read the file final char[] bytes = new char[ buffersize ]; // the input stream does not supply accurate available() data // the zip entry does not know the size of the data int bytesRead = in.read( bytes ); while ( bytesRead > -1 ) { out.write( bytes, 0, bytesRead ); bytesRead = in.read( bytes ); } } /** * Reads the given number of bytes into the target array. This method does not return until all bytes are read. In * case a end-of-stream is reached, the method throws an Exception. * * @param in the inputstream from where to read. * @param data the array where to store the data. * @param offset the offset in the array where to store the data. * @param length the number of bytes to be read. * @throws IOException if an IO error occured or the End of the stream has been reached. */ public void readFully( final InputStream in, final byte[] data, final int offset, final int length ) throws IOException { int bytesToRead = length; int bytesRead = 0; do { final int size = in.read( data, offset + bytesRead, bytesToRead ); if ( size == -1 ) { throw new IOException( "End-Of-File reached" ); } bytesToRead = bytesToRead - size; bytesRead += size; } while ( bytesToRead > 0 ); } /** * Reads the given number of bytes into the target array. This method does not return until all bytes are read. In * case a end-of-stream is reached, the method throws an Exception. * * @param in the inputstream from where to read. * @param data the array where to store the data. * @param offset the offset in the array where to store the data. * @param length the number of bytes to be read. * @throws IOException if an IO error occured or the End of the stream has been reached. */ public int readSafely( final InputStream in, final byte[] data, final int offset, final int length ) throws IOException { int bytesToRead = length; int bytesRead = 0; do { final int size = in.read( data, offset + bytesRead, bytesToRead ); if ( size == -1 ) { return bytesRead; } bytesToRead = bytesToRead - size; bytesRead += size; } while ( bytesToRead > 0 ); // end of file reached .. return 0; } /** * Extracts the file name from the URL. * * @param url the url. * @return the extracted filename. */ public String getFileName( final URL url ) { final String fileRaw = url.getFile(); final int query = fileRaw.lastIndexOf( '?' ); final String file; if ( query == -1 ) { file = fileRaw; } else { file = fileRaw.substring( 0, query ); } // Now the processing is the same as if it is a string return getFileName( file ); } /** * Extracts the last file name from the given pathname. * * @param path the path name. * @return the extracted filename. */ public String getFileName( final String path ) { // Check for slash and backslash final int last = Math.max( path.lastIndexOf( '/' ), path.lastIndexOf( '\\' ) ); if ( last < 0 ) { return path; } return path.substring( last + 1 ); } /** * Removes the file extension from the given file name. * * @param file the file name. * @return the file name without the file extension. */ public String stripFileExtension( final String file ) { final int idx = file.lastIndexOf( '.' ); // handles unix hidden files and files without an extension. if ( idx < 1 ) { return file; } return file.substring( 0, idx ); } /** * Returns the file extension of the given file name. The returned value will contain the dot. * * @param file the file name. * @return the file extension. */ public String getFileExtension( final String file ) { final int idx = file.lastIndexOf( '.' ); // handles unix hidden files and files without an extension. if ( idx < 1 ) { return ""; } return file.substring( idx ); } /** * Checks, whether the child directory is a subdirectory of the base directory. * * @param base the base directory. * @param child the suspected child directory. * @return true, if the child is a subdirectory of the base directory. * @throws java.io.IOException if an IOError occured during the test. */ public boolean isSubDirectory( File base, File child ) throws IOException { base = base.getCanonicalFile(); child = child.getCanonicalFile(); File parentFile = child; while ( parentFile != null ) { if ( base.equals( parentFile ) ) { return true; } parentFile = parentFile.getParentFile(); } return false; } /** * Returns a reference to a file with the specified name that is located somewhere on the classpath. The code for * this method is an adaptation of code supplied by Dave Postill. * * @param name the filename. * @return a reference to a file or <code>null</code> if no file could be found. * @throws SecurityException if access to the system properties or filesystem is forbidden. * @noinspection AccessOfSystemProperties */ public File findFileOnClassPath( final String name ) throws SecurityException { final String classpath = System.getProperty( "java.class.path" ); final String pathSeparator = System.getProperty( "path.separator" ); final StringTokenizer tokenizer = new StringTokenizer( classpath, pathSeparator ); while ( tokenizer.hasMoreTokens() ) { final String pathElement = tokenizer.nextToken(); final File directoryOrJar = new File( pathElement ); final File absoluteDirectoryOrJar = directoryOrJar.getAbsoluteFile(); if ( absoluteDirectoryOrJar.isFile() ) { final File target = new File( absoluteDirectoryOrJar.getParent(), name ); if ( target.exists() ) { return target; } } else { final File target = new File( directoryOrJar, name ); if ( target.exists() ) { return target; } } } return null; } /** * Computes the absolute filename for the target file using the baseFile as root directory. If the baseFile is null or * empty, the target file will be normalized (all navigation elements like ".." are removed). * * @param targetFile the target file name. * @param baseFile the base file (can be null). * @return the absolute path. */ public String getAbsolutePath( final String targetFile, final String baseFile ) { if ( targetFile == null ) { throw new NullPointerException( "targetFile must not be null." ); } if ( baseFile == null || ( baseFile != null && baseFile.isEmpty() ) ) { return stripNavigationPaths( targetFile ); } if ( targetFile.length() > 0 && targetFile.charAt( 0 ) == '/' ) { return stripNavigationPaths( targetFile.substring( 1 ) ); } final List<String> baseName = parseName( baseFile ); if ( baseName.isEmpty() ) { return stripNavigationPaths( targetFile ); } final List urlName = parseName( targetFile ); if ( urlName.isEmpty() ) { return stripNavigationPaths( baseFile ); } if ( ( baseFile.length() > 0 && baseFile.charAt( baseFile.length() - 1 ) == '/' ) == false ) { // trailing slashes indicate directory, // so remove last entry if the basefile name does not end with a slash (ie it points to a file) baseName.remove( baseName.size() - 1 ); if ( baseName.isEmpty() ) { return stripNavigationPaths( targetFile ); } } for ( int i = 0; i < urlName.size(); i++ ) { final String pathElement = (String) urlName.get( i ); if ( ( pathElement != null && pathElement.isEmpty() ) || pathElement == null ) { continue; } if ( ".".equals( pathElement ) ) { continue; } if ( "..".equals( pathElement ) ) { if ( baseName.isEmpty() == false ) { baseName.remove( baseName.size() - 1 ); } continue; } baseName.add( pathElement ); } final String s = formatName( baseName, null ); if ( targetFile.length() > 0 && targetFile.charAt( targetFile.length() - 1 ) == '/' ) { return s + '/'; } return s; } /** * Normalizes the given pathname. * * @param targetFile the target file to be normalized, never null. * @return the normalized filename. */ private String stripNavigationPaths( final String targetFile ) { final List<String> list = parseName( targetFile ); final int capacity = list.size(); final List<String> path = new ArrayList<String>( capacity ); for ( int i = 0; i < capacity; i++ ) { final String pathElement = list.get( i ); if ( ( pathElement != null && pathElement.isEmpty() ) || pathElement == null ) { continue; } if ( ".".equals( pathElement ) ) { continue; } if ( "..".equals( pathElement ) ) { if ( path.isEmpty() == false ) { path.remove( path.size() - 1 ); } continue; } path.add( pathElement ); } final String s = formatName( path, null ); if ( targetFile.length() > 0 && targetFile.charAt( targetFile.length() - 1 ) == '/' ) { return s + '/'; } return s; } /** * Returns the path-portion of the given path (anything before the last slash or backslash) or an empty string. * * @param path the path or filename from where to extract the path name. * @return the extracted path or a empty string. */ public String getPath( final String path ) { // Check for slash and backslash final int last = Math.max( path.lastIndexOf( '/' ), path.lastIndexOf( '\\' ) ); if ( last < 0 ) { return ""; } return path.substring( 0, last ); } /** * Converts a SQL-Clob object into a String. If the Clob is larger than 2^31 characters, we cannot convert it. If * there are errors converting it, this method will log the cause and return null. * * @param clob the clob to be read as string. * @return the string or null in case of errors. */ public String readClob( final Clob clob ) throws IOException, SQLException { final long length = clob.length(); if ( length > Integer.MAX_VALUE ) { logger.warn( "This CLOB contains more than 2^31 characters. We cannot handle that." ); throw new IOException( "This CLOB contains more than 2^31 characters. We cannot handle that." ); } final Reader inStream = clob.getCharacterStream(); final MemoryStringWriter outStream = new MemoryStringWriter( (int) length, 65536 ); try { IOUtils.getInstance().copyWriter( inStream, outStream ); } finally { try { inStream.close(); } catch ( IOException e ) { logger.warn( "Failed to close input stream. No worries, we will be alright anyway.", e ); } } return outStream.toString(); } /** * Converts a SQL-Clob object into a String. If the Clob is larger than 2^31 characters, we cannot convert it. If * there are errors converting it, this method will log the cause and return null. * * @param clob the clob to be read as string. * @return the string or null in case of errors. */ public byte[] readBlob( final Blob clob ) throws IOException, SQLException { final long length = clob.length(); if ( length > Integer.MAX_VALUE ) { logger.warn( "This CLOB contains more than 2^31 characters. We cannot handle that." ); throw new IOException( "This BLOB contains more than 2^31 characters. We cannot handle that." ); } final InputStream inStream = clob.getBinaryStream(); final MemoryByteArrayOutputStream outStream = new MemoryByteArrayOutputStream( (int) length, 65536 ); try { IOUtils.getInstance().copyStreams( inStream, outStream ); } finally { try { inStream.close(); } catch ( IOException e ) { logger.warn( "Failed to close input stream. No worries, we will be alright anyway.", e ); } } return outStream.toByteArray(); } }