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 }