    001/* ========================================================================
    002 * JCommon : a free general purpose class library for the Java(tm) platform
    003 * ========================================================================
    004 *
    005 * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
    006 *
    007 * Project Info:  http://www.jfree.org/jcommon/index.html
    008 *
    009 * This library is free software; you can redistribute it and/or modify it
    010 * under the terms of the GNU Lesser General Public License as published by
    011 * the Free Software Foundation; either version 2.1 of the License, or
    012 * (at your option) any later version.
    013 *
    014 * This library is distributed in the hope that it will be useful, but
    015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
    017 * License for more details.
    018 *
    019 * You should have received a copy of the GNU Lesser General Public
    020 * License along with this library; if not, write to the Free Software
    021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
    022 * USA.
    023 *
    024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
    025 * in the United States and other countries.]
    026 *
    027 * ------------
    028 * IOUtils.java
    029 * ------------
    030 * (C)opyright 2002-2004, by Thomas Morgner and Contributors.
    031 *
    032 * Original Author:  Thomas Morgner;
    033 * Contributor(s):   David Gilbert (for Object Refinery Limited);
    034 *
    035 * $Id: IOUtils.java,v 1.8 2009/01/22 08:34:58 taqua Exp $
    036 *
    037 * Changes
    038 * -------
    039 * 26-Jan-2003 : Initial version
    040 * 23-Feb-2003 : Documentation
    041 * 25-Feb-2003 : Fixed Checkstyle issues (DG);
    042 * 29-Apr-2003 : Moved to jcommon
    043 * 04-Jan-2004 : Fixed JDK 1.2.2 issues with createRelativeURL;
    044 *               added support for query strings within these urls (TM);
    045 */
    046
    047package org.jfree.io;
    048
    049import java.io.File;
    050import java.io.IOException;
    051import java.io.InputStream;
    052import java.io.OutputStream;
    053import java.io.Reader;
    054import java.io.Writer;
    055import java.net.URL;
    056import java.util.ArrayList;
    057import java.util.Iterator;
    058import java.util.List;
    059import java.util.StringTokenizer;
    060
    061/**
    062 * The IOUtils provide some IO related helper methods.
    063 *
    064 * @author Thomas Morgner.
    065 */
    066public class IOUtils {
    067
    068    /** the singleton instance of the utility package. */
    069    private static IOUtils instance;
    070
    071    /**
    072     * DefaultConstructor.
    073     */
    074    private IOUtils() {
    075    }
    076
    077    /**
    078     * Gets the singleton instance of the utility package.
    079     *
    080     * @return the singleton instance.
    081     */
    082    public static IOUtils getInstance() {
    083        if (instance == null) {
    084            instance = new IOUtils();
    085        }
    086        return instance;
    087    }
    088
    089    /**
    090     * Checks, whether the URL uses a file based protocol.
    091     *
    092     * @param url the url.
    093     * @return true, if the url is file based.
    094     */
    095    private boolean isFileStyleProtocol(final URL url) {
    096        if (url.getProtocol().equals("http")) {
    097            return true;
    098        }
    099        if (url.getProtocol().equals("https")) {
    100            return true;
    101        }
    102        if (url.getProtocol().equals("ftp")) {
    103            return true;
    104        }
    105        if (url.getProtocol().equals("file")) {
    106            return true;
    107        }
    108        if (url.getProtocol().equals("jar")) {
    109            return true;
    110        }
    111        return false;
    112    }
    113
    114    /**
    115     * Parses the given name and returns the name elements as List of Strings.
    116     *
    117     * @param name the name, that should be parsed.
    118     * @return the parsed name.
    119     */
    120    private List parseName(final String name) {
    121        final ArrayList list = new ArrayList();
    122        final StringTokenizer strTok = new StringTokenizer(name, "/");
    123        while (strTok.hasMoreElements()) {
    124            final String s = (String) strTok.nextElement();
    125            if (s.length() != 0) {
    126                list.add(s);
    127            }
    128        }
    129        return list;
    130    }
    131
    132    /**
    133     * Transforms the name list back into a single string, separated with "/".
    134     *
    135     * @param name the name list.
    136     * @param query the (optional) query for the URL.
    137     * @return the constructed name.
    138     */
    139    private String formatName(final List name, final String query) {
    140        final StringBuffer b = new StringBuffer();
    141        final Iterator it = name.iterator();
    142        while (it.hasNext()) {
    143            b.append(it.next());
    144            if (it.hasNext()) {
    145                b.append("/");
    146            }
    147        }
    148        if (query != null) {
    149            b.append('?');
    150            b.append(query);
    151        }
    152        return b.toString();
    153    }
    154
    155    /**
    156     * Compares both name lists, and returns the last common index shared
    157     * between the two lists.
    158     *
    159     * @param baseName the name created using the base url.
    160     * @param urlName  the target url name.
    161     * @return the number of shared elements.
    162     */
    163    private int startsWithUntil(final List baseName, final List urlName) {
    164        final int minIdx = Math.min(urlName.size(), baseName.size());
    165        for (int i = 0; i < minIdx; i++) {
    166            final String baseToken = (String) baseName.get(i);
    167            final String urlToken = (String) urlName.get(i);
    168            if (!baseToken.equals(urlToken)) {
    169                return i;
    170            }
    171        }
    172        return minIdx;
    173    }
    174
    175    /**
    176     * Checks, whether the URL points to the same service. A service is equal
    177     * if the protocol, host and port are equal.
    178     *
    179     * @param url a url
    180     * @param baseUrl an other url, that should be compared.
    181     * @return true, if the urls point to the same host and port and use the
    182     *         same protocol, false otherwise.
    183     */
    184    private boolean isSameService(final URL url, final URL baseUrl) {
    185        if (!url.getProtocol().equals(baseUrl.getProtocol())) {
    186            return false;
    187        }
    188        if (!url.getHost().equals(baseUrl.getHost())) {
    189            return false;
    190        }
    191        if (url.getPort() != baseUrl.getPort()) {
    192            return false;
    193        }
    194        return true;
    195    }
    196
    197    /**
    198     * Creates a relative url by stripping the common parts of the the url.
    199     *
    200     * @param url the to be stripped url
    201     * @param baseURL the base url, to which the <code>url</code> is relative
    202     *                to.
    203     * @return the relative url, or the url unchanged, if there is no relation
    204     * beween both URLs.
    205     */
    206    public String createRelativeURL(final URL url, final URL baseURL) {
    207        if (url == null) {
    208            throw new NullPointerException("content url must not be null.");
    209        }
    210        if (baseURL == null) {
    211            throw new NullPointerException("baseURL must not be null.");
    212        }
    213        if (isFileStyleProtocol(url) && isSameService(url, baseURL)) {
    214
    215            // If the URL contains a query, ignore that URL; do not
    216            // attemp to modify it...
    217            final List urlName = parseName(getPath(url));
    218            final List baseName = parseName(getPath(baseURL));
    219            final String query = getQuery(url);
    220
    221            if (!isPath(baseURL)) {
    222                baseName.remove(baseName.size() - 1);
    223            }
    224
    225            // if both urls are identical, then return the plain file name...
    226            if (url.equals(baseURL)) {
    227                return (String) urlName.get(urlName.size() - 1);
    228            }
    229
    230            int commonIndex = startsWithUntil(urlName, baseName);
    231            if (commonIndex == 0) {
    232                return url.toExternalForm();
    233            }
    234
    235            if (commonIndex == urlName.size()) {
    236                // correct the base index if there is some weird mapping
    237                // detected,
    238                // fi. the file url is fully included in the base url:
    239                //
    240                // base: /file/test/funnybase
    241                // file: /file/test
    242                //
    243                // this could be a valid configuration whereever virtual
    244                // mappings are allowed.
    245                commonIndex -= 1;
    246            }
    247
    248            final ArrayList retval = new ArrayList();
    249            if (baseName.size() >= urlName.size()) {
    250                final int levels = baseName.size() - commonIndex;
    251                for (int i = 0; i < levels; i++) {
    252                    retval.add("..");
    253                }
    254            }
    255
    256            retval.addAll(urlName.subList(commonIndex, urlName.size()));
    257            return formatName(retval, query);
    258        }
    259        return url.toExternalForm();
    260    }
    261
    262    /**
    263     * Returns <code>true</code> if the URL represents a path, and
    264     * <code>false</code> otherwise.
    265     *
    266     * @param baseURL  the URL.
    267     *
    268     * @return A boolean.
    269     */
    270    private boolean isPath(final URL baseURL) {
    271        if (getPath(baseURL).endsWith("/")) {
    272            return true;
    273        }
    274        else if (baseURL.getProtocol().equals("file")) {
    275            final File f = new File(getPath(baseURL));
    276            try {
    277                if (f.isDirectory()) {
    278                    return true;
    279                }
    280            }
    281            catch (SecurityException se) {
    282                // ignored ...
    283            }
    284        }
    285        return false;
    286    }
    287
    288    /**
    289     * Implements the JDK 1.3 method URL.getPath(). The path is defined
    290     * as URL.getFile() minus the (optional) query.
    291     *
    292     * @param url the URL
    293     * @return the path
    294     */
    295    private String getQuery (final URL url) {
    296        final String file = url.getFile();
    297        final int queryIndex = file.indexOf('?');
    298        if (queryIndex == -1) {
    299            return null;
    300        }
    301        return file.substring(queryIndex + 1);
    302    }
    303
    304    /**
    305     * Implements the JDK 1.3 method URL.getPath(). The path is defined
    306     * as URL.getFile() minus the (optional) query.
    307     *
    308     * @param url the URL
    309     * @return the path
    310     */
    311    private String getPath (final URL url) {
    312        final String file = url.getFile();
    313        final int queryIndex = file.indexOf('?');
    314        if (queryIndex == -1) {
    315            return file;
    316        }
    317        return file.substring(0, queryIndex);
    318    }
    319
    320    /**
    321     * Copies the InputStream into the OutputStream, until the end of the stream
    322     * has been reached. This method uses a buffer of 4096 kbyte.
    323     *
    324     * @param in the inputstream from which to read.
    325     * @param out the outputstream where the data is written to.
    326     * @throws IOException if a IOError occurs.
    327     */
    328    public void copyStreams(final InputStream in, final OutputStream out)
    329        throws IOException {
    330        copyStreams(in, out, 4096);
    331    }
    332
    333    /**
    334     * Copies the InputStream into the OutputStream, until the end of the stream
    335     * has been reached.
    336     *
    337     * @param in the inputstream from which to read.
    338     * @param out the outputstream where the data is written to.
    339     * @param buffersize the buffer size.
    340     * @throws IOException if a IOError occurs.
    341     */
    342    public void copyStreams(final InputStream in, final OutputStream out,
    343            final int buffersize) throws IOException {
    344        // create a 4kbyte buffer to read the file
    345        final byte[] bytes = new byte[buffersize];
    346
    347        // the input stream does not supply accurate available() data
    348        // the zip entry does not know the size of the data
    349        int bytesRead = in.read(bytes);
    350        while (bytesRead > -1) {
    351            out.write(bytes, 0, bytesRead);
    352            bytesRead = in.read(bytes);
    353        }
    354    }
    355
    356    /**
    357     * Copies the contents of the Reader into the Writer, until the end of the
    358     * stream has been reached. This method uses a buffer of 4096 kbyte.
    359     *
    360     * @param in the reader from which to read.
    361     * @param out the writer where the data is written to.
    362     * @throws IOException if a IOError occurs.
    363     */
    364    public void copyWriter(final Reader in, final Writer out)
    365        throws IOException {
    366        copyWriter(in, out, 4096);
    367    }
    368
    369    /**
    370     * Copies the contents of the Reader into the Writer, until the end of the
    371     * stream has been reached.
    372     *
    373     * @param in  the reader from which to read.
    374     * @param out  the writer where the data is written to.
    375     * @param buffersize  the buffer size.
    376     *
    377     * @throws IOException if a IOError occurs.
    378     */
    379    public void copyWriter(final Reader in, final Writer out,
    380            final int buffersize)
    381        throws IOException {
    382        // create a 4kbyte buffer to read the file
    383        final char[] bytes = new char[buffersize];
    384
    385        // the input stream does not supply accurate available() data
    386        // the zip entry does not know the size of the data
    387        int bytesRead = in.read(bytes);
    388        while (bytesRead > -1) {
    389            out.write(bytes, 0, bytesRead);
    390            bytesRead = in.read(bytes);
    391        }
    392    }
    393
    394    /**
    395     * Extracts the file name from the URL.
    396     *
    397     * @param url the url.
    398     * @return the extracted filename.
    399     */
    400    public String getFileName(final URL url) {
    401        final String file = getPath(url);
    402        final int last = file.lastIndexOf("/");
    403        if (last < 0) {
    404            return file;
    405        }
    406        return file.substring(last + 1);
    407    }
    408
    409    /**
    410     * Removes the file extension from the given file name.
    411     *
    412     * @param file the file name.
    413     * @return the file name without the file extension.
    414     */
    415    public String stripFileExtension(final String file) {
    416        final int idx = file.lastIndexOf(".");
    417        // handles unix hidden files and files without an extension.
    418        if (idx < 1) {
    419            return file;
    420        }
    421        return file.substring(0, idx);
    422    }
    423
    424    /**
    425     * Returns the file extension of the given file name.
    426     * The returned value will contain the dot.
    427     *
    428     * @param file the file name.
    429     * @return the file extension.
    430     */
    431    public String getFileExtension(final String file) {
    432        final int idx = file.lastIndexOf(".");
    433        // handles unix hidden files and files without an extension.
    434        if (idx < 1) {
    435            return "";
    436        }
    437        return file.substring(idx);
    438    }
    439
    440    /**
    441     * Checks, whether the child directory is a subdirectory of the base
    442     * directory.
    443     *
    444     * @param base the base directory.
    445     * @param child the suspected child directory.
    446     * @return true, if the child is a subdirectory of the base directory.
    447     * @throws IOException if an IOError occured during the test.
    448     */
    449    public boolean isSubDirectory(File base, File child)
    450        throws IOException {
    451        base = base.getCanonicalFile();
    452        child = child.getCanonicalFile();
    453
    454        File parentFile = child;
    455        while (parentFile != null) {
    456            if (base.equals(parentFile)) {
    457                return true;
    458            }
    459            parentFile = parentFile.getParentFile();
    460        }
    461        return false;
    462    }
    463}