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}