View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.util;
23  
24  import static org.opencastproject.util.PathSupport.path;
25  import static org.opencastproject.util.data.Either.left;
26  import static org.opencastproject.util.data.Either.right;
27  import static org.opencastproject.util.data.Option.none;
28  import static org.opencastproject.util.data.Option.option;
29  import static org.opencastproject.util.data.Option.some;
30  import static org.opencastproject.util.data.functions.Misc.chuck;
31  
32  import org.opencastproject.security.api.TrustedHttpClient;
33  import org.opencastproject.security.api.TrustedHttpClientException;
34  import org.opencastproject.util.data.Effect0;
35  import org.opencastproject.util.data.Either;
36  import org.opencastproject.util.data.Function;
37  import org.opencastproject.util.data.Function0;
38  import org.opencastproject.util.data.Function2;
39  import org.opencastproject.util.data.Option;
40  
41  import com.entwinemedia.fn.Fn;
42  import com.entwinemedia.fn.FnX;
43  import com.entwinemedia.fn.Prelude;
44  import com.entwinemedia.fn.Unit;
45  import com.google.common.io.Resources;
46  
47  import org.apache.commons.io.IOUtils;
48  import org.apache.http.HttpResponse;
49  import org.apache.http.client.methods.HttpGet;
50  import org.slf4j.Logger;
51  import org.slf4j.LoggerFactory;
52  
53  import java.io.ByteArrayInputStream;
54  import java.io.ByteArrayOutputStream;
55  import java.io.Closeable;
56  import java.io.DataInputStream;
57  import java.io.File;
58  import java.io.FileInputStream;
59  import java.io.FileNotFoundException;
60  import java.io.IOException;
61  import java.io.InputStream;
62  import java.io.ObjectInputStream;
63  import java.io.ObjectOutputStream;
64  import java.io.OutputStream;
65  import java.io.RandomAccessFile;
66  import java.io.Serializable;
67  import java.net.URISyntaxException;
68  import java.net.URL;
69  import java.nio.channels.FileLock;
70  import java.nio.charset.Charset;
71  import java.util.Properties;
72  
73  import de.schlichtherle.io.FileWriter;
74  
75  /**
76   * Contains operations concerning IO.
77   */
78  public final class IoSupport {
79  
80    /**
81     * the logging facility provided by log4j
82     */
83    private static Logger logger = LoggerFactory.getLogger(IoSupport.class.getName());
84  
85    public static String getSystemTmpDir() {
86      String tmpdir = System.getProperty("java.io.tmpdir");
87      if (tmpdir == null) {
88        tmpdir = File.separator + "tmp" + File.separator;
89      } else {
90        if (!tmpdir.endsWith(File.separator)) {
91          tmpdir += File.separator;
92        }
93      }
94      return tmpdir;
95    }
96  
97    private IoSupport() {
98    }
99  
100   /**
101    * Closes a <code>Closable</code> quietly so that no exceptions are thrown.
102    *
103    * @param s
104    *          maybe null
105    */
106   public static boolean closeQuietly(final Closeable s) {
107     if (s == null) {
108       return false;
109     }
110     try {
111       s.close();
112       return true;
113     } catch (IOException e) {
114       return false;
115     }
116   }
117 
118   /**
119    * Closes the processes input, output and error streams.
120    *
121    * @param process
122    *          the process
123    * @return <code>true</code> if the streams were closed
124    */
125   public static boolean closeQuietly(final Process process) {
126     if (process != null) {
127       closeQuietly(process.getInputStream());
128       closeQuietly(process.getErrorStream());
129       closeQuietly(process.getOutputStream());
130       return true;
131     }
132     return false;
133   }
134 
135   /**
136    * Writes the contents variable to the {@code URL}. Note that the URL must be a local {@code URL}.
137    *
138    * @param file
139    *          The {@code URL} of the local file you wish to write to.
140    * @param contents
141    *          The contents of the file you wish to create.
142    * @throws IOException
143    */
144   public static void writeUTF8File(URL file, String contents) throws IOException {
145     try {
146       writeUTF8File(new File(file.toURI()), contents);
147     } catch (URISyntaxException e) {
148       throw new IOException("Couldn't parse the URL", e);
149     }
150   }
151 
152   /**
153    * Writes the contents variable to the {@code File}.
154    *
155    * @param file
156    *          The {@code File} of the local file you wish to write to.
157    * @param contents
158    *          The contents of the file you wish to create.
159    */
160   public static void writeUTF8File(File file, String contents) throws IOException {
161     writeUTF8File(file.getAbsolutePath(), contents);
162   }
163 
164   /**
165    * Writes the contents variable to the {@code File} located at the filename.
166    *
167    * @param filename
168    *          The {@code File} of the local file you wish to write to.
169    * @param contents
170    *          The contents of the file you wish to create.
171    */
172   public static void writeUTF8File(String filename, String contents) throws IOException {
173     FileWriter out = new FileWriter(filename);
174     try {
175       out.write(contents);
176     } finally {
177       closeQuietly(out);
178     }
179   }
180 
181   /**
182    * Convenience method to read in a file from a local source.
183    *
184    * @param url
185    *          The {@code URL} to read the source data from.
186    * @return A String containing the source data or null in the case of an error.
187    * @deprecated this method doesn't support UTF8 or handle HTTP response codes
188    */
189   @Deprecated
190   public static String readFileFromURL(URL url) {
191     return readFileFromURL(url, null);
192   }
193 
194   /**
195    * Convenience method to read in a file from either a remote or local source.
196    *
197    * @param url
198    *          The {@code URL} to read the source data from.
199    * @param trustedClient
200    *          The {@code TrustedHttpClient} which should be used to communicate with the remote server. This can be null
201    *          for local file reads.
202    * @return A String containing the source data or null in the case of an error.
203    * @deprecated this method doesn't support UTF8 or handle HTTP response codes
204    */
205   @Deprecated
206   public static String readFileFromURL(URL url, TrustedHttpClient trustedClient) {
207     StringBuilder sb = new StringBuilder();
208     DataInputStream in = null;
209     HttpResponse response = null;
210     try {
211       // Do different things depending on what we're reading...
212       if ("file".equals(url.getProtocol())) {
213         in = new DataInputStream(url.openStream());
214       } else {
215         if (trustedClient == null) {
216           logger.error("Unable to read from remote source {} because trusted client is null!", url.getFile());
217           return null;
218         }
219         HttpGet get = new HttpGet(url.toURI());
220         try {
221           response = trustedClient.execute(get);
222         } catch (TrustedHttpClientException e) {
223           logger.warn("Unable to fetch file from {}.", url, e);
224           trustedClient.close(response);
225           return null;
226         }
227         in = new DataInputStream(response.getEntity().getContent());
228       }
229       int c = 0;
230       while ((c = in.read()) != -1) {
231         sb.append((char) c);
232       }
233     } catch (IOException e) {
234       logger.warn("IOException attempting to get file from {}.", url);
235       return null;
236     } catch (URISyntaxException e) {
237       logger.warn("URI error attempting to get file from {}.", url);
238       return null;
239     } catch (NullPointerException e) {
240       logger.warn("Nullpointer attempting to get file from {}.", url);
241       return null;
242     } finally {
243       IOUtils.closeQuietly(in);
244 
245       if (response != null) {
246         try {
247           trustedClient.close(response);
248         } catch (IOException e) {
249         }
250       }
251     }
252 
253     return sb.toString();
254   }
255 
256   public static Properties loadPropertiesFromUrl(final URL url) {
257     try {
258       return loadPropertiesFromStream(url.openStream());
259     } catch (IOException e) {
260       return chuck(e);
261     }
262   }
263 
264   /** Load properties from a stream. Close the stream after reading. */
265   public static Properties loadPropertiesFromStream(final InputStream stream) {
266     return withResource(stream, new Function.X<InputStream, Properties>() {
267       @Override
268       public Properties xapply(InputStream in) throws Exception {
269         final Properties p = new Properties();
270         p.load(in);
271         return p;
272       }
273     });
274   }
275 
276   /**
277    * Handle a closeable resource inside <code>f</code> and ensure it gets closed properly.
278    */
279   public static <A, B extends Closeable> A withResource(B b, Function<B, A> f) {
280     try {
281       return f.apply(b);
282     } finally {
283       IoSupport.closeQuietly(b);
284     }
285   }
286 
287   /**
288    * Handle a closeable resource inside <code>f</code> and ensure it gets closed properly.
289    */
290   public static <A, B extends Closeable> A withResource(B b, Fn<? super B, ? extends A> f) {
291     try {
292       return f.apply(b);
293     } finally {
294       IoSupport.closeQuietly(b);
295     }
296   }
297 
298   /**
299    * Open a classpath resource using the class loader of the given class.
300    *
301    * @return an input stream to the resource wrapped in a Some or none if the resource cannot be found
302    */
303   public static Option<InputStream> openClassPathResource(String resource, Class<?> clazz) {
304     return option(clazz.getResourceAsStream(resource));
305   }
306 
307   /**
308    * Open a classpath resource using the class loader of {@link IoSupport}.
309    *
310    * @see #openClassPathResource(String, Class)
311    */
312   public static Option<InputStream> openClassPathResource(String resource) {
313     return openClassPathResource(resource, IoSupport.class);
314   }
315 
316   /** Get a classpath resource as a file using the class loader of {@link IoSupport}. */
317   public static Option<File> classPathResourceAsFile(String resource) {
318     try {
319       final URL res = IoSupport.class.getResource(resource);
320       if (res != null) {
321         return Option.some(new File(res.toURI()));
322       } else {
323         return Option.none();
324       }
325     } catch (URISyntaxException e) {
326       return Option.none();
327     }
328   }
329 
330   /**
331    * Load a classpath resource into a string using UTF-8 encoding and the class loader of the given class.
332    *
333    * @return the content of the resource wrapped in a Some or none in case of any error
334    */
335   public static Option<String> loadFileFromClassPathAsString(String resource, Class<?> clazz) {
336     try {
337       final URL url = clazz.getResource(resource);
338       return url != null ? some(Resources.toString(clazz.getResource(resource), Charset.forName("UTF-8")))
339               : none(String.class);
340     } catch (IOException e) {
341       return none();
342     }
343   }
344 
345   /**
346    * Load a classpath resource into a string using the class loader of {@link IoSupport}.
347    *
348    * @see #loadFileFromClassPathAsString(String, Class)
349    */
350   public static Option<String> loadFileFromClassPathAsString(String resource) {
351     return loadFileFromClassPathAsString(resource, IoSupport.class);
352   }
353 
354   /**
355    * Handle a stream inside <code>f</code> and ensure that <code>s</code> gets closed properly.
356    * <p>
357    * <strong>Please note:</strong> The outcome of <code>f</code> is wrapped into a some. Therefore <code>f</code> is
358    * <em>not</em> allowed to return <code>null</code>. Use an <code>Option</code> instead and flatten the overall
359    * result.
360    *
361    * @return none, if the file does not exist
362    */
363   public static <A> Option<A> withFile(File file, Function2<InputStream, File, A> f) {
364     InputStream s = null;
365     try {
366       s = new FileInputStream(file);
367       return some(f.apply(s, file));
368     } catch (FileNotFoundException ignore) {
369       return none();
370     } finally {
371       IoSupport.closeQuietly(s);
372     }
373   }
374 
375   /**
376    * Handle a stream inside <code>f</code> and ensure that <code>s</code> gets closed properly.
377    *
378    * @param s
379    *          the stream creation function
380    * @param toErr
381    *          error handler transforming an exception into something else
382    * @param f
383    *          stream handler
384    * @deprecated use
385    *             {@link #withResource(org.opencastproject.util.data.Function0, org.opencastproject.util.data.Function, org.opencastproject.util.data.Function)}
386    *             instead
387    */
388   @Deprecated
389   public static <A, Err> Either<Err, A> withStream(Function0<InputStream> s, Function<Exception, Err> toErr,
390           Function<InputStream, A> f) {
391     InputStream in = null;
392     try {
393       in = s.apply();
394       return right(f.apply(in));
395     } catch (Exception e) {
396       return left(toErr.apply(e));
397     } finally {
398       IoSupport.closeQuietly(in);
399     }
400   }
401 
402   /**
403    * Handle a closeable resource inside <code>f</code> and ensure that <code>r</code> gets closed properly.
404    *
405    * @param r
406    *          resource creation function
407    * @param toErr
408    *          error handler transforming an exception into something else
409    * @param f
410    *          resource handler
411    */
412   public static <A, Err, B extends Closeable> Either<Err, A> withResource(Function0<B> r,
413           Function<Exception, Err> toErr, Function<B, A> f) {
414     B b = null;
415     try {
416       b = r.apply();
417       return right(f.apply(b));
418     } catch (Exception e) {
419       return left(toErr.apply(e));
420     } finally {
421       IoSupport.closeQuietly(b);
422     }
423   }
424 
425   /**
426    * Handle a stream inside <code>f</code> and ensure that <code>s</code> gets closed properly.
427    *
428    * @deprecated use {@link #withResource(java.io.Closeable, org.opencastproject.util.data.Function)} instead
429    */
430   @Deprecated
431   public static <A> A withStream(OutputStream s, Function<OutputStream, A> f) {
432     try {
433       return f.apply(s);
434     } finally {
435       IoSupport.closeQuietly(s);
436     }
437   }
438 
439   /** Function that reads an input stream into a string using utf-8 encoding. Stream does not get closed. */
440   public static final Function<InputStream, String> readToString = new Function.X<InputStream, String>() {
441     @Override
442     public String xapply(InputStream in) throws IOException {
443       return IOUtils.toString(in, "utf-8");
444     }
445   };
446 
447   /** Create a function that creates a {@link java.io.FileInputStream}. */
448   public static Function0<InputStream> fileInputStream(final File a) {
449     return new Function0.X<InputStream>() {
450       @Override
451       public InputStream xapply() throws Exception {
452         return new FileInputStream(a);
453       }
454     };
455   }
456 
457   /** Create a file from the list of path elements. */
458   public static File file(String... pathElems) {
459     return new File(path(pathElems));
460   }
461 
462   /**
463    * Run function <code>f</code> having exclusive read/write access to the given file.
464    * <p>
465    * Please note that the implementation uses Java NIO {@link java.nio.channels.FileLock} which only guarantees that two
466    * Java processes cannot interfere with each other.
467    * <p>
468    * The implementation blocks until a lock can be acquired.
469    *
470    * @throws NotFoundException
471    *            if the path to the file, to create a lock for, does not exist
472    * @throws IOException
473    *            if the file lock can not be created due to access limitations
474    */
475   public static synchronized <A> A locked(File file, Function<File, A> f) throws NotFoundException, IOException {
476     final Effect0 key = acquireLock(file);
477     try {
478       return f.apply(file);
479     } finally {
480       key.apply();
481     }
482   }
483 
484   /**
485    * Acquire a lock on a file. Return a key to release the lock.
486    *
487    * @return a key to release the lock
488    *
489    * @throws NotFoundException
490    *            if the path to the file, to create a lock for, does not exist
491    * @throws IOException
492    *            if the file lock can not be created due to access limitations
493    */
494   private static Effect0 acquireLock(File file) throws NotFoundException, IOException {
495     final RandomAccessFile raf;
496     try {
497       raf = new RandomAccessFile(file, "rw");
498     } catch (FileNotFoundException e) {
499       // this exception is thrown only if the directory path to the file isn't exist
500       // make sure to create all parent directories before locking the file
501       throw new NotFoundException("Error acquiring lock for " + file.getAbsolutePath(), e);
502     }
503     final FileLock lock = raf.getChannel().lock();
504     return new Effect0() {
505       @Override
506       protected void run() {
507         try {
508           lock.release();
509         } catch (IOException ignore) {
510         }
511         IoSupport.closeQuietly(raf);
512       }
513     };
514   }
515 
516   /**
517    * Serialize and deserialize an object. To test serializability.
518    */
519   public static <A extends Serializable> A serializeDeserialize(final A a) {
520     final ByteArrayOutputStream out = new ByteArrayOutputStream();
521     try {
522       withResource(
523               new ObjectOutputStream(out),
524               new FnX<ObjectOutputStream, Unit>() {
525                 @Override public Unit applyX(ObjectOutputStream out) throws Exception {
526                   out.writeObject(a);
527                   return Unit.unit;
528                 }
529               });
530       return withResource(
531               new ObjectInputStream(new ByteArrayInputStream(out.toByteArray())),
532               new FnX<ObjectInputStream, A>() {
533                 @Override public A applyX(ObjectInputStream in) throws Exception {
534                   return (A) in.readObject();
535                 }
536               });
537     } catch (IOException e) {
538       return Prelude.chuck(e);
539     }
540   }
541 }