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}