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.v4; 018 019import static java.util.Objects.requireNonNull; 020 021import com.google.common.collect.ImmutableList; 022import com.google.common.collect.ImmutableMap; 023import com.mojang.datafixers.util.Pair; 024import com.mojang.serialization.DataResult; 025import com.mojang.serialization.Dynamic; 026import com.mojang.serialization.DynamicOps; 027import com.mojang.serialization.MapLike; 028import org.checkerframework.checker.nullness.qual.Nullable; 029import org.spongepowered.configurate.BasicConfigurationNode; 030import org.spongepowered.configurate.CommentedConfigurationNode; 031import org.spongepowered.configurate.ConfigurationNode; 032import org.spongepowered.configurate.ConfigurationNodeFactory; 033import org.spongepowered.configurate.serialize.TypeSerializerCollection; 034 035import java.nio.ByteBuffer; 036import java.util.List; 037import java.util.Map; 038import java.util.function.Consumer; 039import java.util.function.Function; 040import java.util.stream.IntStream; 041import java.util.stream.LongStream; 042import java.util.stream.Stream; 043 044/** 045 * Implementation of DataFixerUpper's DynamicOps. 046 * 047 * <p>The {@link DynamicOps} interface should be thought of essentially as a way 048 * to perform operations on a type without having to directly implement that 049 * interface on the type. Rather than taking an object that implements an 050 * interface, DFU methods take the implementation of the DynamicOps interface 051 * plus the type implemented onto. 052 * 053 * <p>When possible, the first node's {@link ConfigurationNode#copy()} method 054 * will be used to create a new node to contain results. Otherwise, the provided 055 * factory will be used. The default factory creates a 056 * {@link CommentedConfigurationNode} with the default serializer collection 057 * but a custom factory may be provided. 058 * 059 * <p>DynamicOps has the following primitive types (as determined by those 060 * codecs that implement {@link com.mojang.serialization.codecs.PrimitiveCodec}): 061 * <dl> 062 * <dt>boolean</dt> 063 * <dd>literal boolean, or numeric 1 for true, 0 for false</dd> 064 * <dt>byte</dt> 065 * <dd>numeric value, {@link Number#byteValue() coerced} to a byte. 066 * If {@link #compressMaps()}, a string may be parsed as a byte as well.</dd> 067 * <dt>short</dt> 068 * <dd>numeric value, {@link Number#shortValue() coerced} to a short. 069 * If {@link #compressMaps()}, a string may be parsed as a short as well.</dd> 070 * <dt>int</dt> 071 * <dd>numeric value, {@link Number#intValue() coerced} to an integer. 072 * If {@link #compressMaps()}, a string may be parsed as an integer as well.</dd> 073 * <dt>long</dt> 074 * <dd>numeric value, {@link Number#longValue() coerced} to a long. 075 * If {@link #compressMaps()}, a string may be parsed as a long as well.</dd> 076 * <dt>float</dt> 077 * <dd>numeric value, {@link Number#floatValue() coerced} to a float. 078 * If {@link #compressMaps()}, a string may be parsed as a float as well.</dd> 079 * <dt>double</dt> 080 * <dd>numeric value, {@link Number#doubleValue() coerced} to a double. 081 * If {@link #compressMaps()}, a string may be parsed as a double as well.</dd> 082 * <dt>{@link String}</dt> 083 * <dd>Any scalar value, as {@link Object#toString() a string}</dd> 084 * <dt>{@link java.nio.ByteBuffer}</dt> 085 * <dd>An array of bytes. Either a native byte array in the node, 086 * or (by default impl) a list of bytes</dd> 087 * <dt>{@link java.util.stream.IntStream}</dt> 088 * <dd>A sequence of integers. Either a native int array in the node, 089 * or (by default impl) a list of integers</dd> 090 * <dt>{@link java.util.stream.LongStream}</dt> 091 * <dd>A sequence of longs. Either a native long array in the node, 092 * or (by default impl) a list of longs</dd> 093 * </dl> 094 * 095 * @since 4.0.0 096 */ 097public final class ConfigurateOps implements DynamicOps<ConfigurationNode> { 098 099 private static final ConfigurateOps UNCOMPRESSED = ConfigurateOps.builder().build(); 100 private static final ConfigurateOps COMPRESSED = ConfigurateOps.builder().compressed(true).build(); 101 102 private final ConfigurationNodeFactory<? extends ConfigurationNode> factory; 103 private final boolean compressed; 104 private final Protection readProtection; 105 private final Protection writeProtection; 106 107 /** 108 * Get the shared instance of this class, which creates new nodes using 109 * the default factory. The returned instance will not be compressed 110 * 111 * @return the shared instance 112 * @since 4.0.0 113 */ 114 public static DynamicOps<ConfigurationNode> instance() { 115 return instance(false); 116 } 117 118 /** 119 * Get the shared instance of this class, which creates new nodes using 120 * the default factory. 121 * 122 * <p>See {@link #compressMaps()} for a description of what the 123 * <pre>compressed</pre> parameter does. 124 * 125 * @param compressed whether keys should be compressed in the output of 126 * this serializer 127 * @return the shared instance 128 * @since 4.0.0 129 */ 130 public static DynamicOps<ConfigurationNode> instance(final boolean compressed) { 131 return compressed ? COMPRESSED : UNCOMPRESSED; 132 } 133 134 /** 135 * Get an ops instance that will create nodes using the provided collection. 136 * 137 * @param collection collection to provide through created nodes' options 138 * @return ops instance 139 * @since 4.0.0 140 */ 141 public static DynamicOps<ConfigurationNode> forSerializers(final TypeSerializerCollection collection) { 142 if (requireNonNull(collection, "collection").equals(TypeSerializerCollection.defaults())) { 143 return UNCOMPRESSED; 144 } else { 145 return builder().factoryFromSerializers(collection).build(); 146 } 147 } 148 149 /** 150 * Wrap a ConfigurationNode in a {@link Dynamic} instance. The returned 151 * Dynamic will use the same type serializer collection as the original node 152 * for its operations. 153 * 154 * @param node the node to wrap 155 * @return a wrapped node 156 * @since 4.0.0 157 */ 158 public static Dynamic<ConfigurationNode> wrap(final ConfigurationNode node) { 159 if (node.options().serializers().equals(TypeSerializerCollection.defaults())) { 160 return new Dynamic<>(instance(), node); 161 } else { 162 return builder().factoryFromNode(node).buildWrapping(node); 163 } 164 } 165 166 /** 167 * Configure an ops instance using the options of an existing node. 168 * 169 * @param value the value type 170 * @return values 171 * @since 4.0.0 172 */ 173 public static DynamicOps<ConfigurationNode> fromNode(final ConfigurationNode value) { 174 return builder().factoryFromNode(value).build(); 175 } 176 177 /** 178 * Create a new builder for an ops instance. 179 * 180 * @return builder 181 * @since 4.0.0 182 */ 183 public static ConfigurateOpsBuilder builder() { 184 return new ConfigurateOpsBuilder(); 185 } 186 187 ConfigurateOps(final ConfigurationNodeFactory<? extends ConfigurationNode> factory, final boolean compressed, 188 final Protection readProtection, final Protection writeProtection) { 189 this.factory = factory; 190 this.compressed = compressed; 191 this.readProtection = readProtection; 192 this.writeProtection = writeProtection; 193 } 194 195 /** 196 * Whether data passed through this ops will be compressed or not. 197 * 198 * <p>In the context of DFU, <pre>compressed</pre> means that in situations 199 * where values are of a {@link com.mojang.serialization.Keyable} type 200 * (as is with types like Minecraft Registries) 201 * rather than fully encoding each value, its index into the container 202 * is encoded. 203 * 204 * <p>While data encoded this way may take less space to store, the 205 * compressed data will also require an explicit mapping of indices to 206 * values. If this is not stored with the node, the indices of values must 207 * be preserved to correctly deserialize compressed values. 208 * 209 * <p>For example, for an enum new values could only be appended, not added 210 * in the middle of the constants. 211 * 212 * @return whether maps are compressed 213 */ 214 @Override 215 public boolean compressMaps() { 216 return this.compressed; 217 } 218 219 /** 220 * Extract a value from a node used as a {@code key} in DFU methods. 221 * 222 * <p>This currently only attempts to interpret the key as a single level 223 * down. However, we may want to try to extract an array or iterable of path 224 * elements to be able to traverse multiple levels. 225 * 226 * @param node data source 227 * @return a key, asserted non-null 228 */ 229 static Object keyFrom(final ConfigurationNode node) { 230 if (node.isList() || node.isMap()) { 231 throw new IllegalArgumentException("Key nodes must have scalar values"); 232 } 233 return requireNonNull(node.raw(), "The provided key node must have a value"); 234 } 235 236 /** 237 * Guard source node according to ops instance's copying policy. 238 * 239 * @implNote currently, this will make a deep copy of the node. 240 * 241 * @param untrusted original node 242 * @return a node with equivalent data 243 */ 244 ConfigurationNode guardOutputRead(final ConfigurationNode untrusted) { 245 switch (this.readProtection) { 246 case COPY_DEEP: return untrusted.copy(); 247 case NONE: return untrusted; 248 default: throw new IllegalArgumentException("Unexpected state"); 249 } 250 } 251 252 ConfigurationNode guardInputWrite(final ConfigurationNode untrusted) { 253 switch (this.writeProtection) { 254 case COPY_DEEP: return untrusted.copy(); 255 case NONE: return untrusted; 256 default: throw new IllegalArgumentException("Unexpected state"); 257 } 258 } 259 260 /** 261 * Create a new empty node using this ops instance's factory. 262 * 263 * @return the new node 264 */ 265 @Override 266 public ConfigurationNode empty() { 267 return this.factory.createNode(); 268 } 269 270 @Override 271 public ConfigurationNode emptyMap() { 272 return empty().raw(ImmutableMap.of()); 273 } 274 275 @Override 276 public ConfigurationNode emptyList() { 277 return empty().raw(ImmutableList.of()); 278 } 279 280 // If the destination ops is another Configurate ops instance, just directly pass the node through 281 @SuppressWarnings("unchecked") 282 private <U> @Nullable U convertSelf(final DynamicOps<U> outOps, final ConfigurationNode input) { 283 if (outOps instanceof ConfigurateOps) { 284 return (U) input; 285 } else { 286 return null; 287 } 288 } 289 290 /** 291 * Create a copy of the source node converted to a different data structure. 292 * 293 * <p>Value types will be preserved as much as possible, but a reverse 294 * conversion will most likely be lossy 295 * 296 * @param targetOps output type 297 * @param source source value 298 * @param <U> output type 299 * @return output value 300 */ 301 @Override 302 public <U> U convertTo(final DynamicOps<U> targetOps, final ConfigurationNode source) { 303 final @Nullable U self = convertSelf(requireNonNull(targetOps, "targetOps"), requireNonNull(source, "source")); 304 if (self != null) { 305 return self; 306 } 307 308 if (source.isMap()) { 309 return convertMap(targetOps, source); 310 } else if (source.isList()) { 311 return convertList(targetOps, source); 312 } else { 313 final @Nullable Object value = source.rawScalar(); 314 if (value == null) { 315 return targetOps.empty(); 316 } else if (value instanceof String) { 317 return targetOps.createString((String) value); 318 } else if (value instanceof Boolean) { 319 return targetOps.createBoolean((Boolean) value); 320 } else if (value instanceof Short) { 321 return targetOps.createShort((Short) value); 322 } else if (value instanceof Integer) { 323 return targetOps.createInt((Integer) value); 324 } else if (value instanceof Long) { 325 return targetOps.createLong((Long) value); 326 } else if (value instanceof Float) { 327 return targetOps.createFloat((Float) value); 328 } else if (value instanceof Double) { 329 return targetOps.createDouble((Double) value); 330 } else if (value instanceof Byte) { 331 return targetOps.createByte((Byte) value); 332 } else if (value instanceof byte[]) { 333 return targetOps.createByteList(ByteBuffer.wrap((byte[]) value)); 334 } else if (value instanceof int[]) { 335 return targetOps.createIntList(IntStream.of((int[]) value)); 336 } else if (value instanceof long[]) { 337 return targetOps.createLongList(LongStream.of((long[]) value)); 338 } else { 339 throw new IllegalArgumentException("Scalar value '" + source + "' has an unknown type: " + value.getClass().getName()); 340 } 341 } 342 } 343 344 /** 345 * Get the value of the provided node if it is a number or boolean. 346 * 347 * <p>If {@link #compressMaps()} is true, values may be coerced from 348 * another type. 349 * 350 * @param input data source 351 * @return extracted number 352 */ 353 @Override 354 public DataResult<Number> getNumberValue(final ConfigurationNode input) { 355 if (!(input.isMap() || input.isList())) { 356 final @Nullable Object value = input.rawScalar(); 357 if (value instanceof Number) { 358 return DataResult.success((Number) value); 359 } else if (value instanceof Boolean) { 360 return DataResult.success((boolean) value ? 1 : 0); 361 } 362 363 if (compressMaps()) { 364 final int result = input.getInt(Integer.MIN_VALUE); 365 if (result == Integer.MIN_VALUE) { 366 return DataResult.error("Value is not a number"); 367 } 368 return DataResult.success(result); 369 } 370 } 371 372 return DataResult.error("Not a number: " + input); 373 } 374 375 /** 376 * Get the value of the provided node if it is a scalar, converted to 377 * a {@link String}. 378 * 379 * @param input data source 380 * @return string | error 381 */ 382 @Override 383 public DataResult<String> getStringValue(final ConfigurationNode input) { 384 final @Nullable String value = input.getString(); 385 if (value != null) { 386 return DataResult.success(value); 387 } 388 389 return DataResult.error("Not a string: " + input); 390 } 391 392 /** 393 * Create a new node using this ops instance's node factory, 394 * and set its value to the provided number. 395 * 396 * @param value value 397 * @return new node with value 398 */ 399 @Override 400 public ConfigurationNode createNumeric(final Number value) { 401 return empty().raw(requireNonNull(value, "value")); 402 } 403 404 /** 405 * Create a new node using this ops instance's node factory, 406 * and set its value to the provided boolean. 407 * 408 * @param value value 409 * @return new node with value 410 */ 411 @Override 412 public ConfigurationNode createBoolean(final boolean value) { 413 return empty().raw(value); 414 } 415 416 /** 417 * Create a new node using this ops instance's node factory, 418 * and set its value to the provided string. 419 * 420 * @param value value 421 * @return new node with value 422 */ 423 @Override 424 public ConfigurationNode createString(final String value) { 425 return empty().raw(requireNonNull(value, "value")); 426 } 427 428 /** 429 * Return a result where if {@code prefix} is empty, the node is 430 * {@code value}, but otherwise returns an error. 431 * 432 * @param prefix starting value 433 * @param value to update base with 434 * @return result of updated node or error 435 */ 436 @Override 437 public DataResult<ConfigurationNode> mergeToPrimitive(final ConfigurationNode prefix, final ConfigurationNode value) { 438 if (!prefix.empty()) { 439 return DataResult.error("Cannot merge " + value + " into non-empty node " + prefix); 440 } 441 return DataResult.success(guardOutputRead(value)); 442 } 443 444 /** 445 * Appends element {@code value} to list node {@code input}. 446 * 447 * @param input base node. Must be empty or of list type 448 * @param value value to add as element to the list 449 * @return success with modified node, or error if {@code input} contains a 450 * non-{@link ConfigurationNode#isList() list} value 451 */ 452 @Override 453 public DataResult<ConfigurationNode> mergeToList(final ConfigurationNode input, final ConfigurationNode value) { 454 if (input.isList() || input.empty()) { 455 final ConfigurationNode ret = guardOutputRead(input); 456 ret.appendListNode().from(value); 457 return DataResult.success(ret); 458 } 459 460 return DataResult.error("mergeToList called on a node which is not a list: " + input, input); 461 } 462 463 /** 464 * Appends nodes in {@code values} to copy of list node {@code input}. 465 * 466 * @param input base node. Must be empty or of list type 467 * @param values list of values to append to base node 468 * @return success with modified node, or error if {@code input} contains a 469 * non-{@link ConfigurationNode#isList() list} value 470 */ 471 @Override 472 public DataResult<ConfigurationNode> mergeToList(final ConfigurationNode input, final List<ConfigurationNode> values) { 473 if (input.isList() || input.empty()) { 474 final ConfigurationNode ret = guardInputWrite(input); 475 for (ConfigurationNode node : values) { 476 ret.appendListNode().from(node); 477 } 478 return DataResult.success(ret); 479 } 480 481 return DataResult.error("mergeToList called on a node which is not a list: " + input, input); 482 } 483 484 /** 485 * Update the child of {@code input} at {@code key} with {@code value}. 486 * 487 * <p>This operation will only affect the returned copy of the input node 488 * 489 * @param input base node. Must be empty or of map type 490 * @param key key relative to base node 491 * @param value value to set at empty node 492 * @return success with modified node, or error if {@code input} contains a 493 * non-{@link ConfigurationNode#isList() list} value 494 */ 495 @Override 496 public DataResult<ConfigurationNode> mergeToMap(final ConfigurationNode input, final ConfigurationNode key, final ConfigurationNode value) { 497 if (input.isMap() || input.empty()) { 498 final ConfigurationNode copied = guardInputWrite(input); 499 copied.node(keyFrom(key)).from(value); 500 return DataResult.success(copied); 501 } 502 503 return DataResult.error("mergeToMap called on a node which is not a map: " + input, input); 504 } 505 506 /** 507 * Return a stream of pairs of (key, value) for map data in the input node. 508 * 509 * <p>If the input node is non-empty and not a map, the result will 510 * be a failure. 511 * 512 * @param input input node 513 * @return result, if successful, of a stream of pairs (key, value) of 514 * entries in the input node. 515 */ 516 @Override 517 public DataResult<Stream<Pair<ConfigurationNode, ConfigurationNode>>> getMapValues(final ConfigurationNode input) { 518 if (input.empty() || input.isMap()) { 519 return DataResult.success(input.childrenMap().entrySet().stream() 520 .map(entry -> Pair.of(BasicConfigurationNode.root(input.options()).raw(entry.getKey()), 521 guardOutputRead(entry.getValue())))); 522 } 523 524 return DataResult.error("Not a map: " + input); 525 } 526 527 /** 528 * Get a map-like view of a copy of the contents of {@code input}. 529 * 530 * <p>If the input node is non-empty and not a map, the result will 531 * be a failure. 532 * 533 * @param input input node 534 * @return result, if successful, of map-like view of a copy of the input 535 */ 536 @Override 537 public DataResult<MapLike<ConfigurationNode>> getMap(final ConfigurationNode input) { 538 if (input.empty() || input.isMap()) { 539 return DataResult.success(new NodeMaplike(this, input.options(), input.childrenMap())); 540 } else { 541 return DataResult.error("Input node is not a map"); 542 } 543 } 544 545 /** 546 * Get a consumer that takes an action to perform on every element of 547 * list node {@code input}. 548 * 549 * <p>As an example, to print out every node in a list 550 * (minus error checking): 551 * <pre> 552 * getList(listNode).result().get() 553 * .accept(element -> System.out.println(element); 554 * </pre> 555 * 556 * @param input data source 557 * @return result, that if successful will take an action to perform on 558 * every element 559 */ 560 @Override 561 public DataResult<Consumer<Consumer<ConfigurationNode>>> getList(final ConfigurationNode input) { 562 if (input.isList()) { 563 return DataResult.success(action -> { 564 for (ConfigurationNode child : input.childrenList()) { 565 action.accept(guardOutputRead(child)); 566 } 567 }); 568 } else { 569 return DataResult.error("Input node is not a list"); 570 } 571 } 572 573 /** 574 * Get the contents of list node {@code input} as a {@link Stream} of nodes. 575 * 576 * @param input data source 577 * @return if node is empty or a list, stream of nodes 578 */ 579 @Override 580 public DataResult<Stream<ConfigurationNode>> getStream(final ConfigurationNode input) { 581 if (input.empty() || input.isList()) { 582 final Stream<ConfigurationNode> stream = input.childrenList().stream().map(this::guardOutputRead); 583 return DataResult.success(stream); 584 } 585 586 return DataResult.error("Not a list: " + input); 587 } 588 589 /** 590 * Create a new node containing the map entries from the 591 * stream {@code values}. 592 * 593 * <p>Keys will be interpreted as a single Object, and can only 594 * currently access direct children. 595 * 596 * @param values entries in the map 597 * @return newly created node 598 */ 599 @Override 600 public ConfigurationNode createMap(final Stream<Pair<ConfigurationNode, ConfigurationNode>> values) { 601 final ConfigurationNode ret = empty(); 602 603 values.forEach(p -> ret.node(keyFrom(p.getFirst())).from(p.getSecond())); 604 605 return ret; 606 } 607 608 /** 609 * Create a new node containing the map entries from the 610 * map {@code values}. 611 * 612 * <p>Keys will be interpreted as a single Object, and can only 613 * currently access direct children. 614 * 615 * @param values unwrapped node map 616 * @return newly created node 617 */ 618 @Override 619 public ConfigurationNode createMap(final Map<ConfigurationNode, ConfigurationNode> values) { 620 final ConfigurationNode ret = empty(); 621 622 for (Map.Entry<ConfigurationNode, ConfigurationNode> entry : values.entrySet()) { 623 ret.node(keyFrom(entry.getKey())).from(entry.getValue()); 624 } 625 626 return ret; 627 } 628 629 /** 630 * Create a new node containing values emitted by {@code input} as 631 * list elements. 632 * 633 * @param input data source 634 * @return newly created node 635 */ 636 @Override 637 public ConfigurationNode createList(final Stream<ConfigurationNode> input) { 638 final ConfigurationNode ret = empty(); 639 input.forEach(it -> ret.appendListNode().from(it)); 640 return ret; 641 } 642 643 /** 644 * Get a copy of {@code input} without the value at node {@code key}. 645 * 646 * <p>If the input node is not a map, the input node will be returned. 647 * 648 * @param input data source 649 * @param key key to the node to be removed 650 * @return if node removed, a copy of the input without node, 651 * otherwise input 652 */ 653 @Override 654 public ConfigurationNode remove(final ConfigurationNode input, final String key) { 655 if (input.isMap()) { 656 final ConfigurationNode ret = guardInputWrite(input); 657 ret.node(key).raw(null); 658 return ret; 659 } 660 661 return input; 662 } 663 664 /** 665 * Attempt to get the child of {@code input} at {@code key}. 666 * 667 * @param input data source 668 * @param key child key 669 * @return success containing child if child is non-virtual, 670 * otherwise failure 671 */ 672 @Override 673 public DataResult<ConfigurationNode> get(final ConfigurationNode input, final String key) { 674 final ConfigurationNode ret = input.node(key); 675 return ret.virtual() ? DataResult.error("No element " + key + " in the map " + input) : DataResult.success(guardOutputRead(ret)); 676 } 677 678 /** 679 * Get a child of the provided node at {@code key}. 680 * 681 * <p>Keys will be interpreted as a single Object, and can only 682 * currently access direct children. 683 * 684 * @param input parent node 685 * @param key wrapped key of child 686 * @return success containing child if child is non-virtual, 687 * otherwise failure 688 */ 689 @Override 690 public DataResult<ConfigurationNode> getGeneric(final ConfigurationNode input, final ConfigurationNode key) { 691 final ConfigurationNode ret = input.node(keyFrom(key)); 692 return ret.virtual() ? DataResult.error("No element " + key + " in the map " + input) : DataResult.success(guardOutputRead(ret)); 693 } 694 695 /** 696 * Update a copy of {@code input} with {@code value} at path {@code key}. 697 * 698 * @param input data source 699 * @param key key of child node 700 * @param value value for child node 701 * @return updated parent node 702 */ 703 @Override 704 public ConfigurationNode set(final ConfigurationNode input, final String key, final ConfigurationNode value) { 705 final ConfigurationNode ret = guardInputWrite(input); 706 ret.node(key).from(value); 707 return ret; 708 } 709 710 /** 711 * Copies the input node and transform its child at {@code key}. 712 * 713 * <p>Return a copy of the input node with the child at {@code key} 714 * transformed by the provided function 715 * 716 * <p>If there is no value at {@code key}, the input node will be 717 * returned unmodified. 718 * 719 * @param input base value 720 * @param key key to change 721 * @param function function to process the node at {@code wrappedKey} 722 * @return an updated copy of input node 723 */ 724 @Override 725 public ConfigurationNode update(final ConfigurationNode input, final String key, final Function<ConfigurationNode, ConfigurationNode> function) { 726 if (input.node(key).virtual()) { 727 return input; 728 } 729 730 final ConfigurationNode ret = guardInputWrite(input); 731 final ConfigurationNode child = ret.node(key); 732 child.from(function.apply(child)); 733 return ret; 734 } 735 736 /** 737 * Copies the input node and transform the node at {@code wrappedKey}. 738 * 739 * <p>Return a copy of the input node with the child at {@code wrappedKey} 740 * transformed by the provided function 741 * 742 * <p>If there is no value at {@code wrappedKey}, the input node will be 743 * returned unmodified. 744 * 745 * <p>Keys will be interpreted as a single Object, and can only 746 * currently access direct children. 747 * 748 * @param input base value 749 * @param wrappedKey key to change 750 * @param function function to process the node at {@code wrappedKey} 751 * @return an updated copy of input node 752 */ 753 @Override 754 public ConfigurationNode updateGeneric(final ConfigurationNode input, final ConfigurationNode wrappedKey, 755 final Function<ConfigurationNode, ConfigurationNode> function) { 756 final Object key = keyFrom(wrappedKey); 757 if (input.node(key).virtual()) { 758 return input; 759 } 760 761 final ConfigurationNode ret = guardInputWrite(input); 762 763 final ConfigurationNode child = ret.node(key); 764 child.from(function.apply(child)); 765 return ret; 766 } 767 768 @Override 769 public String toString() { 770 return "Configurate"; 771 } 772 773 /** 774 * Protection level for configuration node accesses through ops instance. 775 * 776 * @since 4.0.0 777 */ 778 public enum Protection { 779 /** 780 * When an operation is executed on the node, make a deep copy of the 781 * result. 782 */ 783 COPY_DEEP, 784 785 /** 786 * Directly pass on nodes, still attached to their original structure. 787 */ 788 NONE 789 790 } 791 792}