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<H> 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 }