BoundaryIOManager.java

  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. import java.text.MessageFormat;
  19. import java.util.ArrayList;
  20. import java.util.Collections;
  21. import java.util.HashMap;
  22. import java.util.Iterator;
  23. import java.util.List;
  24. import java.util.Locale;
  25. import java.util.Map;
  26. import java.util.Objects;
  27. import java.util.stream.Collectors;
  28. import java.util.stream.Stream;

  29. import org.apache.commons.geometry.core.partitioning.BoundarySource;
  30. import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
  31. import org.apache.commons.geometry.io.core.input.GeometryInput;
  32. import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
  33. import org.apache.commons.geometry.io.core.output.GeometryOutput;
  34. import org.apache.commons.numbers.core.Precision;

  35. /** Class managing IO operations for geometric data formats containing region boundaries.
  36.  * All IO operations are delegated to registered format-specific {@link BoundaryReadHandler read handlers}
  37.  * and {@link BoundaryWriteHandler write handlers}.
  38.  *
  39.  * <p><strong>Exceptions</strong>
  40.  * <p>Despite having functionality related to I/O operations, this class has been designed to <em>not</em>
  41.  * throw checked exceptions, in particular {@link java.io.IOException IOException}. The primary reasons for
  42.  * this choice are
  43.  * <ul>
  44.  *  <li>convenience,</li>
  45.  *  <li>compatibility with functional programming, and </li>
  46.  *  <li>the fact that modern Java practice is moving away from checked exceptions in general (as exemplified
  47.  *      by the JDK's {@link java.io.UncheckedIOException UncheckedIOException}).</li>
  48.  * </ul>
  49.  * As a result, any {@link java.io.IOException IOException} thrown internally by this or related classes
  50.  * is wrapped with {@link java.io.UncheckedIOException UncheckedIOException}. Other common runtime exceptions
  51.  * include {@link IllegalArgumentException}, which typically indicates mathematically invalid data, and
  52.  * {@link IllegalStateException}, which typically indicates format or parsing errors. See the method-level
  53.  * documentation for more details.
  54.  *
  55.  * <p><strong>Implementation note:</strong> Instances of this class are thread-safe as long as the
  56.  * registered handler instances are thread-safe.</p>
  57.  * @param <H> Geometric boundary type
  58.  * @param <B> Boundary source type
  59.  * @param <R> Read handler type
  60.  * @param <W> Write handler type
  61.  * @see BoundaryReadHandler
  62.  * @see BoundaryWriteHandler
  63.  * @see <a href="https://en.wikipedia.org/wiki/Boundary_representations">Boundary representations</a>
  64.  */
  65. public class BoundaryIOManager<
  66.     H extends HyperplaneConvexSubset<?>,
  67.     B extends BoundarySource<H>,
  68.     R extends BoundaryReadHandler<H, B>,
  69.     W extends BoundaryWriteHandler<H, B>> {

  70.     /** Error message used when a handler is null. */
  71.     private static final String HANDLER_NULL_ERR = "Handler cannot be null";

  72.     /** Error message used when a format is null. */
  73.     private static final String FORMAT_NULL_ERR = "Format cannot be null";

  74.     /** Error message used when a format name is null. */
  75.     private static final String FORMAT_NAME_NULL_ERR = "Format name cannot be null";

  76.     /** Read handler registry. */
  77.     private final HandlerRegistry<R> readRegistry = new HandlerRegistry<>();

  78.     /** Write handler registry. */
  79.     private final HandlerRegistry<W> writeRegistry = new HandlerRegistry<>();

  80.     /** Register a {@link BoundaryReadHandler read handler} with the instance, replacing
  81.      * any handler previously registered for the argument's supported data format, as returned
  82.      * by {@link BoundaryReadHandler#getFormat()}.
  83.      * @param handler handler to register
  84.      * @throws NullPointerException if {@code handler}, its {@link BoundaryReadHandler#getFormat() format},
  85.      *      or the {@link GeometryFormat#getFormatName() format's name} are null
  86.      */
  87.     public void registerReadHandler(final R handler) {
  88.         Objects.requireNonNull(handler, HANDLER_NULL_ERR);
  89.         readRegistry.register(handler.getFormat(), handler);
  90.     }

  91.     /** Unregister a previously registered {@link BoundaryReadHandler read handler};
  92.      * does nothing if the argument is null or is not currently registered.
  93.      * @param handler handler to unregister; may be null
  94.      */
  95.     public void unregisterReadHandler(final R handler) {
  96.         readRegistry.unregister(handler);
  97.     }

  98.     /** Get all registered {@link BoundaryReadHandler read handlers}.
  99.      * @return list containing all registered read handlers
  100.      */
  101.     public List<R> getReadHandlers() {
  102.         return readRegistry.getHandlers();
  103.     }

  104.     /** Get the list of formats supported by the currently registered
  105.      * {@link BoundaryReadHandler read handlers}.
  106.      * @return list of read formats
  107.      * @see BoundaryReadHandler#getFormat()
  108.      */
  109.     public List<GeometryFormat> getReadFormats() {
  110.         return readRegistry.getHandlers().stream()
  111.                 .map(BoundaryReadHandler::getFormat)
  112.                 .collect(Collectors.toList());
  113.     }

  114.     /** Get the {@link BoundaryReadHandler read handler} for the given format or
  115.      * null if no such handler has been registered.
  116.      * @param fmt format to obtain a handler for
  117.      * @return read handler for the given format or null if not found
  118.      */
  119.     public R getReadHandlerForFormat(final GeometryFormat fmt) {
  120.         return readRegistry.getByFormat(fmt);
  121.     }

  122.     /** Get the {@link BoundaryReadHandler read handler} for the given file extension
  123.      * or null if no such handler has been registered. File extension comparisons are
  124.      * not case-sensitive.
  125.      * @param fileExt file extension to obtain a handler for
  126.      * @return read handler for the given file extension or null if not found
  127.      * @see GeometryFormat#getFileExtensions()
  128.      */
  129.     public R getReadHandlerForFileExtension(final String fileExt) {
  130.         return readRegistry.getByFileExtension(fileExt);
  131.     }

  132.     /** Register a {@link BoundaryWriteHandler write handler} with the instance, replacing
  133.      * any handler previously registered for the argument's supported data format, as returned
  134.      * by {@link BoundaryWriteHandler#getFormat()}.
  135.      * @param handler handler to register
  136.      * @throws NullPointerException if {@code handler}, its {@link BoundaryWriteHandler#getFormat() format},
  137.      *      or the {@link GeometryFormat#getFormatName() format's name} are null
  138.      */
  139.     public void registerWriteHandler(final W handler) {
  140.         Objects.requireNonNull(handler, HANDLER_NULL_ERR);
  141.         writeRegistry.register(handler.getFormat(), handler);
  142.     }

  143.     /** Unregister a previously registered {@link BoundaryWriteHandler write handler};
  144.      * does nothing if the argument is null or is not currently registered.
  145.      * @param handler handler to unregister; may be null
  146.      */
  147.     public void unregisterWriteHandler(final W handler) {
  148.         writeRegistry.unregister(handler);
  149.     }

  150.     /** Get all registered {@link BoundaryWriteHandler write handlers}.
  151.      * @return list containing all registered write handlers
  152.      */
  153.     public List<W> getWriteHandlers() {
  154.         return writeRegistry.getHandlers();
  155.     }

  156.     /** Get the list of formats supported by the currently registered
  157.      * {@link BoundaryWriteHandler write handlers}.
  158.      * @return list of write formats
  159.      * @see BoundaryWriteHandler#getFormat()
  160.      */
  161.     public List<GeometryFormat> getWriteFormats() {
  162.         return writeRegistry.getHandlers().stream()
  163.                 .map(BoundaryWriteHandler::getFormat)
  164.                 .collect(Collectors.toList());
  165.     }

  166.     /** Get the {@link BoundaryWriteHandler write handler} for the given format or
  167.      * null if no such handler has been registered.
  168.      * @param fmt format to obtain a handler for
  169.      * @return write handler for the given format or null if not found
  170.      */
  171.     public W getWriteHandlerForFormat(final GeometryFormat fmt) {
  172.         return writeRegistry.getByFormat(fmt);
  173.     }

  174.     /** Get the {@link BoundaryWriteHandler write handler} for the given file extension
  175.      * or null if no such handler has been registered. File extension comparisons are
  176.      * not case-sensitive.
  177.      * @param fileExt file extension to obtain a handler for
  178.      * @return write handler for the given file extension or null if not found
  179.      * @see GeometryFormat#getFileExtensions()
  180.      */
  181.     public W getWriteHandlerForFileExtension(final String fileExt) {
  182.         return writeRegistry.getByFileExtension(fileExt);
  183.     }

  184.     /** Return a {@link BoundarySource} containing all boundaries from the given input.
  185.      * A runtime exception may be thrown if mathematically invalid boundaries are encountered.
  186.      * @param in input to read boundaries from
  187.      * @param fmt format of the input; if null, the format is determined implicitly from the
  188.      *      file extension of the input {@link GeometryInput#getFileName() file name}
  189.      * @param precision precision context used for floating point comparisons
  190.      * @return object containing all boundaries from the input
  191.      * @throws IllegalArgumentException if mathematically invalid data is encountered or no
  192.      *      {@link BoundaryReadHandler read handler} can be found for the input format
  193.      * @throws IllegalStateException if a data format error occurs
  194.      * @throws java.io.UncheckedIOException if an I/O error occurs
  195.      */
  196.     public B read(final GeometryInput in, final GeometryFormat fmt, final Precision.DoubleEquivalence precision) {
  197.         return requireReadHandler(in, fmt).read(in, precision);
  198.     }

  199.     /** Return a {@link Stream} providing access to all boundaries from the given input. The underlying input
  200.      * stream is closed when the returned stream is closed. Callers should therefore use the returned stream
  201.      * in a try-with-resources statement to ensure that all resources are properly released. Ex:
  202.      * <pre>
  203.      *  try (Stream&lt;H&gt; stream = manager.boundaries(in, fmt, precision)) {
  204.      *      // access stream content
  205.      *  }
  206.      *  </pre>
  207.      * <p>The following exceptions may be thrown during stream iteration:
  208.      *  <ul>
  209.      *      <li>{@link IllegalArgumentException} if mathematically invalid data is encountered</li>
  210.      *      <li>{@link IllegalStateException} if a data format error occurs</li>
  211.      *      <li>{@link java.io.UncheckedIOException UncheckedIOException} if an I/O error occurs</li>
  212.      *  </ul>
  213.      * @param in input to read boundaries from
  214.      * @param fmt format of the input; if null, the format is determined implicitly from the
  215.      *      file extension of the input {@link GeometryInput#getFileName() file name}
  216.      * @param precision precision context used for floating point comparisons
  217.      * @return stream providing access to all boundaries from the input
  218.      * @throws IllegalArgumentException if no {@link BoundaryReadHandler read handler} can be found for
  219.      *      the input format
  220.      * @throws IllegalStateException if a data format error occurs during stream creation
  221.      * @throws java.io.UncheckedIOException if an I/O error occurs during stream creation
  222.      */
  223.     public Stream<H> boundaries(final GeometryInput in, final GeometryFormat fmt,
  224.             final Precision.DoubleEquivalence precision) {
  225.         return requireReadHandler(in, fmt).boundaries(in, precision);
  226.     }

  227.     /** Write all boundaries from {@code src} to the given output.
  228.      * @param src object containing boundaries to write
  229.      * @param out output to write boundaries to
  230.      * @param fmt format of the output; if null, the format is determined implicitly from the
  231.      *      file extension of the output {@link GeometryOutput#getFileName()}
  232.      * @throws IllegalArgumentException if no {@link BoundaryWriteHandler write handler} can be found
  233.      *      for the output format
  234.      * @throws java.io.UncheckedIOException if an I/O error occurs
  235.      */
  236.     public void write(final B src, final GeometryOutput out, final GeometryFormat fmt) {
  237.         requireWriteHandler(out, fmt).write(src, out);
  238.     }

  239.     /** Get the {@link BoundaryReadHandler read handler} matching the arguments, throwing an exception
  240.      * on failure. If {@code fmt} is given, the handler registered for that format is returned and the
  241.      * {@code input} object is not examined. If {@code fmt} is null, the file extension of the input
  242.      * {@link GeometryInput#getFileName() file name} is used to implicitly determine the format and locate
  243.      * the handler.
  244.      * @param in input object
  245.      * @param fmt format; may be null
  246.      * @return the read handler for {@code fmt} or, if {@code fmt} is null, the read handler for the
  247.      *      file extension indicated by the input
  248.      * @throws NullPointerException if {@code in} is null
  249.      * @throws IllegalArgumentException if no matching handler can be found
  250.      */
  251.     protected R requireReadHandler(final GeometryInput in, final GeometryFormat fmt) {
  252.         Objects.requireNonNull(in, "Input cannot be null");
  253.         return readRegistry.requireHandlerByFormatOrFileName(fmt, in.getFileName());
  254.     }

  255.     /** Get the {@link BoundaryWriteHandler write handler} matching the arguments, throwing an exception
  256.      * on failure. If {@code fmt} is given, the handler registered for that format is returned and the
  257.      * {@code input} object is not examined. If {@code fmt} is null, the file extension of the output
  258.      * {@link GeometryOutput#getFileName() file name} is used to implicitly determine the format and locate
  259.      * the handler.
  260.      * @param out output object
  261.      * @param fmt format; may be null
  262.      * @return the write handler for {@code fmt} or, if {@code fmt} is null, the write handler for the
  263.      *      file extension indicated by the output
  264.      * @throws NullPointerException if {@code out} is null
  265.      * @throws IllegalArgumentException if no matching handler can be found
  266.      */
  267.     protected W requireWriteHandler(final GeometryOutput out, final GeometryFormat fmt) {
  268.         Objects.requireNonNull(out, "Output cannot be null");
  269.         return writeRegistry.requireHandlerByFormatOrFileName(fmt, out.getFileName());
  270.     }

  271.     /** Internal class used to manage handler registration. Instances of this class
  272.      * are thread-safe.
  273.      * @param <T> Handler type
  274.      */
  275.     private static final class HandlerRegistry<T> {

  276.         /** List of registered handlers. */
  277.         private final List<T> handlers = new ArrayList<>();

  278.         /** Handlers keyed by lower-case format name. */
  279.         private final Map<String, T> handlersByFormatName = new HashMap<>();

  280.         /** Handlers keyed by lower-case file extension. */
  281.         private final Map<String, T> handlersByFileExtension = new HashMap<>();

  282.         /** Register a handler for the given {@link GeometryFormat format}.
  283.          * @param fmt format for the handler
  284.          * @param handler handler to register
  285.          * @throws NullPointerException if either argument is null
  286.          */
  287.         public synchronized void register(final GeometryFormat fmt, final T handler) {
  288.             Objects.requireNonNull(fmt, FORMAT_NULL_ERR);
  289.             Objects.requireNonNull(handler, HANDLER_NULL_ERR);

  290.             if (!handlers.contains(handler)) {
  291.                 // remove any previously registered handler
  292.                 unregisterFormat(fmt);

  293.                 // add the new handler
  294.                 addToFormat(fmt.getFormatName(), handler);
  295.                 addToFileExtensions(fmt.getFileExtensions(), handler);

  296.                 handlers.add(handler);
  297.             }
  298.         }

  299.         /** Unregister the given handler.
  300.          * @param handler handler to unregister
  301.          */
  302.         public synchronized void unregister(final T handler) {
  303.             if (handler != null && handlers.remove(handler)) {
  304.                 removeValue(handlersByFormatName, handler);
  305.                 removeValue(handlersByFileExtension, handler);
  306.             }
  307.         }

  308.         /** Unregister the current handler for the given format and return it.
  309.          * Null is returned if no handler was registered.
  310.          * @param fmt format to unregister
  311.          * @return handler instance previously registered for the format or null
  312.          *      if not found
  313.          */
  314.         public synchronized T unregisterFormat(final GeometryFormat fmt) {
  315.             final T handler = getByFormat(fmt);
  316.             if (handler != null) {
  317.                 unregister(handler);
  318.             }
  319.             return handler;
  320.         }

  321.         /** Get all registered handlers.
  322.          * @return list of all registered handlers
  323.          */
  324.         public synchronized List<T> getHandlers() {
  325.             return Collections.unmodifiableList(new ArrayList<>(handlers));
  326.         }

  327.         /** Get the first handler registered for the given format, or null if
  328.          * not found.
  329.          * @param fmt format to obtain a handler for
  330.          * @return first handler registered for the format
  331.          */
  332.         public synchronized T getByFormat(final GeometryFormat fmt) {
  333.             if (fmt != null) {
  334.                 return getByNormalizedKey(handlersByFormatName, fmt.getFormatName());
  335.             }
  336.             return null;
  337.         }

  338.         /** Get the first handler registered for the given file extension or null if not found.
  339.          * @param fileExt file extension
  340.          * @return first handler registered for the given file extension or null if not found
  341.          */
  342.         public synchronized T getByFileExtension(final String fileExt) {
  343.             return getByNormalizedKey(handlersByFileExtension, fileExt);
  344.         }

  345.         /** Get the handler for the given format or file extension, throwing an exception if one
  346.          * cannot be found. If {@code fmt} is not null, it is used to directly look up the handler
  347.          * and the {@code fileName} argument is ignored. Otherwise, the file extension is extracted
  348.          * from {@code fileName} and used to look up the handler.
  349.          * @param fmt format to look up; if present, {@code fileName} is ignored
  350.          * @param fileName file name to use for the look up if {@code fmt} is null
  351.          * @return the handler matching the arguments
  352.          * @throws IllegalArgumentException if a handler cannot be found
  353.          */
  354.         public synchronized T requireHandlerByFormatOrFileName(final GeometryFormat fmt, final String fileName) {
  355.             T handler = null;
  356.             if (fmt != null) {
  357.                 handler = getByFormat(fmt);

  358.                 if (handler == null) {
  359.                     throw new IllegalArgumentException(MessageFormat.format(
  360.                             "Failed to find handler for format \"{0}\"", fmt.getFormatName()));
  361.                 }
  362.             } else {
  363.                 final String fileExt = GeometryIOUtils.getFileExtension(fileName);
  364.                 if (fileExt != null && !fileExt.isEmpty()) {
  365.                     handler = getByFileExtension(fileExt);

  366.                     if (handler == null) {
  367.                         throw new IllegalArgumentException(MessageFormat.format(
  368.                                "Failed to find handler for file extension \"{0}\"", fileExt));
  369.                     }
  370.                 } else {
  371.                     throw new IllegalArgumentException(
  372.                             "Failed to find handler: no format specified and no file extension available");
  373.                 }
  374.             }

  375.             return handler;
  376.         }

  377.         /** Add the handler to the internal format name map.
  378.          * @param fmtName format name
  379.          * @param handler handler to add
  380.          * @throws NullPointerException if {@code fmtName} is null
  381.          */
  382.         private void addToFormat(final String fmtName, final T handler) {
  383.             Objects.requireNonNull(fmtName, FORMAT_NAME_NULL_ERR);
  384.             handlersByFormatName.put(normalizeString(fmtName), handler);
  385.         }

  386.         /** Add the handler to the internal file extension map under each file extension.
  387.          * @param fileExts file extensions to map to the handler
  388.          * @param handler handler to add to the file extension map
  389.          */
  390.         private void addToFileExtensions(final List<String> fileExts, final T handler) {
  391.             if (fileExts != null) {
  392.                 for (final String fileExt : fileExts) {
  393.                     addToFileExtension(fileExt, handler);
  394.                 }
  395.             }
  396.         }

  397.         /** Add the handler to the internal file extension map.
  398.          * @param fileExt file extension to map to the handler
  399.          * @param handler handler to add to the file extension map
  400.          */
  401.         private void addToFileExtension(final String fileExt, final T handler) {
  402.             if (fileExt != null) {
  403.                 handlersByFileExtension.put(normalizeString(fileExt), handler);
  404.             }
  405.         }

  406.         /** Normalize the given key and return its associated value in the map, or null
  407.          * if not found.
  408.          * @param <V> Value type
  409.          * @param map map to search
  410.          * @param key unnormalized map key
  411.          * @return the value associated with the key after normalization, or null if not found
  412.          */
  413.         private static <V> V getByNormalizedKey(final Map<String, V> map, final String key) {
  414.             if (key != null) {
  415.                 return map.get(normalizeString(key));
  416.             }
  417.             return null;
  418.         }

  419.         /** Remove all keys that map to {@code value}.
  420.          * @param <V> Value type
  421.          * @param map map to remove keys from
  422.          * @param value value to remove from all entries in the map
  423.          */
  424.         private static <V> void removeValue(final Map<String, V> map, final V value) {
  425.             final Iterator<Map.Entry<String, V>> it = map.entrySet().iterator();
  426.             while (it.hasNext()) {
  427.                 if (value.equals(it.next().getValue())) {
  428.                     it.remove();
  429.                 }
  430.             }
  431.         }

  432.         /** Normalize the given string for use as a registry identifier.
  433.          * @param str string to normalize
  434.          * @return normalized string
  435.          */
  436.         private static String normalizeString(final String str) {
  437.             return str.toLowerCase(Locale.ROOT);
  438.         }
  439.     }
  440. }