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