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}