BoundaryIOManager.java
- /*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package org.apache.commons.geometry.io.core;
- import java.text.MessageFormat;
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.HashMap;
- import java.util.Iterator;
- import java.util.List;
- import java.util.Locale;
- import java.util.Map;
- import java.util.Objects;
- import java.util.stream.Collectors;
- import java.util.stream.Stream;
- import org.apache.commons.geometry.core.partitioning.BoundarySource;
- import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
- import org.apache.commons.geometry.io.core.input.GeometryInput;
- import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
- import org.apache.commons.geometry.io.core.output.GeometryOutput;
- import org.apache.commons.numbers.core.Precision;
- /** Class managing IO operations for geometric data formats containing region boundaries.
- * All IO operations are delegated to registered format-specific {@link BoundaryReadHandler read handlers}
- * and {@link BoundaryWriteHandler write handlers}.
- *
- * <p><strong>Exceptions</strong>
- * <p>Despite having functionality related to I/O operations, this class has been designed to <em>not</em>
- * throw checked exceptions, in particular {@link java.io.IOException IOException}. The primary reasons for
- * this choice are
- * <ul>
- * <li>convenience,</li>
- * <li>compatibility with functional programming, and </li>
- * <li>the fact that modern Java practice is moving away from checked exceptions in general (as exemplified
- * by the JDK's {@link java.io.UncheckedIOException UncheckedIOException}).</li>
- * </ul>
- * As a result, any {@link java.io.IOException IOException} thrown internally by this or related classes
- * is wrapped with {@link java.io.UncheckedIOException UncheckedIOException}. Other common runtime exceptions
- * include {@link IllegalArgumentException}, which typically indicates mathematically invalid data, and
- * {@link IllegalStateException}, which typically indicates format or parsing errors. See the method-level
- * documentation for more details.
- *
- * <p><strong>Implementation note:</strong> Instances of this class are thread-safe as long as the
- * registered handler instances are thread-safe.</p>
- * @param <H> Geometric boundary type
- * @param <B> Boundary source type
- * @param <R> Read handler type
- * @param <W> Write handler type
- * @see BoundaryReadHandler
- * @see BoundaryWriteHandler
- * @see <a href="https://en.wikipedia.org/wiki/Boundary_representations">Boundary representations</a>
- */
- public class BoundaryIOManager<
- H extends HyperplaneConvexSubset<?>,
- B extends BoundarySource<H>,
- R extends BoundaryReadHandler<H, B>,
- W extends BoundaryWriteHandler<H, B>> {
- /** Error message used when a handler is null. */
- private static final String HANDLER_NULL_ERR = "Handler cannot be null";
- /** Error message used when a format is null. */
- private static final String FORMAT_NULL_ERR = "Format cannot be null";
- /** Error message used when a format name is null. */
- private static final String FORMAT_NAME_NULL_ERR = "Format name cannot be null";
- /** Read handler registry. */
- private final HandlerRegistry<R> readRegistry = new HandlerRegistry<>();
- /** Write handler registry. */
- private final HandlerRegistry<W> writeRegistry = new HandlerRegistry<>();
- /** Register a {@link BoundaryReadHandler read handler} with the instance, replacing
- * any handler previously registered for the argument's supported data format, as returned
- * by {@link BoundaryReadHandler#getFormat()}.
- * @param handler handler to register
- * @throws NullPointerException if {@code handler}, its {@link BoundaryReadHandler#getFormat() format},
- * or the {@link GeometryFormat#getFormatName() format's name} are null
- */
- public void registerReadHandler(final R handler) {
- Objects.requireNonNull(handler, HANDLER_NULL_ERR);
- readRegistry.register(handler.getFormat(), handler);
- }
- /** Unregister a previously registered {@link BoundaryReadHandler read handler};
- * does nothing if the argument is null or is not currently registered.
- * @param handler handler to unregister; may be null
- */
- public void unregisterReadHandler(final R handler) {
- readRegistry.unregister(handler);
- }
- /** Get all registered {@link BoundaryReadHandler read handlers}.
- * @return list containing all registered read handlers
- */
- public List<R> getReadHandlers() {
- return readRegistry.getHandlers();
- }
- /** Get the list of formats supported by the currently registered
- * {@link BoundaryReadHandler read handlers}.
- * @return list of read formats
- * @see BoundaryReadHandler#getFormat()
- */
- public List<GeometryFormat> getReadFormats() {
- return readRegistry.getHandlers().stream()
- .map(BoundaryReadHandler::getFormat)
- .collect(Collectors.toList());
- }
- /** Get the {@link BoundaryReadHandler read handler} for the given format or
- * null if no such handler has been registered.
- * @param fmt format to obtain a handler for
- * @return read handler for the given format or null if not found
- */
- public R getReadHandlerForFormat(final GeometryFormat fmt) {
- return readRegistry.getByFormat(fmt);
- }
- /** Get the {@link BoundaryReadHandler read handler} for the given file extension
- * or null if no such handler has been registered. File extension comparisons are
- * not case-sensitive.
- * @param fileExt file extension to obtain a handler for
- * @return read handler for the given file extension or null if not found
- * @see GeometryFormat#getFileExtensions()
- */
- public R getReadHandlerForFileExtension(final String fileExt) {
- return readRegistry.getByFileExtension(fileExt);
- }
- /** Register a {@link BoundaryWriteHandler write handler} with the instance, replacing
- * any handler previously registered for the argument's supported data format, as returned
- * by {@link BoundaryWriteHandler#getFormat()}.
- * @param handler handler to register
- * @throws NullPointerException if {@code handler}, its {@link BoundaryWriteHandler#getFormat() format},
- * or the {@link GeometryFormat#getFormatName() format's name} are null
- */
- public void registerWriteHandler(final W handler) {
- Objects.requireNonNull(handler, HANDLER_NULL_ERR);
- writeRegistry.register(handler.getFormat(), handler);
- }
- /** Unregister a previously registered {@link BoundaryWriteHandler write handler};
- * does nothing if the argument is null or is not currently registered.
- * @param handler handler to unregister; may be null
- */
- public void unregisterWriteHandler(final W handler) {
- writeRegistry.unregister(handler);
- }
- /** Get all registered {@link BoundaryWriteHandler write handlers}.
- * @return list containing all registered write handlers
- */
- public List<W> getWriteHandlers() {
- return writeRegistry.getHandlers();
- }
- /** Get the list of formats supported by the currently registered
- * {@link BoundaryWriteHandler write handlers}.
- * @return list of write formats
- * @see BoundaryWriteHandler#getFormat()
- */
- public List<GeometryFormat> getWriteFormats() {
- return writeRegistry.getHandlers().stream()
- .map(BoundaryWriteHandler::getFormat)
- .collect(Collectors.toList());
- }
- /** Get the {@link BoundaryWriteHandler write handler} for the given format or
- * null if no such handler has been registered.
- * @param fmt format to obtain a handler for
- * @return write handler for the given format or null if not found
- */
- public W getWriteHandlerForFormat(final GeometryFormat fmt) {
- return writeRegistry.getByFormat(fmt);
- }
- /** Get the {@link BoundaryWriteHandler write handler} for the given file extension
- * or null if no such handler has been registered. File extension comparisons are
- * not case-sensitive.
- * @param fileExt file extension to obtain a handler for
- * @return write handler for the given file extension or null if not found
- * @see GeometryFormat#getFileExtensions()
- */
- public W getWriteHandlerForFileExtension(final String fileExt) {
- return writeRegistry.getByFileExtension(fileExt);
- }
- /** Return a {@link BoundarySource} containing all boundaries from the given input.
- * A runtime exception may be thrown if mathematically invalid boundaries are encountered.
- * @param in input to read boundaries from
- * @param fmt format of the input; if null, the format is determined implicitly from the
- * file extension of the input {@link GeometryInput#getFileName() file name}
- * @param precision precision context used for floating point comparisons
- * @return object containing all boundaries from the input
- * @throws IllegalArgumentException if mathematically invalid data is encountered or no
- * {@link BoundaryReadHandler read handler} can be found for the input format
- * @throws IllegalStateException if a data format error occurs
- * @throws java.io.UncheckedIOException if an I/O error occurs
- */
- public B read(final GeometryInput in, final GeometryFormat fmt, final Precision.DoubleEquivalence precision) {
- return requireReadHandler(in, fmt).read(in, precision);
- }
- /** Return a {@link Stream} providing access to all boundaries from the given input. The underlying input
- * stream is closed when the returned stream is closed. Callers should therefore use the returned stream
- * in a try-with-resources statement to ensure that all resources are properly released. Ex:
- * <pre>
- * try (Stream<H> stream = manager.boundaries(in, fmt, precision)) {
- * // access stream content
- * }
- * </pre>
- * <p>The following exceptions may be thrown during stream iteration:
- * <ul>
- * <li>{@link IllegalArgumentException} if mathematically invalid data is encountered</li>
- * <li>{@link IllegalStateException} if a data format error occurs</li>
- * <li>{@link java.io.UncheckedIOException UncheckedIOException} if an I/O error occurs</li>
- * </ul>
- * @param in input to read boundaries from
- * @param fmt format of the input; if null, the format is determined implicitly from the
- * file extension of the input {@link GeometryInput#getFileName() file name}
- * @param precision precision context used for floating point comparisons
- * @return stream providing access to all boundaries from the input
- * @throws IllegalArgumentException if no {@link BoundaryReadHandler read handler} can be found for
- * the input format
- * @throws IllegalStateException if a data format error occurs during stream creation
- * @throws java.io.UncheckedIOException if an I/O error occurs during stream creation
- */
- public Stream<H> boundaries(final GeometryInput in, final GeometryFormat fmt,
- final Precision.DoubleEquivalence precision) {
- return requireReadHandler(in, fmt).boundaries(in, precision);
- }
- /** Write all boundaries from {@code src} to the given output.
- * @param src object containing boundaries to write
- * @param out output to write boundaries to
- * @param fmt format of the output; if null, the format is determined implicitly from the
- * file extension of the output {@link GeometryOutput#getFileName()}
- * @throws IllegalArgumentException if no {@link BoundaryWriteHandler write handler} can be found
- * for the output format
- * @throws java.io.UncheckedIOException if an I/O error occurs
- */
- public void write(final B src, final GeometryOutput out, final GeometryFormat fmt) {
- requireWriteHandler(out, fmt).write(src, out);
- }
- /** Get the {@link BoundaryReadHandler read handler} matching the arguments, throwing an exception
- * on failure. If {@code fmt} is given, the handler registered for that format is returned and the
- * {@code input} object is not examined. If {@code fmt} is null, the file extension of the input
- * {@link GeometryInput#getFileName() file name} is used to implicitly determine the format and locate
- * the handler.
- * @param in input object
- * @param fmt format; may be null
- * @return the read handler for {@code fmt} or, if {@code fmt} is null, the read handler for the
- * file extension indicated by the input
- * @throws NullPointerException if {@code in} is null
- * @throws IllegalArgumentException if no matching handler can be found
- */
- protected R requireReadHandler(final GeometryInput in, final GeometryFormat fmt) {
- Objects.requireNonNull(in, "Input cannot be null");
- return readRegistry.requireHandlerByFormatOrFileName(fmt, in.getFileName());
- }
- /** Get the {@link BoundaryWriteHandler write handler} matching the arguments, throwing an exception
- * on failure. If {@code fmt} is given, the handler registered for that format is returned and the
- * {@code input} object is not examined. If {@code fmt} is null, the file extension of the output
- * {@link GeometryOutput#getFileName() file name} is used to implicitly determine the format and locate
- * the handler.
- * @param out output object
- * @param fmt format; may be null
- * @return the write handler for {@code fmt} or, if {@code fmt} is null, the write handler for the
- * file extension indicated by the output
- * @throws NullPointerException if {@code out} is null
- * @throws IllegalArgumentException if no matching handler can be found
- */
- protected W requireWriteHandler(final GeometryOutput out, final GeometryFormat fmt) {
- Objects.requireNonNull(out, "Output cannot be null");
- return writeRegistry.requireHandlerByFormatOrFileName(fmt, out.getFileName());
- }
- /** Internal class used to manage handler registration. Instances of this class
- * are thread-safe.
- * @param <T> Handler type
- */
- private static final class HandlerRegistry<T> {
- /** List of registered handlers. */
- private final List<T> handlers = new ArrayList<>();
- /** Handlers keyed by lower-case format name. */
- private final Map<String, T> handlersByFormatName = new HashMap<>();
- /** Handlers keyed by lower-case file extension. */
- private final Map<String, T> handlersByFileExtension = new HashMap<>();
- /** Register a handler for the given {@link GeometryFormat format}.
- * @param fmt format for the handler
- * @param handler handler to register
- * @throws NullPointerException if either argument is null
- */
- public synchronized void register(final GeometryFormat fmt, final T handler) {
- Objects.requireNonNull(fmt, FORMAT_NULL_ERR);
- Objects.requireNonNull(handler, HANDLER_NULL_ERR);
- if (!handlers.contains(handler)) {
- // remove any previously registered handler
- unregisterFormat(fmt);
- // add the new handler
- addToFormat(fmt.getFormatName(), handler);
- addToFileExtensions(fmt.getFileExtensions(), handler);
- handlers.add(handler);
- }
- }
- /** Unregister the given handler.
- * @param handler handler to unregister
- */
- public synchronized void unregister(final T handler) {
- if (handler != null && handlers.remove(handler)) {
- removeValue(handlersByFormatName, handler);
- removeValue(handlersByFileExtension, handler);
- }
- }
- /** Unregister the current handler for the given format and return it.
- * Null is returned if no handler was registered.
- * @param fmt format to unregister
- * @return handler instance previously registered for the format or null
- * if not found
- */
- public synchronized T unregisterFormat(final GeometryFormat fmt) {
- final T handler = getByFormat(fmt);
- if (handler != null) {
- unregister(handler);
- }
- return handler;
- }
- /** Get all registered handlers.
- * @return list of all registered handlers
- */
- public synchronized List<T> getHandlers() {
- return Collections.unmodifiableList(new ArrayList<>(handlers));
- }
- /** Get the first handler registered for the given format, or null if
- * not found.
- * @param fmt format to obtain a handler for
- * @return first handler registered for the format
- */
- public synchronized T getByFormat(final GeometryFormat fmt) {
- if (fmt != null) {
- return getByNormalizedKey(handlersByFormatName, fmt.getFormatName());
- }
- return null;
- }
- /** Get the first handler registered for the given file extension or null if not found.
- * @param fileExt file extension
- * @return first handler registered for the given file extension or null if not found
- */
- public synchronized T getByFileExtension(final String fileExt) {
- return getByNormalizedKey(handlersByFileExtension, fileExt);
- }
- /** Get the handler for the given format or file extension, throwing an exception if one
- * cannot be found. If {@code fmt} is not null, it is used to directly look up the handler
- * and the {@code fileName} argument is ignored. Otherwise, the file extension is extracted
- * from {@code fileName} and used to look up the handler.
- * @param fmt format to look up; if present, {@code fileName} is ignored
- * @param fileName file name to use for the look up if {@code fmt} is null
- * @return the handler matching the arguments
- * @throws IllegalArgumentException if a handler cannot be found
- */
- public synchronized T requireHandlerByFormatOrFileName(final GeometryFormat fmt, final String fileName) {
- T handler = null;
- if (fmt != null) {
- handler = getByFormat(fmt);
- if (handler == null) {
- throw new IllegalArgumentException(MessageFormat.format(
- "Failed to find handler for format \"{0}\"", fmt.getFormatName()));
- }
- } else {
- final String fileExt = GeometryIOUtils.getFileExtension(fileName);
- if (fileExt != null && !fileExt.isEmpty()) {
- handler = getByFileExtension(fileExt);
- if (handler == null) {
- throw new IllegalArgumentException(MessageFormat.format(
- "Failed to find handler for file extension \"{0}\"", fileExt));
- }
- } else {
- throw new IllegalArgumentException(
- "Failed to find handler: no format specified and no file extension available");
- }
- }
- return handler;
- }
- /** Add the handler to the internal format name map.
- * @param fmtName format name
- * @param handler handler to add
- * @throws NullPointerException if {@code fmtName} is null
- */
- private void addToFormat(final String fmtName, final T handler) {
- Objects.requireNonNull(fmtName, FORMAT_NAME_NULL_ERR);
- handlersByFormatName.put(normalizeString(fmtName), handler);
- }
- /** Add the handler to the internal file extension map under each file extension.
- * @param fileExts file extensions to map to the handler
- * @param handler handler to add to the file extension map
- */
- private void addToFileExtensions(final List<String> fileExts, final T handler) {
- if (fileExts != null) {
- for (final String fileExt : fileExts) {
- addToFileExtension(fileExt, handler);
- }
- }
- }
- /** Add the handler to the internal file extension map.
- * @param fileExt file extension to map to the handler
- * @param handler handler to add to the file extension map
- */
- private void addToFileExtension(final String fileExt, final T handler) {
- if (fileExt != null) {
- handlersByFileExtension.put(normalizeString(fileExt), handler);
- }
- }
- /** Normalize the given key and return its associated value in the map, or null
- * if not found.
- * @param <V> Value type
- * @param map map to search
- * @param key unnormalized map key
- * @return the value associated with the key after normalization, or null if not found
- */
- private static <V> V getByNormalizedKey(final Map<String, V> map, final String key) {
- if (key != null) {
- return map.get(normalizeString(key));
- }
- return null;
- }
- /** Remove all keys that map to {@code value}.
- * @param <V> Value type
- * @param map map to remove keys from
- * @param value value to remove from all entries in the map
- */
- private static <V> void removeValue(final Map<String, V> map, final V value) {
- final Iterator<Map.Entry<String, V>> it = map.entrySet().iterator();
- while (it.hasNext()) {
- if (value.equals(it.next().getValue())) {
- it.remove();
- }
- }
- }
- /** Normalize the given string for use as a registry identifier.
- * @param str string to normalize
- * @return normalized string
- */
- private static String normalizeString(final String str) {
- return str.toLowerCase(Locale.ROOT);
- }
- }
- }