org.pentaho.reporting.libraries.base.util.IOUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.pentaho.reporting.libraries.base.util.IOUtils.java

Source

/*!
* 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-2017 Hitachi Vantara..  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();
    }
}