001/* 002 * Configurate 003 * Copyright (C) zml and Configurate contributors 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * 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.spongepowered.configurate.loader; 018 019import static java.util.Objects.requireNonNull; 020import static org.spongepowered.configurate.loader.ParsingException.UNKNOWN_POS; 021 022import com.google.errorprone.annotations.ForOverride; 023import net.kyori.option.Option; 024import net.kyori.option.OptionSchema; 025import net.kyori.option.OptionState; 026import net.kyori.option.value.ValueSource; 027import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 028import org.checkerframework.checker.nullness.qual.Nullable; 029import org.spongepowered.configurate.ConfigurateException; 030import org.spongepowered.configurate.ConfigurationNode; 031import org.spongepowered.configurate.ConfigurationOptions; 032import org.spongepowered.configurate.ScopedConfigurationNode; 033import org.spongepowered.configurate.reference.ConfigurationReference; 034import org.spongepowered.configurate.util.UnmodifiableCollections; 035 036import java.io.BufferedReader; 037import java.io.BufferedWriter; 038import java.io.File; 039import java.io.FileNotFoundException; 040import java.io.IOException; 041import java.io.InputStreamReader; 042import java.io.StringReader; 043import java.io.StringWriter; 044import java.io.Writer; 045import java.net.URL; 046import java.nio.charset.StandardCharsets; 047import java.nio.file.Files; 048import java.nio.file.NoSuchFileException; 049import java.nio.file.Path; 050import java.util.Iterator; 051import java.util.List; 052import java.util.concurrent.Callable; 053import java.util.function.Consumer; 054import java.util.function.UnaryOperator; 055import java.util.regex.Pattern; 056 057/** 058 * Base class for many stream-based configuration loaders. This class provides 059 * conversion from a variety of input sources to {@link BufferedReader} 060 * suppliers, providing a consistent API for loaders to read from and write to. 061 * 062 * <p>Either the source or sink may be null. If this is true, this loader may 063 * not support either loading or saving. In this case, implementing classes are 064 * expected to throw an {@link IOException} for the unsupported operation.</p> 065 * 066 * @param <N> the {@link ConfigurationNode} type produced by the loader 067 * @since 4.0.0 068 */ 069public abstract class AbstractConfigurationLoader<N extends ScopedConfigurationNode<N>> implements ConfigurationLoader<N> { 070 071 /** 072 * The escape sequence used by Configurate to separate comment lines. 073 */ 074 public static final String CONFIGURATE_LINE_SEPARATOR = "\n"; 075 076 /** 077 * A pattern that will match line breaks in comments. 078 */ 079 public static final Pattern CONFIGURATE_LINE_PATTERN = Pattern.compile(CONFIGURATE_LINE_SEPARATOR); 080 081 /** 082 * The line separator used by the system. 083 * 084 * @see System#lineSeparator() 085 */ 086 protected static final String SYSTEM_LINE_SEPARATOR = System.lineSeparator(); 087 088 /** 089 * The reader source for this loader. 090 * 091 * <p>Can be null (for loaders which don't support loading!)</p> 092 */ 093 protected final @Nullable Callable<BufferedReader> source; 094 095 /** 096 * The writer sink for this loader. 097 * 098 * <p>Can be null (for loaders which don't support saving!)</p> 099 */ 100 protected final @Nullable Callable<BufferedWriter> sink; 101 102 /** 103 * The comment handlers defined for this loader. 104 */ 105 private final List<CommentHandler> commentHandlers; 106 107 /** 108 * The mode used to read/write configuration headers. 109 */ 110 private final HeaderMode headerMode; 111 112 /** 113 * The default {@link ConfigurationOptions} used by this loader. 114 */ 115 private final ConfigurationOptions defaultOptions; 116 117 /** 118 * Create a loader instance from a builder. 119 * 120 * @param builder the user-configured builder 121 * @param commentHandlers supported comment formats for extracting the 122 * configuration header 123 * @since 4.0.0 124 */ 125 protected AbstractConfigurationLoader(final Builder<?, ?> builder, final CommentHandler[] commentHandlers) { 126 this.source = builder.source(); 127 this.sink = builder.sink(); 128 this.headerMode = builder.optionState().value(Builder.HEADER_MODE); 129 this.commentHandlers = UnmodifiableCollections.toList(commentHandlers); 130 this.defaultOptions = builder.defaultOptions(); 131 } 132 133 /** 134 * Gets the primary {@link CommentHandler} used by this loader. 135 * 136 * @return the default comment handler 137 * @since 4.0.0 138 */ 139 public CommentHandler defaultCommentHandler() { 140 return this.commentHandlers.get(0); 141 } 142 143 @Override 144 public ConfigurationReference<N> loadToReference() throws ConfigurateException { 145 return ConfigurationReference.fixed(this); 146 } 147 148 @Override 149 public N load(ConfigurationOptions options) throws ParsingException { 150 if (this.source == null) { 151 throw new ParsingException(UNKNOWN_POS, UNKNOWN_POS, "", "No source present to read from!", null); 152 } 153 try (BufferedReader reader = this.source.call()) { 154 if (this.headerMode == HeaderMode.PRESERVE || this.headerMode == HeaderMode.NONE) { 155 final @Nullable String comment = CommentHandlers.extractComment(reader, this.commentHandlers); 156 if (comment != null && comment.length() > 0) { 157 options = options.header(comment); 158 } 159 } 160 final N node = createNode(options); 161 loadInternal(node, reader); 162 return node; 163 } catch (final ParsingException ex) { 164 throw ex; 165 } catch (final FileNotFoundException | NoSuchFileException e) { 166 // Squash -- there's nothing to read 167 return createNode(options); 168 } catch (final IOException e) { 169 throw new ParsingException(UNKNOWN_POS, UNKNOWN_POS, options.header(), null, e); 170 } catch (final Exception e) { 171 throw new ParsingException(UNKNOWN_POS, UNKNOWN_POS, options.header(), "Unknown error occurred while loading", e); 172 } 173 } 174 175 /** 176 * Using a created node, attempt to read a configuration file. 177 * 178 * <p>The header will already have been read if applicable.</p> 179 * 180 * @param node node to load into 181 * @param reader reader to load from 182 * @throws ParsingException if an error occurs at any stage of loading 183 * @since 4.0.0 184 */ 185 @ForOverride 186 protected abstract void loadInternal(N node, BufferedReader reader) throws ParsingException; 187 188 @Override 189 public void save(final ConfigurationNode node) throws ConfigurateException { 190 if (this.sink == null) { 191 throw new ConfigurateException(node, "No sink present to write to!"); 192 } 193 this.checkCanWrite(node); 194 try (Writer writer = this.sink.call()) { 195 writeHeaderInternal(writer); 196 if (this.headerMode != HeaderMode.NONE) { 197 final @Nullable String header = node.options().header(); 198 if (header != null && !header.isEmpty()) { 199 final Iterator<String> lines = defaultCommentHandler().toComment(CONFIGURATE_LINE_PATTERN.splitAsStream(header)).iterator(); 200 while (lines.hasNext()) { 201 writer.write(lines.next()); 202 writer.write(SYSTEM_LINE_SEPARATOR); 203 } 204 writer.write(SYSTEM_LINE_SEPARATOR); 205 } 206 } 207 saveInternal(node, writer); 208 } catch (final ConfigurateException ex) { 209 throw ex; 210 } catch (final Exception ex) { 211 throw new ConfigurateException(node, ex); 212 } 213 } 214 215 /** 216 * Perform format-specific validation of a node. 217 * 218 * <p>This method will be called before a writer is opened, allowing the 219 * loader to perform any basic validation it may need to before it opens a 220 * writer replacing an existing file.</p> 221 * 222 * @param node the node to write 223 * @throws ConfigurateException if any invalid data is present 224 * @since 4.1.0 225 */ 226 @ForOverride 227 protected void checkCanWrite(final ConfigurationNode node) throws ConfigurateException {} 228 229 /** 230 * Write out any implementation-specific file header. 231 * 232 * @param writer writer to output to 233 * @throws IOException if an error occurs in the implementation 234 * @since 4.0.0 235 */ 236 @ForOverride 237 protected void writeHeaderInternal(final Writer writer) throws IOException {} 238 239 /** 240 * Perform a save of the node to the provided writer. 241 * 242 * @param node node to save 243 * @param writer writer to output to 244 * @throws ConfigurateException if any of the node's data is unsavable 245 * @since 4.0.0 246 */ 247 @ForOverride 248 protected abstract void saveInternal(ConfigurationNode node, Writer writer) throws ConfigurateException; 249 250 @Override 251 public ConfigurationOptions defaultOptions() { 252 return this.defaultOptions; 253 } 254 255 @Override 256 public final boolean canLoad() { 257 return this.source != null; 258 } 259 260 @Override 261 public final boolean canSave() { 262 return this.sink != null; 263 } 264 265 /** 266 * An abstract builder implementation for {@link AbstractConfigurationLoader}s. 267 * 268 * @param <T> the builder's own type (for chaining using generic types) 269 * @since 4.0.0 270 */ 271 public abstract static class Builder<T extends Builder<T, L>, L extends AbstractConfigurationLoader<?>> { 272 273 private static final String CONFIGURATE_PREFIX = "configurate"; 274 275 private static final OptionSchema.Mutable UNSAFE_SCHEMA = OptionSchema.emptySchema(); 276 protected static final OptionSchema SCHEMA = UNSAFE_SCHEMA.frozenView(); 277 278 /** 279 * How to read and emit headers discovered on documents processed by 280 * the created loader. 281 * 282 * @since 4.2.0 283 */ 284 public static final Option<HeaderMode> HEADER_MODE = UNSAFE_SCHEMA.enumOption("header", HeaderMode.class, HeaderMode.PRESERVE); 285 286 @Deprecated 287 protected HeaderMode headerMode; 288 private OptionState.@MonotonicNonNull Builder optionBuilder; 289 private @Nullable OptionState optionState; 290 protected @Nullable Callable<BufferedReader> source; 291 protected @Nullable Callable<BufferedWriter> sink; 292 protected ConfigurationOptions defaultOptions = ConfigurationOptions.defaults(); 293 294 /** 295 * Create a new builder. 296 * 297 * @since 4.0.0 298 */ 299 protected Builder() {} 300 301 /** 302 * Begin building an option state, initialized from the current 303 * environment. 304 * 305 * @return the pre-initialized option state builder 306 * @since 4.2.0 307 */ 308 protected OptionState.Builder optionStateBuilder() { 309 if (this.optionBuilder == null) { 310 this.optionState = null; 311 this.optionBuilder = this.optionSchema().stateBuilder() 312 .values(ValueSource.systemProperty(CONFIGURATE_PREFIX)) 313 .values(ValueSource.environmentVariable(CONFIGURATE_PREFIX)); 314 } 315 return this.optionBuilder; 316 } 317 318 /** 319 * Compute a snapshot of the currently set options for created loaders. 320 * 321 * @return the option state 322 * @since 4.2.0 323 */ 324 public OptionState optionState() { 325 if (this.optionState == null) { 326 this.optionState = this.optionStateBuilder().build(); 327 } 328 return this.optionState; 329 } 330 331 /** 332 * Set the option state for this loader to the provided state. 333 * 334 * <p>The provided state must be within the 335 * {@link #optionSchema() loader's schema}.</p> 336 * 337 * @param state the state 338 * @return this builder 339 * @since 4.2.0 340 */ 341 public T optionState(final OptionState state) { 342 this.optionBuilder = this.optionSchema().stateBuilder() 343 .values(state); 344 this.optionState = null; 345 346 return this.self(); 347 } 348 349 /** 350 * Modify the state of loader options set on this loader. 351 * 352 * @param builderConsumer a consumer that receives the modifier to 353 * perform changes 354 * @return this builder 355 * @since 4.2.0 356 */ 357 public T editOptions(final Consumer<OptionState.Builder> builderConsumer) { 358 this.optionState = null; 359 builderConsumer.accept(this.optionStateBuilder()); 360 return this.self(); 361 } 362 363 /** 364 * Get the schema of available options that can be set on this loader. 365 * 366 * <p>This schema should inherit from {@link #UNSAFE_SCHEMA}.</p> 367 * 368 * @return the option schema 369 * @since 4.2.0 370 */ 371 @ForOverride 372 protected OptionSchema optionSchema() { 373 return SCHEMA; // fallback 374 } 375 376 @SuppressWarnings("unchecked") 377 private T self() { 378 return (T) this; 379 } 380 381 /** 382 * Sets the sink and source of the resultant loader to the given file. 383 * 384 * <p>The {@link #source() source} is defined using 385 * {@link Files#newBufferedReader(Path)} with UTF-8 encoding.</p> 386 * 387 * <p>The {@link #sink() sink} is defined using {@link AtomicFiles} with UTF-8 388 * encoding.</p> 389 * 390 * @param file the configuration file 391 * @return this builder (for chaining) 392 * @since 4.0.0 393 */ 394 public T file(final File file) { 395 return path(requireNonNull(file, "file").toPath()); 396 } 397 398 /** 399 * Sets the sink and source of the resultant loader to the given path. 400 * 401 * <p>The {@link #source() source} is defined using 402 * {@link Files#newBufferedReader(Path)} with UTF-8 encoding.</p> 403 * 404 * <p>The {@link #sink() sink} is defined using {@link AtomicFiles} with UTF-8 405 * encoding.</p> 406 * 407 * @param path the path of the configuration file 408 * @return this builder (for chaining) 409 * @since 4.0.0 410 */ 411 public T path(final Path path) { 412 final Path absPath = requireNonNull(path, "path").toAbsolutePath(); 413 this.source = () -> Files.newBufferedReader(absPath, StandardCharsets.UTF_8); 414 this.sink = AtomicFiles.atomicWriterFactory(absPath, StandardCharsets.UTF_8); 415 return self(); 416 } 417 418 /** 419 * Sets the source of the resultant loader to the given URL. 420 * 421 * @param url the URL of the source 422 * @return this builder (for chaining) 423 * @since 4.0.0 424 */ 425 public T url(final URL url) { 426 requireNonNull(url, "url"); 427 this.source = () -> new BufferedReader(new InputStreamReader(url.openConnection().getInputStream(), StandardCharsets.UTF_8)); 428 return self(); 429 } 430 431 /** 432 * Sets the source of the resultant loader. 433 * 434 * <p>The "source" is used by the loader to load the configuration.</p> 435 * 436 * @param source the source 437 * @return this builder (for chaining) 438 * @since 4.0.0 439 */ 440 public T source(final @Nullable Callable<BufferedReader> source) { 441 this.source = source; 442 return self(); 443 } 444 445 /** 446 * Gets the source to be used by the resultant loader. 447 * 448 * @return the source 449 * @since 4.0.0 450 */ 451 public @Nullable Callable<BufferedReader> source() { 452 return this.source; 453 } 454 455 /** 456 * Sets the sink of the resultant loader. 457 * 458 * <p>The "sink" is used by the loader to save the configuration.</p> 459 * 460 * @param sink the sink 461 * @return this builder (for chaining) 462 * @since 4.0.0 463 */ 464 public T sink(final @Nullable Callable<BufferedWriter> sink) { 465 this.sink = sink; 466 return self(); 467 } 468 469 /** 470 * Gets the sink to be used by the resultant loader. 471 * 472 * @return the sink 473 * @since 4.0.0 474 */ 475 public @Nullable Callable<BufferedWriter> sink() { 476 return this.sink; 477 } 478 479 /** 480 * Sets the header mode of the resultant loader. 481 * 482 * @param mode the header mode 483 * @return this builder (for chaining) 484 * @since 4.0.0 485 */ 486 public T headerMode(final HeaderMode mode) { 487 this.optionStateBuilder().value(Builder.HEADER_MODE, mode); 488 return self(); 489 } 490 491 /** 492 * Gets the header mode to be used by the resultant loader. 493 * 494 * @return the header mode 495 * @since 4.0.0 496 */ 497 @SuppressWarnings("deprecation") 498 public HeaderMode headerMode() { 499 // manually overridden for some reason? 500 if (this.headerMode != null) { 501 this.optionStateBuilder().value(HEADER_MODE, this.headerMode); 502 this.headerMode = null; 503 } 504 return this.optionState().value(HEADER_MODE); 505 } 506 507 /** 508 * Sets the default configuration options to be used by the 509 * resultant loader. 510 * 511 * @param defaultOptions the options 512 * @return this builder (for chaining) 513 * @since 4.0.0 514 */ 515 public T defaultOptions(final ConfigurationOptions defaultOptions) { 516 this.defaultOptions = requireNonNull(defaultOptions, "defaultOptions"); 517 return self(); 518 } 519 520 /** 521 * Sets the default configuration options to be used by the resultant 522 * loader by providing a function which takes the current default 523 * options and applies any desired changes. 524 * 525 * @param defaultOptions to transform the existing default options 526 * @return this builder (for chaining) 527 * @since 4.0.0 528 */ 529 public T defaultOptions(final UnaryOperator<ConfigurationOptions> defaultOptions) { 530 this.defaultOptions = requireNonNull(defaultOptions.apply(this.defaultOptions), "defaultOptions (updated)"); 531 return self(); 532 } 533 534 /** 535 * Gets the default configuration options to be used by the resultant 536 * loader. 537 * 538 * @return the options 539 * @since 4.0.0 540 */ 541 public ConfigurationOptions defaultOptions() { 542 return this.defaultOptions; 543 } 544 545 /** 546 * Builds the loader. 547 * 548 * @return a new loader 549 * @since 4.0.0 550 */ 551 public abstract L build(); 552 553 /** 554 * Configure to read from a string, build, and load in one step. 555 * 556 * @param input the input to load 557 * @return a deserialized node 558 * @since 4.1.0 559 */ 560 public ConfigurationNode buildAndLoadString(final String input) throws ConfigurateException { 561 return this.source(() -> new BufferedReader(new StringReader(input))) 562 .build() 563 .load(); 564 } 565 566 /** 567 * Configure to write to a string, build, and save in one step. 568 * 569 * @param output the node to write 570 * @return the output string 571 * @since 4.1.0 572 */ 573 public String buildAndSaveString(final ConfigurationNode output) throws ConfigurateException { 574 requireNonNull(output, "output"); 575 final StringWriter writer = new StringWriter(); 576 this.sink(() -> new BufferedWriter(writer)) 577 .build() 578 .save(output); 579 return writer.toString(); 580 } 581 582 } 583 584}