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.7 2008/11/06 13:56:15 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    
047    package org.jfree.io;
048    
049    import java.io.File;
050    import java.io.IOException;
051    import java.io.InputStream;
052    import java.io.OutputStream;
053    import java.io.Reader;
054    import java.io.Writer;
055    import java.net.URL;
056    import java.util.ArrayList;
057    import java.util.Iterator;
058    import java.util.List;
059    import java.util.StringTokenizer;
060    
061    /**
062     * The IOUtils provide some IO related helper methods.
063     *
064     * @author Thomas Morgner.
065     */
066    public 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 = url.getFile();
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    }