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}