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}