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}