001/*
002 * ModeShape (http://www.modeshape.org)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *       http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.modeshape.common.util;
017
018import java.io.BufferedInputStream;
019import java.io.ByteArrayOutputStream;
020import java.io.Closeable;
021import java.io.File;
022import java.io.FileInputStream;
023import java.io.FileNotFoundException;
024import java.io.FileOutputStream;
025import java.io.FileReader;
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.InputStreamReader;
029import java.io.OutputStream;
030import java.io.Reader;
031import java.io.Writer;
032import java.net.MalformedURLException;
033import java.net.URL;
034import java.util.Arrays;
035import org.modeshape.common.annotation.Immutable;
036import org.modeshape.common.logging.Logger;
037
038/**
039 * A set of utilities for more easily performing I/O.
040 */
041@Immutable
042public class IoUtil {
043
044    private static final Logger LOGGER = Logger.getLogger(IoUtil.class);
045
046    /**
047     * Read and return the entire contents of the supplied {@link InputStream stream}. This method always closes the stream when
048     * finished reading.
049     * 
050     * @param stream the stream to the contents; may be null
051     * @return the contents, or an empty byte array if the supplied reader is null
052     * @throws IOException if there is an error reading the content
053     */
054    public static byte[] readBytes( InputStream stream ) throws IOException {
055        if (stream == null) return new byte[] {};
056        byte[] buffer = new byte[1024];
057        ByteArrayOutputStream output = new ByteArrayOutputStream();
058        boolean error = false;
059        try {
060            int numRead = 0;
061            while ((numRead = stream.read(buffer)) > -1) {
062                output.write(buffer, 0, numRead);
063            }
064        } catch (IOException e) {
065            error = true; // this error should be thrown, even if there is an error closing stream
066            throw e;
067        } catch (RuntimeException e) {
068            error = true; // this error should be thrown, even if there is an error closing stream
069            throw e;
070        } finally {
071            try {
072                stream.close();
073            } catch (IOException e) {
074                if (!error) throw e;
075            }
076        }
077        output.flush();
078        return output.toByteArray();
079    }
080
081    /**
082     * Read and return the entire contents of the supplied {@link File file}.
083     * 
084     * @param file the file containing the contents; may be null
085     * @return the contents, or an empty byte array if the supplied file is null
086     * @throws IOException if there is an error reading the content
087     */
088    public static byte[] readBytes( File file ) throws IOException {
089        if (file == null) return new byte[] {};
090        InputStream stream = new BufferedInputStream(new FileInputStream(file));
091        boolean error = false;
092        try {
093            return readBytes(stream);
094        } catch (IOException e) {
095            error = true; // this error should be thrown, even if there is an error closing stream
096            throw e;
097        } catch (RuntimeException e) {
098            error = true; // this error should be thrown, even if there is an error closing stream
099            throw e;
100        } finally {
101            try {
102                stream.close();
103            } catch (IOException e) {
104                if (!error) throw e;
105            }
106        }
107    }
108
109    /**
110     * Read and return the entire contents of the supplied {@link Reader}. This method always closes the reader when finished
111     * reading.
112     * 
113     * @param reader the reader of the contents; may be null
114     * @return the contents, or an empty string if the supplied reader is null
115     * @throws IOException if there is an error reading the content
116     */
117    public static String read( Reader reader ) throws IOException {
118        if (reader == null) return "";
119        StringBuilder sb = new StringBuilder();
120        boolean error = false;
121        try {
122            int numRead = 0;
123            char[] buffer = new char[1024];
124            while ((numRead = reader.read(buffer)) > -1) {
125                sb.append(buffer, 0, numRead);
126            }
127        } catch (IOException e) {
128            error = true; // this error should be thrown, even if there is an error closing reader
129            throw e;
130        } catch (RuntimeException e) {
131            error = true; // this error should be thrown, even if there is an error closing reader
132            throw e;
133        } finally {
134            try {
135                reader.close();
136            } catch (IOException e) {
137                if (!error) throw e;
138            }
139        }
140        return sb.toString();
141    }
142
143    /**
144     * Read and return the entire contents of the supplied {@link InputStream}. This method always closes the stream when finished
145     * reading.
146     * 
147     * @param stream the streamed contents; may be null
148     * @return the contents, or an empty string if the supplied stream is null
149     * @throws IOException if there is an error reading the content
150     */
151    public static String read( InputStream stream ) throws IOException {
152        return stream == null ? "" : read(new InputStreamReader(stream));
153    }
154
155    /**
156     * Read and return the entire contents of the supplied {@link InputStream}. This method always closes the stream when finished
157     * reading.
158     * 
159     * @param stream the streamed contents; may be null
160     * @param charset charset of the stream data; may not be null
161     * @return the contents, or an empty string if the supplied stream is null
162     * @throws IOException if there is an error reading the content
163     */
164    public static String read( InputStream stream,
165                               String charset ) throws IOException {
166        return stream == null ? "" : read(new InputStreamReader(stream, charset));
167    }
168
169    /**
170     * Read and return the entire contents of the supplied {@link File}.
171     * 
172     * @param file the file containing the information to be read; may be null
173     * @return the contents, or an empty string if the supplied reader is null
174     * @throws IOException if there is an error reading the content
175     */
176    public static String read( File file ) throws IOException {
177        if (file == null) return "";
178        StringBuilder sb = new StringBuilder();
179        boolean error = false;
180        Reader reader = new FileReader(file);
181        try {
182            int numRead = 0;
183            char[] buffer = new char[1024];
184            while ((numRead = reader.read(buffer)) > -1) {
185                sb.append(buffer, 0, numRead);
186            }
187        } catch (IOException e) {
188            error = true; // this error should be thrown, even if there is an error closing reader
189            throw e;
190        } catch (RuntimeException e) {
191            error = true; // this error should be thrown, even if there is an error closing reader
192            throw e;
193        } finally {
194            try {
195                reader.close();
196            } catch (IOException e) {
197                if (!error) throw e;
198            }
199        }
200        return sb.toString();
201    }
202
203    /**
204     * Write the entire contents of the supplied string to the given file.
205     * 
206     * @param content the content to write to the stream; may be null
207     * @param file the file to which the content is to be written
208     * @throws IOException
209     * @throws IllegalArgumentException if the stream is null
210     */
211    public static void write( String content,
212                              File file ) throws IOException {
213        CheckArg.isNotNull(file, "destination file");
214        if (content != null) {
215            write(content, new FileOutputStream(file));
216        }
217    }
218
219    /**
220     * Write the entire contents of the supplied string to the given stream. This method always flushes and closes the stream when
221     * finished.
222     * 
223     * @param content the content to write to the stream; may be null
224     * @param stream the stream to which the content is to be written
225     * @throws IOException
226     * @throws IllegalArgumentException if the stream is null
227     */
228    public static void write( String content,
229                              OutputStream stream ) throws IOException {
230        CheckArg.isNotNull(stream, "destination stream");
231        boolean error = false;
232        try {
233            if (content != null) {
234                byte[] bytes = content.getBytes();
235                stream.write(bytes, 0, bytes.length);
236            }
237        } catch (IOException e) {
238            error = true; // this error should be thrown, even if there is an error flushing/closing stream
239            throw e;
240        } catch (RuntimeException e) {
241            error = true; // this error should be thrown, even if there is an error flushing/closing stream
242            throw e;
243        } finally {
244            try {
245                stream.flush();
246            } catch (IOException e) {
247                if (!error) throw e;
248            } finally {
249                try {
250                    stream.close();
251                } catch (IOException e) {
252                    if (!error) throw e;
253                }
254            }
255        }
256    }
257
258    /**
259     * Write the entire contents of the supplied string to the given writer. This method always flushes and closes the writer when
260     * finished.
261     * 
262     * @param content the content to write to the writer; may be null
263     * @param writer the writer to which the content is to be written
264     * @throws IOException
265     * @throws IllegalArgumentException if the writer is null
266     */
267    public static void write( String content,
268                              Writer writer ) throws IOException {
269        CheckArg.isNotNull(writer, "destination writer");
270        boolean error = false;
271        try {
272            if (content != null) {
273                writer.write(content);
274            }
275        } catch (IOException e) {
276            error = true; // this error should be thrown, even if there is an error flushing/closing writer
277            throw e;
278        } catch (RuntimeException e) {
279            error = true; // this error should be thrown, even if there is an error flushing/closing writer
280            throw e;
281        } finally {
282            try {
283                writer.flush();
284            } catch (IOException e) {
285                if (!error) throw e;
286            } finally {
287                try {
288                    writer.close();
289                } catch (IOException e) {
290                    if (!error) throw e;
291                }
292            }
293        }
294    }
295
296    /**
297     * Write the entire contents of the supplied string to the given stream. This method always flushes and closes the stream when
298     * finished.
299     * 
300     * @param input the content to write to the stream; may be null
301     * @param stream the stream to which the content is to be written
302     * @throws IOException
303     * @throws IllegalArgumentException if the stream is null
304     */
305    public static void write( InputStream input,
306                              OutputStream stream ) throws IOException {
307        write(input, stream, 1024);
308    }
309
310    /**
311     * Write the entire contents of the supplied string to the given stream. This method always flushes and closes the stream when
312     * finished.
313     * 
314     * @param input the content to write to the stream; may be null
315     * @param stream the stream to which the content is to be written
316     * @param bufferSize the size of the buffer; must be positive
317     * @throws IOException
318     * @throws IllegalArgumentException if the stream is null
319     */
320    public static void write( InputStream input,
321                              OutputStream stream,
322                              int bufferSize ) throws IOException {
323        CheckArg.isNotNull(stream, "destination stream");
324        CheckArg.isPositive(bufferSize, "bufferSize");
325        boolean error = false;
326        try {
327            if (input != null) {
328                byte[] buffer = new byte[bufferSize];
329                try {
330                    int numRead = 0;
331                    while ((numRead = input.read(buffer)) > -1) {
332                        stream.write(buffer, 0, numRead);
333                    }
334                } finally {
335                    input.close();
336                }
337            }
338        } catch (IOException e) {
339            error = true; // this error should be thrown, even if there is an error flushing/closing stream
340            throw e;
341        } catch (RuntimeException e) {
342            error = true; // this error should be thrown, even if there is an error flushing/closing stream
343            throw e;
344        } finally {
345            try {
346                stream.flush();
347            } catch (IOException e) {
348                if (!error) throw e;
349            } finally {
350                try {
351                    stream.close();
352                } catch (IOException e) {
353                    if (!error) throw e;
354                }
355            }
356        }
357    }
358
359    /**
360     * Write the entire contents of the supplied string to the given writer. This method always flushes and closes the writer when
361     * finished.
362     * 
363     * @param input the content to write to the writer; may be null
364     * @param writer the writer to which the content is to be written
365     * @throws IOException
366     * @throws IllegalArgumentException if the writer is null
367     */
368    public static void write( Reader input,
369                              Writer writer ) throws IOException {
370        CheckArg.isNotNull(writer, "destination writer");
371        boolean error = false;
372        try {
373            if (input != null) {
374                char[] buffer = new char[1024];
375                try {
376                    int numRead = 0;
377                    while ((numRead = input.read(buffer)) > -1) {
378                        writer.write(buffer, 0, numRead);
379                    }
380                } finally {
381                    input.close();
382                }
383            }
384        } catch (IOException e) {
385            error = true; // this error should be thrown, even if there is an error flushing/closing writer
386            throw e;
387        } catch (RuntimeException e) {
388            error = true; // this error should be thrown, even if there is an error flushing/closing writer
389            throw e;
390        } finally {
391            try {
392                writer.flush();
393            } catch (IOException e) {
394                if (!error) throw e;
395            } finally {
396                try {
397                    writer.close();
398                } catch (IOException e) {
399                    if (!error) throw e;
400                }
401            }
402        }
403    }
404
405    /**
406     * Write the entire contents of the supplied string to the given stream. This method always flushes and closes the stream when
407     * finished.
408     * 
409     * @param input1 the first stream
410     * @param input2 the second stream
411     * @return true if the streams contain the same content, or false otherwise
412     * @throws IOException
413     * @throws IllegalArgumentException if the stream is null
414     */
415    public static boolean isSame( InputStream input1,
416                                  InputStream input2 ) throws IOException {
417        CheckArg.isNotNull(input1, "input1");
418        CheckArg.isNotNull(input2, "input2");
419        boolean error = false;
420        try {
421            byte[] buffer1 = new byte[1024];
422            byte[] buffer2 = new byte[1024];
423            try {
424                int numRead1 = 0;
425                int numRead2 = 0;
426                while (true) {
427                    numRead1 = input1.read(buffer1);
428                    numRead2 = input2.read(buffer2);
429                    if (numRead1 > -1) {
430                        if (numRead2 != numRead1) return false;
431                        // Otherwise same number of bytes read
432                        if (!Arrays.equals(buffer1, buffer2)) return false;
433                        // Otherwise same bytes read, so continue ...
434                    } else {
435                        // Nothing more in stream 1 ...
436                        return numRead2 < 0;
437                    }
438                }
439            } finally {
440                input1.close();
441            }
442        } catch (IOException e) {
443            error = true; // this error should be thrown, even if there is an error closing stream 2
444            throw e;
445        } catch (RuntimeException e) {
446            error = true; // this error should be thrown, even if there is an error closing stream 2
447            throw e;
448        } finally {
449            try {
450                input2.close();
451            } catch (IOException e) {
452                if (!error) throw e;
453            }
454        }
455    }
456
457    /**
458     * Get the {@link InputStream input stream} to the resource given by the supplied path. If a class loader is supplied, the
459     * method attempts to resolve the resource using the {@link ClassLoader#getResourceAsStream(String)} method; if the result is
460     * non-null, it is returned. Otherwise, if a class is supplied, this method attempts to resolve the resource using the
461     * {@link Class#getResourceAsStream(String)} method; if the result is non-null, it is returned. Otherwise, this method then
462     * uses the Class' ClassLoader to load the resource; if non-null, it is returned . Otherwise, this method looks for an
463     * existing and readable {@link File file} at the path; if found, a buffered stream to that file is returned. Otherwise, this
464     * method attempts to parse the resource path into a valid {@link URL}; if this succeeds, the method attempts to open a stream
465     * to that URL. If all of these fail, this method returns null.
466     * 
467     * @param resourcePath the logical path to the classpath, file, or URL resource
468     * @param clazz the class that should be used to load the resource as a stream; may be null
469     * @param classLoader the classloader that should be used to load the resource as a stream; may be null
470     * @return an input stream to the resource; or null if the resource could not be found
471     * @throws IllegalArgumentException if the resource path is null or empty
472     */
473    public static InputStream getResourceAsStream( String resourcePath,
474                                                   ClassLoader classLoader,
475                                                   Class<?> clazz ) {
476        CheckArg.isNotEmpty(resourcePath, "resourcePath");
477        InputStream result = null;
478        if (classLoader != null) {
479            // Try using the class loader first ...
480            result = classLoader.getResourceAsStream(resourcePath);
481        }
482        if (result == null && clazz != null) {
483            // Not yet found, so try the class ...
484            result = clazz.getResourceAsStream(resourcePath);
485            if (result == null) {
486                // Not yet found, so try the class's class loader ...
487                result = clazz.getClassLoader().getResourceAsStream(resourcePath);
488            }
489        }
490        if (result == null) {
491            // Still not found, so see if this is an existing File ...
492            try {
493                File file = new File(resourcePath);
494                if (file.exists() && file.canRead()) {
495                    return new BufferedInputStream(new FileInputStream(file));
496                }
497            } catch (FileNotFoundException e) {
498                // just continue ...
499            }
500        }
501        if (result == null) {
502            // Still not found, so try to construct a URL out of it ...
503            try {
504                URL url = new URL(resourcePath);
505                return url.openStream();
506            } catch (MalformedURLException e) {
507                // just continue ...
508            } catch (IOException err) {
509                // just continue ...
510            }
511        }
512        // Couldn't find it anywhere ...
513        return result;
514    }
515
516    /**
517     * Closes the closable silently. Any exceptions are ignored.
518     * 
519     * @param closeable the closeable instance; may be null
520     */
521    public static void closeQuietly( Closeable closeable ) {
522        if (closeable == null) {
523            return;
524        }
525        try {
526            closeable.close();
527        } catch (Throwable t) {
528            LOGGER.debug(t, "Ignored error at closing stream");
529        }
530    }
531
532    private IoUtil() {
533        // Prevent construction
534    }
535}