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}