001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.geometry.io.core; 018 019import java.text.MessageFormat; 020import java.util.ArrayList; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.Iterator; 024import java.util.List; 025import java.util.Locale; 026import java.util.Map; 027import java.util.Objects; 028import java.util.stream.Collectors; 029import java.util.stream.Stream; 030 031import org.apache.commons.geometry.core.partitioning.BoundarySource; 032import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset; 033import org.apache.commons.geometry.io.core.input.GeometryInput; 034import org.apache.commons.geometry.io.core.internal.GeometryIOUtils; 035import org.apache.commons.geometry.io.core.output.GeometryOutput; 036import org.apache.commons.numbers.core.Precision; 037 038/** Class managing IO operations for geometric data formats containing region boundaries. 039 * All IO operations are delegated to registered format-specific {@link BoundaryReadHandler read handlers} 040 * and {@link BoundaryWriteHandler write handlers}. 041 * 042 * <p><strong>Exceptions</strong> 043 * <p>Despite having functionality related to I/O operations, this class has been designed to <em>not</em> 044 * throw checked exceptions, in particular {@link java.io.IOException IOException}. The primary reasons for 045 * this choice are 046 * <ul> 047 * <li>convenience,</li> 048 * <li>compatibility with functional programming, and </li> 049 * <li>the fact that modern Java practice is moving away from checked exceptions in general (as exemplified 050 * by the JDK's {@link java.io.UncheckedIOException UncheckedIOException}).</li> 051 * </ul> 052 * As a result, any {@link java.io.IOException IOException} thrown internally by this or related classes 053 * is wrapped with {@link java.io.UncheckedIOException UncheckedIOException}. Other common runtime exceptions 054 * include {@link IllegalArgumentException}, which typically indicates mathematically invalid data, and 055 * {@link IllegalStateException}, which typically indicates format or parsing errors. See the method-level 056 * documentation for more details. 057 * 058 * <p><strong>Implementation note:</strong> Instances of this class are thread-safe as long as the 059 * registered handler instances are thread-safe.</p> 060 * @param <H> Geometric boundary type 061 * @param <B> Boundary source type 062 * @param <R> Read handler type 063 * @param <W> Write handler type 064 * @see BoundaryReadHandler 065 * @see BoundaryWriteHandler 066 * @see <a href="https://en.wikipedia.org/wiki/Boundary_representations">Boundary representations</a> 067 */ 068public class BoundaryIOManager< 069 H extends HyperplaneConvexSubset<?>, 070 B extends BoundarySource<H>, 071 R extends BoundaryReadHandler<H, B>, 072 W extends BoundaryWriteHandler<H, B>> { 073 074 /** Error message used when a handler is null. */ 075 private static final String HANDLER_NULL_ERR = "Handler cannot be null"; 076 077 /** Error message used when a format is null. */ 078 private static final String FORMAT_NULL_ERR = "Format cannot be null"; 079 080 /** Error message used when a format name is null. */ 081 private static final String FORMAT_NAME_NULL_ERR = "Format name cannot be null"; 082 083 /** Read handler registry. */ 084 private final HandlerRegistry<R> readRegistry = new HandlerRegistry<>(); 085 086 /** Write handler registry. */ 087 private final HandlerRegistry<W> writeRegistry = new HandlerRegistry<>(); 088 089 /** Register a {@link BoundaryReadHandler read handler} with the instance, replacing 090 * any handler previously registered for the argument's supported data format, as returned 091 * by {@link BoundaryReadHandler#getFormat()}. 092 * @param handler handler to register 093 * @throws NullPointerException if {@code handler}, its {@link BoundaryReadHandler#getFormat() format}, 094 * or the {@link GeometryFormat#getFormatName() format's name} are null 095 */ 096 public void registerReadHandler(final R handler) { 097 Objects.requireNonNull(handler, HANDLER_NULL_ERR); 098 readRegistry.register(handler.getFormat(), handler); 099 } 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}