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.extra.dfu.v3;
018
019import static java.util.Objects.requireNonNull;
020
021import com.mojang.datafixers.DSL;
022import com.mojang.datafixers.DataFixUtils;
023import com.mojang.datafixers.DataFixer;
024import com.mojang.datafixers.util.Pair;
025import org.checkerframework.checker.nullness.qual.NonNull;
026import org.checkerframework.checker.nullness.qual.Nullable;
027import org.spongepowered.configurate.ConfigurateException;
028import org.spongepowered.configurate.ConfigurationNode;
029import org.spongepowered.configurate.NodePath;
030import org.spongepowered.configurate.transformation.ConfigurationTransformation;
031
032import java.util.HashSet;
033import java.util.Set;
034
035/**
036 * A transformation that exposes a single DataFixer to a configuration in a
037 * friendly way.
038 *
039 * <p>Because Configurate does not have a schema model and DFU does, this
040 * transformation works by explicitly providing a mapping between configurate
041 * node paths and DFU TypeReferences.</p>
042 *
043 * @since 4.0.0
044 */
045public final class DataFixerTransformation implements ConfigurationTransformation.Versioned {
046
047    private final NodePath versionPath;
048    private final int targetVersion;
049    private final ConfigurationTransformation wrapped;
050    private final ThreadLocal<Integer> versionHolder;
051
052    /**
053     * Create a builder that can work with any DFU DataFixer.
054     *
055     * @return the builder
056     * @since 4.0.0
057     */
058    public static Builder dfuBuilder() {
059        return new Builder();
060    }
061
062    DataFixerTransformation(final NodePath versionPath, final int targetVersion, final ConfigurationTransformation wrapped,
063            final ThreadLocal<Integer> versionHolder) {
064        this.versionPath = versionPath;
065        this.targetVersion = targetVersion;
066        this.wrapped = wrapped;
067        this.versionHolder = versionHolder;
068    }
069
070    @Override
071    public void apply(final @NonNull ConfigurationNode node) throws ConfigurateException {
072        final ConfigurationNode versionNode = node.node(this.versionPath);
073        final int currentVersion = versionNode.getInt(-1);
074        if (currentVersion < this.targetVersion) {
075            this.versionHolder.set(currentVersion);
076            this.wrapped.apply(node);
077            versionNode.set(this.targetVersion);
078        } else if (currentVersion > this.targetVersion) {
079            throw new ConfigurateException(node, "The target version (" + this.targetVersion
080                    + ") is older than the data's current version (" + currentVersion + ")");
081        }
082    }
083
084    /**
085     * Get the version from a specific configuration node, using the configured
086     * {@linkplain #versionKey() version key}.
087     *
088     * @param root base node to query
089     * @return version, or -1 if this node is unversioned.
090     */
091    @Override
092    public int version(final ConfigurationNode root) {
093        return requireNonNull(root, "root").node(this.versionKey()).getInt(-1);
094    }
095
096    @Override
097    public NodePath versionKey() {
098        return this.versionPath;
099    }
100
101    @Override
102    public int latestVersion() {
103        return this.targetVersion;
104    }
105
106    /**
107     * Builder for {@link DataFixerTransformation}.
108     *
109     * @since 4.0.0
110     */
111    public static class Builder {
112        private NodePath versionPath = NodePath.path("dfu-version");
113        private int targetVersion = -1;
114        private @Nullable DataFixer fixer;
115        private final Set<Pair<DSL.TypeReference, NodePath>> dataFixes = new HashSet<>();
116
117        /**
118         * Set the fixer to use to process.
119         *
120         * @param fixer the fixer
121         * @return this builder
122         * @since 4.0.0
123         */
124        public Builder dataFixer(final DataFixer fixer) {
125            this.fixer = requireNonNull(fixer);
126            return this;
127        }
128
129        /**
130         * Set the path of the node to query and store the node's schema
131         * version at.
132         *
133         * @param path the path
134         * @return this builder
135         * @since 4.0.0
136         */
137        public Builder versionPath(final Object... path) {
138            this.versionPath = NodePath.of(requireNonNull(path, "path"));
139            return this;
140        }
141
142        /**
143         * Set the path of the node to query and store the node's schema
144         * version at.
145         *
146         * @param path the path
147         * @return this builder
148         * @since 4.0.0
149         */
150        public Builder versionPath(final NodePath path) {
151            this.versionPath = requireNonNull(path, "path");
152            return this;
153        }
154
155        /**
156         * Set the desired target version. If none is specified, the newest
157         * available version will be determined from the DataFixer.
158         *
159         * @param targetVersion target version
160         * @return this builder
161         * @since 4.0.0
162         */
163        public Builder targetVersion(final int targetVersion) {
164            this.targetVersion = targetVersion;
165            return this;
166        }
167
168        /**
169         * Map values at {@code path} to being of {@code type}.
170         *
171         * @param type value type reference
172         * @param path target path
173         * @return this builder
174         * @since 4.0.0
175         */
176        public Builder addType(final DSL.TypeReference type, final Object... path) {
177            return this.addType(type, NodePath.of(path));
178        }
179
180        /**
181         * Map values at {@code path} to being of {@code type}.
182         *
183         * @param type value type reference
184         * @param path target path
185         * @return this builder
186         * @since 4.0.0
187         */
188        public Builder addType(final DSL.TypeReference type, final NodePath path) {
189            this.dataFixes.add(Pair.of(type, path));
190            return this;
191        }
192
193        /**
194         * Create a new transformation based on the provided info.
195         *
196         * @return new transformation
197         * @since 4.0.0
198         */
199        public DataFixerTransformation build() {
200            requireNonNull(this.fixer, "A fixer must be provided!");
201            if (this.targetVersion == -1) {
202                // DataFixer gets a schema by subsetting the sorted list of schemas with (0, version + 1), so we do max int - 1 to avoid overflow
203                this.targetVersion = DataFixUtils.getVersion(this.fixer.getSchema(Integer.MAX_VALUE - 1).getVersionKey());
204            }
205            final ConfigurationTransformation.Builder wrappedBuilder = ConfigurationTransformation.builder();
206            final ThreadLocal<Integer> versionHolder = new ThreadLocal<>();
207            for (final Pair<DSL.TypeReference, NodePath> fix : this.dataFixes) {
208                wrappedBuilder.addAction(fix.getSecond(), (path, valueAtPath) -> {
209                    valueAtPath.from(this.fixer.update(fix.getFirst(), ConfigurateOps.wrap(valueAtPath),
210                            versionHolder.get(), this.targetVersion).getValue());
211                    return null;
212                });
213            }
214            return new DataFixerTransformation(this.versionPath, this.targetVersion, wrappedBuilder.build(), versionHolder);
215        }
216
217    }
218
219}