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