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;
018
019import java.text.MessageFormat;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.Iterator;
024import java.util.List;
025import java.util.Locale;
026import java.util.Map;
027import java.util.Objects;
028import java.util.stream.Collectors;
029import java.util.stream.Stream;
030
031import org.apache.commons.geometry.core.partitioning.BoundarySource;
032import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
033import org.apache.commons.geometry.io.core.input.GeometryInput;
034import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
035import org.apache.commons.geometry.io.core.output.GeometryOutput;
036import org.apache.commons.numbers.core.Precision;
037
038/** Class managing IO operations for geometric data formats containing region boundaries.
039 * All IO operations are delegated to registered format-specific {@link BoundaryReadHandler read handlers}
040 * and {@link BoundaryWriteHandler write handlers}.
041 *
042 * <p><strong>Exceptions</strong>
043 * <p>Despite having functionality related to I/O operations, this class has been designed to <em>not</em>
044 * throw checked exceptions, in particular {@link java.io.IOException IOException}. The primary reasons for
045 * this choice are
046 * <ul>
047 *  <li>convenience,</li>
048 *  <li>compatibility with functional programming, and </li>
049 *  <li>the fact that modern Java practice is moving away from checked exceptions in general (as exemplified
050 *      by the JDK's {@link java.io.UncheckedIOException UncheckedIOException}).</li>
051 * </ul>
052 * As a result, any {@link java.io.IOException IOException} thrown internally by this or related classes
053 * is wrapped with {@link java.io.UncheckedIOException UncheckedIOException}. Other common runtime exceptions
054 * include {@link IllegalArgumentException}, which typically indicates mathematically invalid data, and
055 * {@link IllegalStateException}, which typically indicates format or parsing errors. See the method-level
056 * documentation for more details.
057 *
058 * <p><strong>Implementation note:</strong> Instances of this class are thread-safe as long as the
059 * registered handler instances are thread-safe.</p>
060 * @param <H> Geometric boundary type
061 * @param <B> Boundary source type
062 * @param <R> Read handler type
063 * @param <W> Write handler type
064 * @see BoundaryReadHandler
065 * @see BoundaryWriteHandler
066 * @see <a href="https://en.wikipedia.org/wiki/Boundary_representations">Boundary representations</a>
067 */
068public class BoundaryIOManager<
069    H extends HyperplaneConvexSubset<?>,
070    B extends BoundarySource<H>,
071    R extends BoundaryReadHandler<H, B>,
072    W extends BoundaryWriteHandler<H, B>> {
073
074    /** Error message used when a handler is null. */
075    private static final String HANDLER_NULL_ERR = "Handler cannot be null";
076
077    /** Error message used when a format is null. */
078    private static final String FORMAT_NULL_ERR = "Format cannot be null";
079
080    /** Error message used when a format name is null. */
081    private static final String FORMAT_NAME_NULL_ERR = "Format name cannot be null";
082
083    /** Read handler registry. */
084    private final HandlerRegistry<R> readRegistry = new HandlerRegistry<>();
085
086    /** Write handler registry. */
087    private final HandlerRegistry<W> writeRegistry = new HandlerRegistry<>();
088
089    /** Register a {@link BoundaryReadHandler read handler} with the instance, replacing
090     * any handler previously registered for the argument's supported data format, as returned
091     * by {@link BoundaryReadHandler#getFormat()}.
092     * @param handler handler to register
093     * @throws NullPointerException if {@code handler}, its {@link BoundaryReadHandler#getFormat() format},
094     *      or the {@link GeometryFormat#getFormatName() format's name} are null
095     */
096    public void registerReadHandler(final R handler) {
097        Objects.requireNonNull(handler, HANDLER_NULL_ERR);
098        readRegistry.register(handler.getFormat(), handler);
099    }
100
101    /** Unregister a previously registered {@link BoundaryReadHandler read handler};
102     * does nothing if the argument is null or is not currently registered.
103     * @param handler handler to unregister; may be null
104     */
105    public void unregisterReadHandler(final R handler) {
106        readRegistry.unregister(handler);
107    }
108
109    /** Get all registered {@link BoundaryReadHandler read handlers}.
110     * @return list containing all registered read handlers
111     */
112    public List<R> getReadHandlers() {
113        return readRegistry.getHandlers();
114    }
115
116    /** Get the list of formats supported by the currently registered
117     * {@link BoundaryReadHandler read handlers}.
118     * @return list of read formats
119     * @see BoundaryReadHandler#getFormat()
120     */
121    public List<GeometryFormat> getReadFormats() {
122        return readRegistry.getHandlers().stream()
123                .map(BoundaryReadHandler::getFormat)
124                .collect(Collectors.toList());
125    }
126
127    /** Get the {@link BoundaryReadHandler read handler} for the given format or
128     * null if no such handler has been registered.
129     * @param fmt format to obtain a handler for
130     * @return read handler for the given format or null if not found
131     */
132    public R getReadHandlerForFormat(final GeometryFormat fmt) {
133        return readRegistry.getByFormat(fmt);
134    }
135
136    /** Get the {@link BoundaryReadHandler read handler} for the given file extension
137     * or null if no such handler has been registered. File extension comparisons are
138     * not case-sensitive.
139     * @param fileExt file extension to obtain a handler for
140     * @return read handler for the given file extension or null if not found
141     * @see GeometryFormat#getFileExtensions()
142     */
143    public R getReadHandlerForFileExtension(final String fileExt) {
144        return readRegistry.getByFileExtension(fileExt);
145    }
146
147    /** Register a {@link BoundaryWriteHandler write handler} with the instance, replacing
148     * any handler previously registered for the argument's supported data format, as returned
149     * by {@link BoundaryWriteHandler#getFormat()}.
150     * @param handler handler to register
151     * @throws NullPointerException if {@code handler}, its {@link BoundaryWriteHandler#getFormat() format},
152     *      or the {@link GeometryFormat#getFormatName() format's name} are null
153     */
154    public void registerWriteHandler(final W handler) {
155        Objects.requireNonNull(handler, HANDLER_NULL_ERR);
156        writeRegistry.register(handler.getFormat(), handler);
157    }
158
159    /** Unregister a previously registered {@link BoundaryWriteHandler write handler};
160     * does nothing if the argument is null or is not currently registered.
161     * @param handler handler to unregister; may be null
162     */
163    public void unregisterWriteHandler(final W handler) {
164        writeRegistry.unregister(handler);
165    }
166
167    /** Get all registered {@link BoundaryWriteHandler write handlers}.
168     * @return list containing all registered write handlers
169     */
170    public List<W> getWriteHandlers() {
171        return writeRegistry.getHandlers();
172    }
173
174    /** Get the list of formats supported by the currently registered
175     * {@link BoundaryWriteHandler write handlers}.
176     * @return list of write formats
177     * @see BoundaryWriteHandler#getFormat()
178     */
179    public List<GeometryFormat> getWriteFormats() {
180        return writeRegistry.getHandlers().stream()
181                .map(BoundaryWriteHandler::getFormat)
182                .collect(Collectors.toList());
183    }
184
185    /** Get the {@link BoundaryWriteHandler write handler} for the given format or
186     * null if no such handler has been registered.
187     * @param fmt format to obtain a handler for
188     * @return write handler for the given format or null if not found
189     */
190    public W getWriteHandlerForFormat(final GeometryFormat fmt) {
191        return writeRegistry.getByFormat(fmt);
192    }
193
194    /** Get the {@link BoundaryWriteHandler write handler} for the given file extension
195     * or null if no such handler has been registered. File extension comparisons are
196     * not case-sensitive.
197     * @param fileExt file extension to obtain a handler for
198     * @return write handler for the given file extension or null if not found
199     * @see GeometryFormat#getFileExtensions()
200     */
201    public W getWriteHandlerForFileExtension(final String fileExt) {
202        return writeRegistry.getByFileExtension(fileExt);
203    }
204
205    /** Return a {@link BoundarySource} containing all boundaries from the given input.
206     * A runtime exception may be thrown if mathematically invalid boundaries are encountered.
207     * @param in input to read boundaries from
208     * @param fmt format of the input; if null, the format is determined implicitly from the
209     *      file extension of the input {@link GeometryInput#getFileName() file name}
210     * @param precision precision context used for floating point comparisons
211     * @return object containing all boundaries from the input
212     * @throws IllegalArgumentException if mathematically invalid data is encountered or no
213     *      {@link BoundaryReadHandler read handler} can be found for the input format
214     * @throws IllegalStateException if a data format error occurs
215     * @throws java.io.UncheckedIOException if an I/O error occurs
216     */
217    public B read(final GeometryInput in, final GeometryFormat fmt, final Precision.DoubleEquivalence precision) {
218        return requireReadHandler(in, fmt).read(in, precision);
219    }
220
221    /** Return a {@link Stream} providing access to all boundaries from the given input. The underlying input
222     * stream is closed when the returned stream is closed. Callers should therefore use the returned stream
223     * in a try-with-resources statement to ensure that all resources are properly released. Ex:
224     * <pre>
225     *  try (Stream&lt;H&gt; stream = manager.boundaries(in, fmt, precision)) {
226     *      // access stream content
227     *  }
228     *  </pre>
229     * <p>The following exceptions may be thrown during stream iteration:
230     *  <ul>
231     *      <li>{@link IllegalArgumentException} if mathematically invalid data is encountered</li>
232     *      <li>{@link IllegalStateException} if a data format error occurs</li>
233     *      <li>{@link java.io.UncheckedIOException UncheckedIOException} if an I/O error occurs</li>
234     *  </ul>
235     * @param in input to read boundaries from
236     * @param fmt format of the input; if null, the format is determined implicitly from the
237     *      file extension of the input {@link GeometryInput#getFileName() file name}
238     * @param precision precision context used for floating point comparisons
239     * @return stream providing access to all boundaries from the input
240     * @throws IllegalArgumentException if no {@link BoundaryReadHandler read handler} can be found for
241     *      the input format
242     * @throws IllegalStateException if a data format error occurs during stream creation
243     * @throws java.io.UncheckedIOException if an I/O error occurs during stream creation
244     */
245    public Stream<H> boundaries(final GeometryInput in, final GeometryFormat fmt,
246            final Precision.DoubleEquivalence precision) {
247        return requireReadHandler(in, fmt).boundaries(in, precision);
248    }
249
250    /** Write all boundaries from {@code src} to the given output.
251     * @param src object containing boundaries to write
252     * @param out output to write boundaries to
253     * @param fmt format of the output; if null, the format is determined implicitly from the
254     *      file extension of the output {@link GeometryOutput#getFileName()}
255     * @throws IllegalArgumentException if no {@link BoundaryWriteHandler write handler} can be found
256     *      for the output format
257     * @throws java.io.UncheckedIOException if an I/O error occurs
258     */
259    public void write(final B src, final GeometryOutput out, final GeometryFormat fmt) {
260        requireWriteHandler(out, fmt).write(src, out);
261    }
262
263    /** Get the {@link BoundaryReadHandler read handler} matching the arguments, throwing an exception
264     * on failure. If {@code fmt} is given, the handler registered for that format is returned and the
265     * {@code input} object is not examined. If {@code fmt} is null, the file extension of the input
266     * {@link GeometryInput#getFileName() file name} is used to implicitly determine the format and locate
267     * the handler.
268     * @param in input object
269     * @param fmt format; may be null
270     * @return the read handler for {@code fmt} or, if {@code fmt} is null, the read handler for the
271     *      file extension indicated by the input
272     * @throws NullPointerException if {@code in} is null
273     * @throws IllegalArgumentException if no matching handler can be found
274     */
275    protected R requireReadHandler(final GeometryInput in, final GeometryFormat fmt) {
276        Objects.requireNonNull(in, "Input cannot be null");
277        return readRegistry.requireHandlerByFormatOrFileName(fmt, in.getFileName());
278    }
279
280    /** Get the {@link BoundaryWriteHandler write handler} matching the arguments, throwing an exception
281     * on failure. If {@code fmt} is given, the handler registered for that format is returned and the
282     * {@code input} object is not examined. If {@code fmt} is null, the file extension of the output
283     * {@link GeometryOutput#getFileName() file name} is used to implicitly determine the format and locate
284     * the handler.
285     * @param out output object
286     * @param fmt format; may be null
287     * @return the write handler for {@code fmt} or, if {@code fmt} is null, the write handler for the
288     *      file extension indicated by the output
289     * @throws NullPointerException if {@code out} is null
290     * @throws IllegalArgumentException if no matching handler can be found
291     */
292    protected W requireWriteHandler(final GeometryOutput out, final GeometryFormat fmt) {
293        Objects.requireNonNull(out, "Output cannot be null");
294        return writeRegistry.requireHandlerByFormatOrFileName(fmt, out.getFileName());
295    }
296
297    /** Internal class used to manage handler registration. Instances of this class
298     * are thread-safe.
299     * @param <T> Handler type
300     */
301    private static final class HandlerRegistry<T> {
302
303        /** List of registered handlers. */
304        private final List<T> handlers = new ArrayList<>();
305
306        /** Handlers keyed by lower-case format name. */
307        private final Map<String, T> handlersByFormatName = new HashMap<>();
308
309        /** Handlers keyed by lower-case file extension. */
310        private final Map<String, T> handlersByFileExtension = new HashMap<>();
311
312        /** Register a handler for the given {@link GeometryFormat format}.
313         * @param fmt format for the handler
314         * @param handler handler to register
315         * @throws NullPointerException if either argument is null
316         */
317        public synchronized void register(final GeometryFormat fmt, final T handler) {
318            Objects.requireNonNull(fmt, FORMAT_NULL_ERR);
319            Objects.requireNonNull(handler, HANDLER_NULL_ERR);
320
321            if (!handlers.contains(handler)) {
322                // remove any previously registered handler
323                unregisterFormat(fmt);
324
325                // add the new handler
326                addToFormat(fmt.getFormatName(), handler);
327                addToFileExtensions(fmt.getFileExtensions(), handler);
328
329                handlers.add(handler);
330            }
331        }
332
333        /** Unregister the given handler.
334         * @param handler handler to unregister
335         */
336        public synchronized void unregister(final T handler) {
337            if (handler != null && handlers.remove(handler)) {
338                removeValue(handlersByFormatName, handler);
339                removeValue(handlersByFileExtension, handler);
340            }
341        }
342
343        /** Unregister the current handler for the given format and return it.
344         * Null is returned if no handler was registered.
345         * @param fmt format to unregister
346         * @return handler instance previously registered for the format or null
347         *      if not found
348         */
349        public synchronized T unregisterFormat(final GeometryFormat fmt) {
350            final T handler = getByFormat(fmt);
351            if (handler != null) {
352                unregister(handler);
353            }
354            return handler;
355        }
356
357        /** Get all registered handlers.
358         * @return list of all registered handlers
359         */
360        public synchronized List<T> getHandlers() {
361            return Collections.unmodifiableList(new ArrayList<>(handlers));
362        }
363
364        /** Get the first handler registered for the given format, or null if
365         * not found.
366         * @param fmt format to obtain a handler for
367         * @return first handler registered for the format
368         */
369        public synchronized T getByFormat(final GeometryFormat fmt) {
370            if (fmt != null) {
371                return getByNormalizedKey(handlersByFormatName, fmt.getFormatName());
372            }
373            return null;
374        }
375
376        /** Get the first handler registered for the given file extension or null if not found.
377         * @param fileExt file extension
378         * @return first handler registered for the given file extension or null if not found
379         */
380        public synchronized T getByFileExtension(final String fileExt) {
381            return getByNormalizedKey(handlersByFileExtension, fileExt);
382        }
383
384        /** Get the handler for the given format or file extension, throwing an exception if one
385         * cannot be found. If {@code fmt} is not null, it is used to directly look up the handler
386         * and the {@code fileName} argument is ignored. Otherwise, the file extension is extracted
387         * from {@code fileName} and used to look up the handler.
388         * @param fmt format to look up; if present, {@code fileName} is ignored
389         * @param fileName file name to use for the look up if {@code fmt} is null
390         * @return the handler matching the arguments
391         * @throws IllegalArgumentException if a handler cannot be found
392         */
393        public synchronized T requireHandlerByFormatOrFileName(final GeometryFormat fmt, final String fileName) {
394            T handler = null;
395            if (fmt != null) {
396                handler = getByFormat(fmt);
397
398                if (handler == null) {
399                    throw new IllegalArgumentException(MessageFormat.format(
400                            "Failed to find handler for format \"{0}\"", fmt.getFormatName()));
401                }
402            } else {
403                final String fileExt = GeometryIOUtils.getFileExtension(fileName);
404                if (fileExt != null && !fileExt.isEmpty()) {
405                    handler = getByFileExtension(fileExt);
406
407                    if (handler == null) {
408                        throw new IllegalArgumentException(MessageFormat.format(
409                               "Failed to find handler for file extension \"{0}\"", fileExt));
410                    }
411                } else {
412                    throw new IllegalArgumentException(
413                            "Failed to find handler: no format specified and no file extension available");
414                }
415            }
416
417            return handler;
418        }
419
420        /** Add the handler to the internal format name map.
421         * @param fmtName format name
422         * @param handler handler to add
423         * @throws NullPointerException if {@code fmtName} is null
424         */
425        private void addToFormat(final String fmtName, final T handler) {
426            Objects.requireNonNull(fmtName, FORMAT_NAME_NULL_ERR);
427            handlersByFormatName.put(normalizeString(fmtName), handler);
428        }
429
430        /** Add the handler to the internal file extension map under each file extension.
431         * @param fileExts file extensions to map to the handler
432         * @param handler handler to add to the file extension map
433         */
434        private void addToFileExtensions(final List<String> fileExts, final T handler) {
435            if (fileExts != null) {
436                for (final String fileExt : fileExts) {
437                    addToFileExtension(fileExt, handler);
438                }
439            }
440        }
441
442        /** Add the handler to the internal file extension map.
443         * @param fileExt file extension to map to the handler
444         * @param handler handler to add to the file extension map
445         */
446        private void addToFileExtension(final String fileExt, final T handler) {
447            if (fileExt != null) {
448                handlersByFileExtension.put(normalizeString(fileExt), handler);
449            }
450        }
451
452        /** Normalize the given key and return its associated value in the map, or null
453         * if not found.
454         * @param <V> Value type
455         * @param map map to search
456         * @param key unnormalized map key
457         * @return the value associated with the key after normalization, or null if not found
458         */
459        private static <V> V getByNormalizedKey(final Map<String, V> map, final String key) {
460            if (key != null) {
461                return map.get(normalizeString(key));
462            }
463            return null;
464        }
465
466        /** Remove all keys that map to {@code value}.
467         * @param <V> Value type
468         * @param map map to remove keys from
469         * @param value value to remove from all entries in the map
470         */
471        private static <V> void removeValue(final Map<String, V> map, final V value) {
472            final Iterator<Map.Entry<String, V>> it = map.entrySet().iterator();
473            while (it.hasNext()) {
474                if (value.equals(it.next().getValue())) {
475                    it.remove();
476                }
477            }
478        }
479
480        /** Normalize the given string for use as a registry identifier.
481         * @param str string to normalize
482         * @return normalized string
483         */
484        private static String normalizeString(final String str) {
485            return str.toLowerCase(Locale.ROOT);
486        }
487    }
488}