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(@NonNull final 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(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 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 (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}