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.transformation;
018
019import static java.util.Objects.requireNonNull;
020
021import org.checkerframework.checker.nullness.qual.NonNull;
022import org.spongepowered.configurate.ConfigurateException;
023import org.spongepowered.configurate.ConfigurationNode;
024import org.spongepowered.configurate.NodePath;
025
026import java.util.NavigableMap;
027import java.util.TreeMap;
028import java.util.function.Consumer;
029
030/**
031 * Represents a set of transformations on a configuration.
032 *
033 * @since 4.0.0
034 */
035@FunctionalInterface
036public interface ConfigurationTransformation {
037
038    /**
039     * A special object that represents a wildcard in a path provided to a
040     * configuration transformer.
041     *
042     * @since 4.0.0
043     */
044    Object WILDCARD_OBJECT = new Object();
045
046    /**
047     * Get an empty transformation.
048     *
049     * <p>This transformation will perform no actions.</p>
050     *
051     * @return empty transformation
052     * @since 4.0.0
053     */
054    static ConfigurationTransformation empty() {
055        return node -> {};
056    }
057
058    /**
059     * Create a new builder to create a basic configuration transformation.
060     *
061     * @return a new transformation builder.
062     * @since 4.0.0
063     */
064    static Builder builder() {
065        return new Builder();
066    }
067
068    /**
069     * This creates a builder for versioned transformations.
070     *
071     * @return a new builder for versioned transformations
072     * @since 4.0.0
073     */
074    static VersionedBuilder versionedBuilder() {
075        return new VersionedBuilder();
076    }
077
078    /**
079     * Creates a chain of {@link ConfigurationTransformation}s.
080     *
081     * @param transformations the transformations
082     * @return a new transformation chain
083     * @since 4.0.0
084     */
085    static ConfigurationTransformation chain(final ConfigurationTransformation... transformations) {
086        if (requireNonNull(transformations, "transformations").length == 0) {
087            throw new IllegalArgumentException("Cannot chain an empty array of transformations!");
088        }
089
090        if (transformations.length == 1) {
091            return transformations[0];
092        } else {
093            return new ChainedConfigurationTransformation(transformations);
094        }
095    }
096
097    /**
098     * Apply this transformation to a given node.
099     *
100     * @param node the target node
101     * @since 4.0.0
102     */
103    void apply(ConfigurationNode node) throws ConfigurateException;
104
105    /**
106     * Builds a basic {@link ConfigurationTransformation}.
107     *
108     * @since 4.0.0
109     */
110    final class Builder {
111        private MoveStrategy strategy = MoveStrategy.OVERWRITE;
112        private final NavigableMap<NodePath, TransformAction> actions;
113
114        Builder() {
115            this.actions = new TreeMap<>(NodePathComparator.INSTANCE);
116        }
117
118        /**
119         * Adds an action to the transformation.
120         *
121         * @param path the path to apply the action at
122         * @param action the action
123         * @return this builder (for chaining)
124         * @since 4.0.0
125         */
126        public Builder addAction(final NodePath path, final TransformAction action) {
127            this.actions.put(requireNonNull(path, "path"), requireNonNull(action, "action"));
128            return this;
129        }
130
131        /**
132         * Gets the move strategy to be used by the resultant transformation.
133         *
134         * @return the move strategy
135         * @since 4.0.0
136         */
137        public MoveStrategy moveStrategy() {
138            return this.strategy;
139        }
140
141        /**
142         * Sets the mode strategy to be used by the resultant transformation.
143         *
144         * @param strategy the strategy
145         * @return this builder (for chaining)
146         * @since 4.0.0
147         */
148        public Builder moveStrategy(final MoveStrategy strategy) {
149            this.strategy = requireNonNull(strategy, "strategy");
150            return this;
151        }
152
153        /**
154         * Builds the transformation.
155         *
156         * @return the transformation
157         * @since 4.0.0
158         */
159        public ConfigurationTransformation build() {
160            if (this.actions.isEmpty()) {
161                return ConfigurationTransformation.empty();
162            }
163            return new SingleConfigurationTransformation(this.actions, this.strategy);
164        }
165    }
166
167    /**
168     * Builds a versioned {@link ConfigurationTransformation}.
169     *
170     * @since 4.0.0
171     */
172    final class VersionedBuilder {
173        private NodePath versionKey = NodePath.path("version");
174        private final NavigableMap<Integer, ConfigurationTransformation> versions = new TreeMap<>();
175
176        VersionedBuilder() {}
177
178        /**
179         * Sets the path of the version key within the configuration.
180         *
181         * @param versionKey the path to the version key
182         * @return this builder (for chaining)
183         * @since 4.0.0
184         */
185        public VersionedBuilder versionKey(final Object... versionKey) {
186            this.versionKey = NodePath.of(versionKey);
187            return this;
188        }
189
190        /**
191         * Adds a transformation to this builder for the given version.
192         *
193         * <p>The version must be between 0 and {@link Integer#MAX_VALUE}, and a version cannot be specified multiple times.
194         *
195         * @param version the version
196         * @param transformation the transformation
197         * @return this builder (for chaining)
198         * @since 4.0.0
199         */
200        public @NonNull VersionedBuilder addVersion(final int version, final @NonNull ConfigurationTransformation transformation) {
201            if (version < 0) {
202                throw new IllegalArgumentException("Version must be at least 0");
203            }
204            if (this.versions.putIfAbsent(version, requireNonNull(transformation, "transformation")) != null) {
205                throw new IllegalArgumentException("Version '" + version + "' has been specified multiple times.");
206            }
207            return this;
208        }
209
210        /**
211         * Adds a new series of transformations for a version.
212         *
213         * <p>The version must be between 0 and {@link Integer#MAX_VALUE}.
214         *
215         * @param version the version
216         * @param transformations the transformations. To perform a version
217         *                        upgrade, these transformations will be
218         *                        executed in order.
219         * @return this builder
220         * @since 4.0.0
221         */
222        public @NonNull VersionedBuilder addVersion(final int version, final @NonNull ConfigurationTransformation... transformations) {
223            return this.addVersion(version, chain(transformations));
224        }
225
226        /**
227         * Create and add a new transformation to this builder.
228         *
229         * <p>The transformation will be created from the builder passed to
230         * the callback function</p>
231         *
232         * <p>The version must be between 0 and {@link Integer#MAX_VALUE}
233         *
234         * @param version the version
235         * @param maker the transformation
236         * @return this builder
237         * @since 4.0.0
238         */
239        public @NonNull VersionedBuilder makeVersion(final int version, final @NonNull Consumer<? super Builder> maker) {
240            final Builder builder = builder();
241            maker.accept(builder);
242            return this.addVersion(version, builder.build());
243        }
244
245        /**
246         * Builds the transformation.
247         *
248         * @return the transformation
249         * @since 4.0.0
250         */
251        public ConfigurationTransformation.@NonNull Versioned build() {
252            if (this.versions.isEmpty()) {
253                throw new IllegalArgumentException("At least one version must be specified to build a transformation");
254            }
255            return new VersionedTransformation(this.versionKey, this.versions);
256        }
257    }
258
259    /**
260     * A transformation that is aware of node versions.
261     *
262     * @since 4.0.0
263     */
264    interface Versioned extends ConfigurationTransformation {
265
266        /**
267         * Indicates a node with an unknown version.
268         *
269         * <p>This can be returned as the latest version.</p>
270         *
271         * @since 4.0.0
272         */
273        int VERSION_UNKNOWN = -1;
274
275        /**
276         * Get the path the node's current version is located at.
277         *
278         * @return version path
279         * @since 4.0.0
280         */
281        NodePath versionKey();
282
283        /**
284         * Get the latest version that nodes can be updated to.
285         *
286         * @return the most recent version
287         * @since 4.0.0
288         */
289        int latestVersion();
290
291        /**
292         * Get the version of a node hierarchy.
293         *
294         * <p>Note that the node checked here must be the same node passed to
295         * {@link #apply(ConfigurationNode)}, not any node in a hierarchy.
296         *
297         * <p>If the node value is not present or not coercible to an integer,
298         * {@link #VERSION_UNKNOWN} will be returned. When the transformation is
299         * executed, every version transformation will be applied.
300         *
301         * @param node node to check
302         * @return version, or {@link #VERSION_UNKNOWN} if no value is present
303         * @since 4.0.0
304         */
305        default int version(final ConfigurationNode node) {
306            return node.node(this.versionKey()).getInt(VERSION_UNKNOWN);
307        }
308    }
309
310}