001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.geometry.io.core.internal;
018
019import java.io.BufferedReader;
020import java.io.BufferedWriter;
021import java.io.Closeable;
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.InputStreamReader;
025import java.io.OutputStreamWriter;
026import java.io.UncheckedIOException;
027import java.net.URL;
028import java.nio.charset.Charset;
029import java.nio.file.Path;
030import java.util.stream.Stream;
031
032import org.apache.commons.geometry.io.core.input.GeometryInput;
033import org.apache.commons.geometry.io.core.output.GeometryOutput;
034
035/** Internal class containing utility methods for IO operations.
036 */
037public final class GeometryIOUtils {
038
039    /** Path separator character used on Unix-like systems. */
040    private static final char UNIX_PATH_SEP = '/';
041
042    /** Path separator character used on Windows. */
043    private static final char WINDOWS_PATH_SEP = '\\';
044
045    /** Utility class; no instantiation. */
046    private GeometryIOUtils() {}
047
048    /** Get the file name of the given path or null if one does not exist
049     * or is the empty string.
050     * @param path path to get the file name of
051     * @return file name of the given path
052     */
053    public static String getFileName(final Path path) {
054        if (path != null) {
055            return getFileName(path.toString());
056        }
057
058        return null;
059    }
060
061    /** Get the file name of the given url or null if one does not exist or is
062     * the empty string.
063     * @param url url to get the file name of
064     * @return file name of the given url
065     */
066    public static String getFileName(final URL url) {
067        if (url != null) {
068            return getFileName(url.getPath());
069        }
070
071        return null;
072    }
073
074    /** Get the file name from the given path string, defined as
075     * the substring following the last path separator character.
076     * Null is returned if the argument is null or the file name is
077     * the empty string.
078     * @param path path to get the file name from
079     * @return file name of the given path string or null if a
080     *      non-empty file name does not exist
081     */
082    public static String getFileName(final String path) {
083        if (path != null) {
084            final int lastSep = Math.max(
085                    path.lastIndexOf(UNIX_PATH_SEP),
086                    path.lastIndexOf(WINDOWS_PATH_SEP));
087
088            if (lastSep < path.length() - 1) {
089                return path.substring(lastSep + 1);
090            }
091        }
092
093        return null;
094    }
095
096    /** Get the part of the file name after the last dot.
097     * @param fileName file name to get the extension for
098     * @return the extension of the file name, the empty string if no extension is found, or
099     *      null if the argument is null
100     */
101    public static String getFileExtension(final String fileName) {
102        if (fileName != null) {
103            final int idx = fileName.lastIndexOf('.');
104            if (idx > -1) {
105                return fileName.substring(idx + 1);
106            }
107
108            return "";
109        }
110
111        return null;
112    }
113
114    /** Create a {@link BufferedReader} for reading from the given input. The charset used is the charset
115     * defined in {@code input} or {@code defaultCharset} if null.
116     * @param input input to read from
117     * @param defaultCharset charset to use if no charset is defined in the input
118     * @return new reader instance
119     * @throws UncheckedIOException if an I/O error occurs
120     */
121    public static BufferedReader createBufferedReader(final GeometryInput input, final Charset defaultCharset) {
122        final Charset charset = input.getCharset() != null ?
123                input.getCharset() :
124                defaultCharset;
125
126        return new BufferedReader(new InputStreamReader(input.getInputStream(), charset));
127    }
128
129    /** Create a {@link BufferedWriter} for writing to the given output. The charset used is the charset
130     * defined in {@code output} or {@code defaultCharset} if null.
131     * @param output output to write to
132     * @param defaultCharset charset to use if no charset is defined in the output
133     * @return new writer instance
134     * @throws UncheckedIOException if an I/O error occurs
135     */
136    public static BufferedWriter createBufferedWriter(final GeometryOutput output, final Charset defaultCharset) {
137        final Charset charset = output.getCharset() != null ?
138                output.getCharset() :
139                defaultCharset;
140
141        return new BufferedWriter(new OutputStreamWriter(output.getOutputStream(), charset));
142    }
143
144    /** Get a value from {@code supplier}, wrapping any {@link IOException} with
145     * {@link UncheckedIOException}.
146     * @param <T> returned type
147     * @param supplier object supplying the return value
148     * @return supplied value
149     * @throws UncheckedIOException if an I/O error occurs
150     */
151    public static <T> T getUnchecked(final IOSupplier<T> supplier) {
152        try {
153            return supplier.get();
154        } catch (IOException exc) {
155            throw createUnchecked(exc);
156        }
157    }
158
159    /** Pass the given argument to the consumer, wrapping any {@link IOException} with
160     * {@link UncheckedIOException}.
161     * @param <T> argument type
162     * @param consumer function to call
163     * @param arg function argument
164     * @throws UncheckedIOException if an I/O error occurs
165     */
166    public static <T> void acceptUnchecked(final IOConsumer<T> consumer, final T arg) {
167        try {
168            consumer.accept(arg);
169        } catch (IOException exc) {
170            throw createUnchecked(exc);
171        }
172    }
173
174    /** Call the given function with the argument and return the {@code int} result, wrapping any
175     * {@link IOException} with {@link UncheckedIOException}.
176     * @param <T> argument type
177     * @param fn function to call
178     * @param arg function argument
179     * @return int value
180     * @throws UncheckedIOException if an I/O error occurs
181     */
182    public static <T> int applyAsIntUnchecked(final IOToIntFunction<T> fn, final T arg) {
183        try {
184            return fn.applyAsInt(arg);
185        } catch (IOException exc) {
186            throw createUnchecked(exc);
187        }
188    }
189
190    /** Close the argument, wrapping any IO exceptions with {@link UncheckedIOException}.
191     * @param closeable argument to close
192     * @throws UncheckedIOException if an I/O error occurs
193     */
194    public static void closeUnchecked(final Closeable closeable) {
195        try {
196            closeable.close();
197        } catch (IOException exc) {
198            throw createUnchecked(exc);
199        }
200    }
201
202    /** Create an unchecked exception from the given checked exception. The message of the
203     * returned exception contains the original exception's type and message.
204     * @param exc exception to wrap in an unchecked exception
205     * @return the unchecked exception
206     */
207    public static UncheckedIOException createUnchecked(final IOException exc) {
208        final String msg = exc.getClass().getSimpleName() + ": " + exc.getMessage();
209        return new UncheckedIOException(msg, exc);
210    }
211
212    /** Create an exception indicating a parsing or syntax error.
213     * @param msg exception message
214     * @return an exception indicating a parsing or syntax error
215     */
216    public static IllegalStateException parseError(final String msg) {
217        return parseError(msg, null);
218    }
219
220    /** Create an exception indicating a parsing or syntax error.
221     * @param msg exception message
222     * @param cause exception cause
223     * @return an exception indicating a parsing or syntax error
224     */
225    public static IllegalStateException parseError(final String msg, final Throwable cause) {
226        return new IllegalStateException(msg, cause);
227    }
228
229    /** Pass a supplied {@link Closeable} instance to {@code function} and return the result.
230     * The {@code Closeable} instance returned by the supplier is closed if function execution
231     * fails, otherwise the instance is <em>not</em> closed.
232     * @param <T> Return type
233     * @param <C> Closeable type
234     * @param function function called with the supplied Closeable instance
235     * @param closeableSupplier supplier used to obtain a Closeable instance
236     * @return result of calling {@code function} with a supplied Closeable instance
237     * @throws java.io.UncheckedIOException if an I/O error occurs
238     */
239    public static <T, C extends Closeable> T tryApplyCloseable(final IOFunction<C, T> function,
240            final IOSupplier<? extends C> closeableSupplier) {
241        C closeable = null;
242        RuntimeException exc;
243        try {
244            closeable = closeableSupplier.get();
245            return function.apply(closeable);
246        } catch (RuntimeException e) {
247            exc = e;
248        } catch (IOException e) {
249            exc = createUnchecked(e);
250        }
251
252        if (closeable != null) {
253            try {
254                closeable.close();
255            } catch (IOException suppressed) {
256                exc.addSuppressed(suppressed);
257            }
258        }
259
260        throw exc;
261    }
262
263    /** Create a stream associated with an input stream. The input stream is closed when the
264     * stream is closed and also closed if stream creation fails. Any {@link IOException} thrown
265     * when the input stream is closed after the return of this method are wrapped with {@link UncheckedIOException}.
266     * @param <T> Stream element type
267     * @param <I> Input stream type
268     * @param streamFunction function accepting an input stream and returning a stream
269     * @param inputStreamSupplier supplier used to obtain the input stream
270     * @return stream associated with the input stream return by the supplier
271     * @throws java.io.UncheckedIOException if an I/O error occurs during input stream and stream creation
272     */
273    public static <T, I extends InputStream> Stream<T> createCloseableStream(
274            final IOFunction<I, Stream<T>> streamFunction, final IOSupplier<? extends I> inputStreamSupplier) {
275        return tryApplyCloseable(
276                in -> streamFunction.apply(in).onClose(closeAsUncheckedRunnable(in)),
277                inputStreamSupplier);
278    }
279
280    /** Return a {@link Runnable} that calls {@link Closeable#getClass() close()} on the argument,
281     * wrapping any {@link IOException} with {@link UncheckedIOException}.
282     * @param closeable instance to be closed
283     * @return runnable that calls {@code close()) on the argument
284     */
285    private static Runnable closeAsUncheckedRunnable(final Closeable closeable) {
286        return () -> closeUnchecked(closeable);
287    }
288}