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.examples;
018
019import static org.spongepowered.configurate.NodePath.path;
020
021import org.checkerframework.checker.nullness.qual.Nullable;
022import org.spongepowered.configurate.ConfigurateException;
023import org.spongepowered.configurate.ConfigurationNode;
024import org.spongepowered.configurate.hocon.HoconConfigurationLoader;
025import org.spongepowered.configurate.transformation.ConfigurationTransformation;
026import org.spongepowered.configurate.transformation.TransformAction;
027
028import java.nio.file.Paths;
029
030/**
031 * An example of how to use transformations to migrate a configuration to a
032 * newer schema version.
033 *
034 * <p>It's like DFU but not hot garbage! (and probably less PhD-worthy)</p>
035 */
036public final class Transformations {
037
038    private static final int VERSION_LATEST = 2; // easy way to track the latest version, update as more revisions are added
039
040    private Transformations() {}
041
042    /**
043     * Create a new builder for versioned configurations. This builder uses a
044     * field in the node (by default {@code schema-version}) to determine the
045     * current schema version (using -1 for no version present).
046     *
047     * @return versioned transformation
048     */
049    public static ConfigurationTransformation.Versioned create() {
050        return ConfigurationTransformation.versionedBuilder()
051                .addVersion(VERSION_LATEST, oneToTwo()) // syntax: target version, latest version
052                .addVersion(1, zeroToOne())
053                .addVersion(0, initialTransform())
054                .build();
055    }
056
057    /**
058     * A transformation. This one has multiple actions, and demonstrates how
059     * wildcards work.
060     *
061     * @return created transformation
062     */
063    public static ConfigurationTransformation initialTransform() {
064        return ConfigurationTransformation.builder()
065                // Move the node at `serverVersion` to the location <code>{"server", "version"}</code>
066                .addAction(path("serverVersion"), (path, value) -> {
067                    return new Object[]{"server", "version"};
068                })
069                // For every direct child of the `section` node, set the value of its child `new-value` to something
070                .addAction(path("section", ConfigurationTransformation.WILDCARD_OBJECT), (path, value) -> {
071                    value.node("new-value").set("i'm a default");
072
073                    return null; // don't move the value
074                })
075                .build();
076    }
077
078    public static ConfigurationTransformation zeroToOne() {
079        return ConfigurationTransformation.builder()
080                // oh, turns out we want to use a different format for this, so we'll change it again
081                .addAction(path("server", "version"), (path, value) -> {
082                    final @Nullable String val = value.getString();
083                    if (val != null) {
084                        value.set(val.replaceAll("-", "_"));
085                    }
086                    return null;
087                })
088                .build();
089    }
090
091    public static ConfigurationTransformation oneToTwo() {
092        return ConfigurationTransformation.builder()
093                .addAction(path("server", "version"), TransformAction.rename("release"))
094                .build();
095    }
096
097    /**
098     * Apply the transformations to a node.
099     *
100     * <p>This method also prints information about the version update that
101     * occurred</p>
102     *
103     * @param node the node to transform
104     * @param <N> node type
105     * @return provided node, after transformation
106     */
107    public static <N extends ConfigurationNode> N updateNode(final N node) throws ConfigurateException {
108        if (!node.virtual()) { // we only want to migrate existing data
109            final ConfigurationTransformation.Versioned trans = create();
110            final int startVersion = trans.version(node);
111            trans.apply(node);
112            final int endVersion = trans.version(node);
113            if (startVersion != endVersion) { // we might not have made any changes
114                System.out.println("Updated config schema from " + startVersion + " to " + endVersion);
115            }
116        }
117        return node;
118    }
119
120    public static void main(final String[] args) throws ConfigurateException {
121        if (args.length != 1) {
122            System.err.println("Not enough arguments, usage: transformations <file>");
123            System.err.println("Apply the test transformations to a single file");
124        }
125        final HoconConfigurationLoader loader = HoconConfigurationLoader.builder()
126                .path(Paths.get(args[0]))
127                .build();
128
129        try {
130            loader.save(updateNode(loader.load())); // tada
131        } catch (final ConfigurateException ex) {
132            // We try to update as much as possible, so could theoretically save a partially updated file here
133            System.err.println("Failed to fully update node: " + ex.getMessage());
134            ex.printStackTrace();
135            System.exit(1);
136        }
137    }
138
139}