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