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                throw new IllegalArgumentException("At least one action must be specified to build a transformation");
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        @NonNull
201        public VersionedBuilder addVersion(final int version, final @NonNull ConfigurationTransformation transformation) {
202            if (version < 0) {
203                throw new IllegalArgumentException("Version must be at least 0");
204            }
205            if (this.versions.putIfAbsent(version, requireNonNull(transformation, "transformation")) != null) {
206                throw new IllegalArgumentException("Version '" + version + "' has been specified multiple times.");
207            }
208            return this;
209        }
210
211        /**
212         * Adds a new series of transformations for a version.
213         *
214         * <p>The version must be between 0 and {@link Integer#MAX_VALUE}.
215         *
216         * @param version the version
217         * @param transformations the transformations. To perform a version
218         *                        upgrade, these transformations will be
219         *                        executed in order.
220         * @return this builder
221         * @since 4.0.0
222         */
223        public @NonNull VersionedBuilder addVersion(final int version, final @NonNull ConfigurationTransformation... transformations) {
224            return this.addVersion(version, chain(transformations));
225        }
226
227        /**
228         * Create and add a new transformation to this builder.
229         *
230         * <p>The transformation will be created from the builder passed to
231         * the callback function</p>
232         *
233         * <p>The version must be between 0 and {@link Integer#MAX_VALUE}
234         *
235         * @param version the version
236         * @param maker the transformation
237         * @return this builder
238         * @since 4.0.0
239         */
240        public @NonNull VersionedBuilder makeVersion(final int version, final @NonNull Consumer<? super Builder> maker) {
241            final Builder builder = builder();
242            maker.accept(builder);
243            return this.addVersion(version, builder.build());
244        }
245
246        /**
247         * Builds the transformation.
248         *
249         * @return the transformation
250         * @since 4.0.0
251         */
252        public ConfigurationTransformation.@NonNull Versioned build() {
253            if (this.versions.isEmpty()) {
254                throw new IllegalArgumentException("At least one version must be specified to build a transformation");
255            }
256            return new VersionedTransformation(this.versionKey, this.versions);
257        }
258    }
259
260    /**
261     * A transformation that is aware of node versions.
262     *
263     * @since 4.0.0
264     */
265    interface Versioned extends ConfigurationTransformation {
266        int VERSION_UNKNOWN = -1;
267
268        /**
269         * Get the path the node's current version is located at.
270         *
271         * @return version path
272         * @since 4.0.0
273         */
274        NodePath versionKey();
275
276        /**
277         * Get the latest version that nodes can be updated to.
278         *
279         * @return the most recent version
280         * @since 4.0.0
281         */
282        int latestVersion();
283
284        /**
285         * Get the version of a node hierarchy.
286         *
287         * <p>Note that the node checked here must be the same node passed to
288         * {@link #apply(ConfigurationNode)}, not any node in a hierarchy.
289         *
290         * <p>If the node value is not present or not coercible to an integer,
291         * {@link #VERSION_UNKNOWN} will be returned. When the transformation is
292         * executed, every version transformation will be applied.
293         *
294         * @param node node to check
295         * @return version, or {@link #VERSION_UNKNOWN} if no value is present
296         * @since 4.0.0
297         */
298        default int version(final ConfigurationNode node) {
299            return node.node(versionKey()).getInt(VERSION_UNKNOWN);
300        }
301    }
302
303}