View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements.  See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache License, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License.  You may obtain a copy of the License at
    8    *
    9    *      http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  package org.apache.commons.geometry.io.core;
    18
    19  import java.text.MessageFormat;
    20  import java.util.ArrayList;
    21  import java.util.Collections;
    22  import java.util.HashMap;
    23  import java.util.Iterator;
    24  import java.util.List;
    25  import java.util.Locale;
    26  import java.util.Map;
    27  import java.util.Objects;
    28  import java.util.stream.Collectors;
    29  import java.util.stream.Stream;
    30
    31  import org.apache.commons.geometry.core.partitioning.BoundarySource;
    32  import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
    33  import org.apache.commons.geometry.io.core.input.GeometryInput;
    34  import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
    35  import org.apache.commons.geometry.io.core.output.GeometryOutput;
    36  import org.apache.commons.numbers.core.Precision;
    37
    38  /** Class managing IO operations for geometric data formats containing region boundaries.
    39   * All IO operations are delegated to registered format-specific {@link BoundaryReadHandler read handlers}
    40   * and {@link BoundaryWriteHandler write handlers}.
    41   *
    42   * <p><strong>Exceptions</strong>
    43   * <p>Despite having functionality related to I/O operations, this class has been designed to <em>not</em>
    44   * throw checked exceptions, in particular {@link java.io.IOException IOException}. The primary reasons for
    45   * this choice are
    46   * <ul>
    47   *  <li>convenience,</li>
    48   *  <li>compatibility with functional programming, and </li>
    49   *  <li>the fact that modern Java practice is moving away from checked exceptions in general (as exemplified
    50   *      by the JDK's {@link java.io.UncheckedIOException UncheckedIOException}).</li>
    51   * </ul>
    52   * As a result, any {@link java.io.IOException IOException} thrown internally by this or related classes
    53   * is wrapped with {@link java.io.UncheckedIOException UncheckedIOException}. Other common runtime exceptions
    54   * include {@link IllegalArgumentException}, which typically indicates mathematically invalid data, and
    55   * {@link IllegalStateException}, which typically indicates format or parsing errors. See the method-level
    56   * documentation for more details.
    57   *
    58   * <p><strong>Implementation note:</strong> Instances of this class are thread-safe as long as the
    59   * registered handler instances are thread-safe.</p>
    60   * @param <H> Geometric boundary type
    61   * @param <B> Boundary source type
    62   * @param <R> Read handler type
    63   * @param <W> Write handler type
    64   * @see BoundaryReadHandler
    65   * @see BoundaryWriteHandler
    66   * @see <a href="https://en.wikipedia.org/wiki/Boundary_representations">Boundary representations</a>
    67   */
    68  public class BoundaryIOManager<
    69      H extends HyperplaneConvexSubset<?>,
    70      B extends BoundarySource<H>,
    71      R extends BoundaryReadHandler<H, B>,
    72      W extends BoundaryWriteHandler<H, B>> {
    73
    74      /** Error message used when a handler is null. */
    75      private static final String HANDLER_NULL_ERR = "Handler cannot be null";
    76
    77      /** Error message used when a format is null. */
    78      private static final String FORMAT_NULL_ERR = "Format cannot be null";
    79
    80      /** Error message used when a format name is null. */
    81      private static final String FORMAT_NAME_NULL_ERR = "Format name cannot be null";
    82
    83      /** Read handler registry. */
    84      private final HandlerRegistry<R> readRegistry = new HandlerRegistry<>();
    85
    86      /** Write handler registry. */
    87      private final HandlerRegistry<W> writeRegistry = new HandlerRegistry<>();
    88
    89      /** Register a {@link BoundaryReadHandler read handler} with the instance, replacing
    90       * any handler previously registered for the argument's supported data format, as returned
    91       * by {@link BoundaryReadHandler#getFormat()}.
    92       * @param handler handler to register
    93       * @throws NullPointerException if {@code handler}, its {@link BoundaryReadHandler#getFormat() format},
    94       *      or the {@link GeometryFormat#getFormatName() format's name} are null
    95       */
    96      public void registerReadHandler(final R handler) {
    97          Objects.requireNonNull(handler, HANDLER_NULL_ERR);
    98          readRegistry.register(handler.getFormat(), handler);
    99      }
    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 }