001/*
002 * Copyright (c) 2012-2021 Institut National des Sciences Appliquées de Lyon (INSA Lyon) and others
003 *
004 * This program and the accompanying materials are made available under the
005 * terms of the Eclipse Public License 2.0 which is available at
006 * http://www.eclipse.org/legal/epl-2.0.
007 *
008 * SPDX-License-Identifier: EPL-2.0
009 */
010package gololang;
011
012import java.io.BufferedOutputStream;
013import java.io.BufferedReader;
014import java.io.File;
015import java.io.IOException;
016import java.io.OutputStream;
017import java.io.PrintStream;
018import java.net.MalformedURLException;
019import java.net.URI;
020import java.net.URL;
021import java.nio.charset.Charset;
022import java.nio.file.Files;
023import java.nio.file.Path;
024import java.nio.file.Paths;
025import java.nio.file.StandardOpenOption;
026import java.util.Iterator;
027import java.util.NoSuchElementException;
028
029import static gololang.Predefined.require;
030
031/**
032 * Module providing IO related utility functions.
033 */
034public final class IO {
035  private IO() {
036    // utility class
037  }
038
039  /**
040   * An iterator on the lines of a {@code java.io.BufferedReader}.
041   */
042  public static final class LinesIterator implements Iterator<String> {
043    private final BufferedReader reader;
044    private String currentLine;
045
046    public static Iterator<String> of(Object source) throws IOException {
047      require(source instanceof BufferedReader, "The source must be a reader");
048      return new LinesIterator((BufferedReader) source);
049    }
050
051    private LinesIterator(BufferedReader reader) throws IOException {
052      require(reader != null, "The reader must not be null");
053      this.reader = reader;
054      this.currentLine = reader.readLine();
055    }
056
057    @Override
058    public boolean hasNext() {
059      return this.currentLine != null;
060    }
061
062    @Override
063    public String next() {
064      if (this.currentLine == null) {
065        throw new NoSuchElementException();
066      }
067      String val = this.currentLine;
068      try {
069        this.currentLine = this.reader.readLine();
070      } catch (IOException e) {
071        this.currentLine = null;
072      }
073      return val;
074    }
075  }
076
077  /**
078   * Opens a file for reading.
079   *
080   * @param file     the file to read from as an instance of either {@link String}, {@link File} or {@link Path}.
081   * @return a {@code java.io.BufferedReader}.
082   */
083  public static BufferedReader openFile(Object file) throws IOException {
084    return Files.newBufferedReader(toPath(file));
085  }
086
087  /**
088   * Returns the default charset.
089   */
090  public static Charset defaultCharset() {
091    return Charset.defaultCharset();
092  }
093
094  /**
095   * Converts the given String or Charset object into a Charset.
096   */
097  public static Charset toCharset(Object encoding) {
098    if (encoding == null) {
099      return defaultCharset();
100    }
101    Charset charset = null;
102    if (encoding instanceof String) {
103      charset = Charset.forName((String) encoding);
104    } else if (encoding instanceof Charset) {
105      charset = (Charset) encoding;
106    } else {
107      throw new IllegalArgumentException("Can't get a charset from a "
108          + encoding.getClass().getName());
109    }
110    return charset;
111  }
112
113  /**
114   * Reads the content of a text file.
115   *
116   * @param file     the file to read from as an instance of either {@link String}, {@link File} or {@link Path}.
117   * @param encoding the file encoding as a {@link String} or {@link Charset}.
118   * @return the content as a {@link String}.
119   */
120  public static String fileToText(Object file, Object encoding) throws Throwable {
121    return new String(Files.readAllBytes(toPath(file)), toCharset(encoding));
122  }
123
124  /**
125   * Polymorphic {@link java.nio.file.Path} creation.
126   *
127   * @param file the file descriptor as an instance of either {@link String}, {@link File} or {@link Path}.
128   * @return the corresponding {@link java.nio.file.Path} object.
129   */
130  public static Path toPath(Object file) {
131    if (file == null) {
132      return null;
133    } else if (file instanceof String) {
134      return Paths.get((String) file);
135    } else if (file instanceof File) {
136      return ((File) file).toPath();
137    } else if (file instanceof Path) {
138      return (Path) file;
139    }
140    throw new IllegalArgumentException("file must be a string, a file or a path");
141  }
142
143  /**
144   * Polymorphic {@link java.net.URL} creation.
145   *
146   * @param ref the url descriptor as a {@link String}, {@link URL}, {@link URI}, {@link File} or {@link Path}.
147   * @return the corresponding {@link java.net.URL} object.
148   */
149  public static URL toURL(Object ref) throws MalformedURLException {
150    if (ref == null) {
151      return null;
152    } else if (ref instanceof String) {
153      return new URL((String) ref);
154    } else if (ref instanceof URL) {
155      return (URL) ref;
156    } else if (ref instanceof URI) {
157      return ((URI) ref).toURL();
158    } else if (ref instanceof Path) {
159      return ((Path) ref).toUri().toURL();
160    } else if (ref instanceof File) {
161      return ((File) ref).toURI().toURL();
162    }
163    throw new IllegalArgumentException(String.format("Can't convert a %s into a URL", ref.getClass().getName()));
164  }
165
166
167  /**
168   * Writes some text to a file.
169   *
170   * The file and parents directories are created if they does not exist. The file is overwritten if it already exists. If the file is {@code "-"}, the content is written to standard output.
171   *
172   * @param text the text to write.
173   * @param file the file to write to as an instance of either {@link String}, {@link File} or {@link Path}.
174   */
175  public static void textToFile(Object text, Object file) throws Throwable {
176    textToFile(text, file, null);
177  }
178
179  /**
180   * Writes some text to a file using the given {@link Charset}.
181   *
182   * The file and parents directories are created if they does not exist. The file is overwritten if it already exists. If the file is {@code "-"}, the content is written to standard output.
183   *
184   * @param text the text to write.
185   * @param file the file to write to as an instance of either {@link String}, {@link File} or {@link Path}.
186   * @param charset the charset to encode the text in.
187   */
188  public static void textToFile(Object text, Object file, Object charset) throws Throwable {
189    require(text instanceof String, "text must be a string");
190    Charset encoding = toCharset(charset);
191    String str = (String) text;
192    if ("-".equals(file.toString())) {
193      System.out.write(str.getBytes(encoding));
194    } else {
195      Path path = toPath(file);
196      if (path.getParent() != null) {
197        Files.createDirectories(path.getParent());
198      }
199      Files.write(
200          path,
201          str.getBytes(encoding),
202          StandardOpenOption.WRITE,
203          StandardOpenOption.CREATE,
204          StandardOpenOption.TRUNCATE_EXISTING);
205    }
206  }
207
208  /**
209   * Check if a file exists.
210   *
211   * @param file the file to read from as an instance of either {@link String}, {@link File} or {@link Path}.
212   * @return true if the file exists, false if it doesn't
213   */
214  public static boolean fileExists(Object file) {
215    return Files.exists(toPath(file));
216  }
217
218  /**
219   * Reads the next line of characters from the console.
220   *
221   * @return a String.
222   */
223  public static String readln() throws IOException {
224    return System.console().readLine();
225  }
226
227  /**
228   * Reads the next line of characters from the console.
229   *
230   * @param message displays a prompt message.
231   * @return a String.
232   */
233  public static String readln(String message) throws IOException {
234    System.out.print(message);
235    return readln();
236  }
237
238  /**
239   * Reads a password from the console with echoing disabled.
240   *
241   * @return a String.
242   */
243  public static String readPassword() throws IOException {
244    return String.valueOf(System.console().readPassword());
245  }
246
247  /**
248   * Reads a password from the console with echoing disabled.
249   *
250   * @param message displays a prompt message.
251   * @return a String.
252   */
253  public static String readPassword(String message) throws IOException {
254    System.out.print(message);
255    return readPassword();
256  }
257
258  /**
259   * Reads a password from the console with echoing disabled, returning an {@code char[]} array.
260   *
261   * @return a character array.
262   */
263  public static char[] secureReadPassword() throws IOException {
264    return System.console().readPassword();
265  }
266
267  /**
268   * Reads a password from the console with echoing disabled, returning an {@code char[]} array.
269   *
270   * @param message displays a prompt message.
271   * @return a character array.
272   */
273  public static char[] secureReadPassword(String message) throws IOException {
274    System.out.print(message);
275    return secureReadPassword();
276  }
277
278  /**
279   * Create an {@code PrintStream} from the specified value.
280   * <p>
281   * Same as {@code printStreamFrom(output, defaultCharset())}
282   *
283   * @param output the file to use; "-" means standard output
284   * @return a buffered {@code PrintStream} or {@code java.lang.System.out}
285   * @see #defaultCharset()
286   * @see #printStreamFrom(Object, Object)
287   */
288  public static PrintStream printStreamFrom(Object output) throws IOException {
289    return printStreamFrom(output, defaultCharset());
290  }
291
292  /**
293   * Create an {@code PrintStream} from the specified value.
294   * <p>
295   * If the given string is "-", {@code java.lang.System.out} is used. Otherwise, a {@code java.nio.file.Path} is
296   * created with {@link #toPath(Object)}.
297   * The returned {@code PrintStream} is buffered and uses the given charset. Parent directory is created. If the file
298   * exists, it is overwritten.
299   *
300   * @param output the file to use; "-" means standard output
301   * @param charset the charset to use, as a {@code java.lang.String} or a {@code java.nio.charset.Charset}
302   * @return a buffered {@code PrintStream} or {@code java.lang.System.out}
303   * @see #toPath(Object)
304   * @see #toCharset(Object)
305   */
306  public static PrintStream printStreamFrom(Object output, Object charset) throws IOException {
307    if ("-".equals(output)) {
308      return System.out;
309    }
310    if (output instanceof PrintStream) {
311      return (PrintStream) output;
312    }
313    OutputStream out;
314    if (output instanceof OutputStream) {
315      out = (OutputStream) output;
316    } else {
317      Path outputPath = toPath(output);
318      if (outputPath.getParent() != null) {
319        Files.createDirectories(outputPath.getParent());
320      }
321      out = Files.newOutputStream(outputPath);
322    }
323    return new PrintStream(
324        new BufferedOutputStream(out),
325        true,
326        toCharset(charset).name());
327  }
328
329
330}