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