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.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 * @since 4.0.0 214 */ 215 @Override 216 public boolean compressMaps() { 217 return this.compressed; 218 } 219 220 /** 221 * Extract a value from a node used as a {@code key} in DFU methods. 222 * 223 * <p>This currently only attempts to interpret the key as a single level 224 * down. However, we may want to try to extract an array or iterable of path 225 * elements to be able to traverse multiple levels. 226 * 227 * @param node data source 228 * @return a key, asserted non-null 229 */ 230 static Object keyFrom(final ConfigurationNode node) { 231 if (node.isList() || node.isMap()) { 232 throw new IllegalArgumentException("Key nodes must have scalar values"); 233 } 234 return requireNonNull(node.raw(), "The provided key node must have a value"); 235 } 236 237 /** 238 * Guard source node according to ops instance's copying policy. 239 * 240 * @implNote currently, this will make a deep copy of the node. 241 * 242 * @param untrusted original node 243 * @return a node with equivalent data 244 */ 245 ConfigurationNode guardOutputRead(final ConfigurationNode untrusted) { 246 switch (this.readProtection) { 247 case COPY_DEEP: return untrusted.copy(); 248 case NONE: return untrusted; 249 default: throw new IllegalArgumentException("Unexpected state"); 250 } 251 } 252 253 ConfigurationNode guardInputWrite(final ConfigurationNode untrusted) { 254 switch (this.writeProtection) { 255 case COPY_DEEP: return untrusted.copy(); 256 case NONE: return untrusted; 257 default: throw new IllegalArgumentException("Unexpected state"); 258 } 259 } 260 261 /** 262 * Create a new empty node using this ops instance's factory. 263 * 264 * @return the new node 265 */ 266 @Override 267 public ConfigurationNode empty() { 268 return this.factory.createNode(); 269 } 270 271 @Override 272 public ConfigurationNode emptyMap() { 273 return empty().raw(ImmutableMap.of()); 274 } 275 276 @Override 277 public ConfigurationNode emptyList() { 278 return empty().raw(ImmutableList.of()); 279 } 280 281 // If the destination ops is another Configurate ops instance, just directly pass the node through 282 @SuppressWarnings("unchecked") 283 private <U> @Nullable U convertSelf(final DynamicOps<U> outOps, final ConfigurationNode input) { 284 if (outOps instanceof ConfigurateOps) { 285 return (U) input; 286 } else { 287 return null; 288 } 289 } 290 291 /** 292 * Create a copy of the source node converted to a different data structure. 293 * 294 * <p>Value types will be preserved as much as possible, but a reverse 295 * conversion will most likely be lossy 296 * 297 * @param targetOps output type 298 * @param source source value 299 * @param <U> output type 300 * @return output value 301 */ 302 @Override 303 public <U> U convertTo(final DynamicOps<U> targetOps, final ConfigurationNode source) { 304 final @Nullable U self = convertSelf(requireNonNull(targetOps, "targetOps"), requireNonNull(source, "source")); 305 if (self != null) { 306 return self; 307 } 308 309 if (source.isMap()) { 310 return convertMap(targetOps, source); 311 } else if (source.isList()) { 312 return convertList(targetOps, source); 313 } else { 314 final @Nullable Object value = source.rawScalar(); 315 if (value == null) { 316 return targetOps.empty(); 317 } else if (value instanceof String) { 318 return targetOps.createString((String) value); 319 } else if (value instanceof Boolean) { 320 return targetOps.createBoolean((Boolean) value); 321 } else if (value instanceof Short) { 322 return targetOps.createShort((Short) value); 323 } else if (value instanceof Integer) { 324 return targetOps.createInt((Integer) value); 325 } else if (value instanceof Long) { 326 return targetOps.createLong((Long) value); 327 } else if (value instanceof Float) { 328 return targetOps.createFloat((Float) value); 329 } else if (value instanceof Double) { 330 return targetOps.createDouble((Double) value); 331 } else if (value instanceof Byte) { 332 return targetOps.createByte((Byte) value); 333 } else if (value instanceof byte[]) { 334 return targetOps.createByteList(ByteBuffer.wrap((byte[]) value)); 335 } else if (value instanceof int[]) { 336 return targetOps.createIntList(IntStream.of((int[]) value)); 337 } else if (value instanceof long[]) { 338 return targetOps.createLongList(LongStream.of((long[]) value)); 339 } else { 340 throw new IllegalArgumentException("Scalar value '" + source + "' has an unknown type: " + value.getClass().getName()); 341 } 342 } 343 } 344 345 /** 346 * Get the value of the provided node if it is a number or boolean. 347 * 348 * <p>If {@link #compressMaps()} is true, values may be coerced from 349 * another type. 350 * 351 * @param input data source 352 * @return extracted number 353 */ 354 @Override 355 public DataResult<Number> getNumberValue(final ConfigurationNode input) { 356 if (!(input.isMap() || input.isList())) { 357 final @Nullable Object value = input.raw(); 358 if (value instanceof Number) { 359 return DataResult.success((Number) value); 360 } else if (value instanceof Boolean) { 361 return DataResult.success((boolean) value ? 1 : 0); 362 } 363 364 if (compressMaps()) { 365 final int result = input.getInt(Integer.MIN_VALUE); 366 if (result == Integer.MIN_VALUE) { 367 return DataResult.error("Value is not a number"); 368 } 369 return DataResult.success(result); 370 } 371 } 372 373 return DataResult.error("Not a number: " + input); 374 } 375 376 /** 377 * Get the value of the provided node if it is a scalar, converted to 378 * a {@link String}. 379 * 380 * @param input data source 381 * @return string | error 382 */ 383 @Override 384 public DataResult<String> getStringValue(final ConfigurationNode input) { 385 final @Nullable String value = input.getString(); 386 if (value != null) { 387 return DataResult.success(value); 388 } 389 390 return DataResult.error("Not a string: " + input); 391 } 392 393 /** 394 * Create a new node using this ops instance's node factory, 395 * and set its value to the provided number. 396 * 397 * @param value value 398 * @return new node with value 399 */ 400 @Override 401 public ConfigurationNode createNumeric(final Number value) { 402 return empty().raw(requireNonNull(value, "value")); 403 } 404 405 /** 406 * Create a new node using this ops instance's node factory, 407 * and set its value to the provided boolean. 408 * 409 * @param value value 410 * @return new node with value 411 */ 412 @Override 413 public ConfigurationNode createBoolean(final boolean value) { 414 return empty().raw(value); 415 } 416 417 /** 418 * Create a new node using this ops instance's node factory, 419 * and set its value to the provided string. 420 * 421 * @param value value 422 * @return new node with value 423 */ 424 @Override 425 public ConfigurationNode createString(final String value) { 426 return empty().raw(requireNonNull(value, "value")); 427 } 428 429 /** 430 * Return a result where if {@code prefix} is empty, the node is 431 * {@code value}, but otherwise returns an error. 432 * 433 * @param prefix starting value 434 * @param value to update base with 435 * @return result of updated node or error 436 */ 437 @Override 438 public DataResult<ConfigurationNode> mergeToPrimitive(final ConfigurationNode prefix, final ConfigurationNode value) { 439 if (!prefix.empty()) { 440 return DataResult.error("Cannot merge " + value + " into non-empty node " + prefix); 441 } 442 return DataResult.success(guardOutputRead(value)); 443 } 444 445 /** 446 * Appends element {@code value} to list node {@code input}. 447 * 448 * @param input base node. Must be empty or of list type 449 * @param value value to add as element to the list 450 * @return success with modified node, or error if {@code input} contains a 451 * non-{@link ConfigurationNode#isList() list} value 452 */ 453 @Override 454 public DataResult<ConfigurationNode> mergeToList(final ConfigurationNode input, final ConfigurationNode value) { 455 if (input.isList() || input.empty()) { 456 final ConfigurationNode ret = guardOutputRead(input); 457 ret.appendListNode().from(value); 458 return DataResult.success(ret); 459 } 460 461 return DataResult.error("mergeToList called on a node which is not a list: " + input, input); 462 } 463 464 /** 465 * Appends nodes in {@code values} to copy of list node {@code input}. 466 * 467 * @param input base node. Must be empty or of list type 468 * @param values list of values to append to base node 469 * @return success with modified node, or error if {@code input} contains a 470 * non-{@link ConfigurationNode#isList() list} value 471 */ 472 @Override 473 public DataResult<ConfigurationNode> mergeToList(final ConfigurationNode input, final List<ConfigurationNode> values) { 474 if (input.isList() || input.empty()) { 475 final ConfigurationNode ret = guardInputWrite(input); 476 for (ConfigurationNode node : values) { 477 ret.appendListNode().from(node); 478 } 479 return DataResult.success(ret); 480 } 481 482 return DataResult.error("mergeToList called on a node which is not a list: " + input, input); 483 } 484 485 /** 486 * Update the child of {@code input} at {@code key} with {@code value}. 487 * 488 * <p>This operation will only affect the returned copy of the input node 489 * 490 * @param input base node. Must be empty or of map type 491 * @param key key relative to base node 492 * @param value value to set at empty node 493 * @return success with modified node, or error if {@code input} contains a 494 * non-{@link ConfigurationNode#isList() list} value 495 */ 496 @Override 497 public DataResult<ConfigurationNode> mergeToMap(final ConfigurationNode input, final ConfigurationNode key, final ConfigurationNode value) { 498 if (input.isMap() || input.empty()) { 499 final ConfigurationNode copied = guardInputWrite(input); 500 copied.node(keyFrom(key)).from(value); 501 return DataResult.success(copied); 502 } 503 504 return DataResult.error("mergeToMap called on a node which is not a map: " + input, input); 505 } 506 507 /** 508 * Return a stream of pairs of (key, value) for map data in the input node. 509 * 510 * <p>If the input node is non-empty and not a map, the result will 511 * be a failure. 512 * 513 * @param input input node 514 * @return result, if successful, of a stream of pairs (key, value) of 515 * entries in the input node. 516 */ 517 @Override 518 public DataResult<Stream<Pair<ConfigurationNode, ConfigurationNode>>> getMapValues(final ConfigurationNode input) { 519 if (input.empty() || input.isMap()) { 520 return DataResult.success(input.childrenMap().entrySet().stream() 521 .map(entry -> Pair.of(BasicConfigurationNode.root(input.options()).raw(entry.getKey()), 522 guardOutputRead(entry.getValue())))); 523 } 524 525 return DataResult.error("Not a map: " + input); 526 } 527 528 /** 529 * Get a map-like view of a copy of the contents of {@code input}. 530 * 531 * <p>If the input node is non-empty and not a map, the result will 532 * be a failure. 533 * 534 * @param input input node 535 * @return result, if successful, of map-like view of a copy of the input 536 */ 537 @Override 538 public DataResult<MapLike<ConfigurationNode>> getMap(final ConfigurationNode input) { 539 if (input.empty() || input.isMap()) { 540 return DataResult.success(new NodeMaplike(this, input.options(), input.childrenMap())); 541 } else { 542 return DataResult.error("Input node is not a map"); 543 } 544 } 545 546 /** 547 * Get a consumer that takes an action to perform on every element of 548 * list node {@code input}. 549 * 550 * <p>As an example, to print out every node in a list 551 * (minus error checking): 552 * <pre> 553 * getList(listNode).result().get() 554 * .accept(element -> System.out.println(element); 555 * </pre> 556 * 557 * @param input data source 558 * @return result, that if successful will take an action to perform on 559 * every element 560 */ 561 @Override 562 public DataResult<Consumer<Consumer<ConfigurationNode>>> getList(final ConfigurationNode input) { 563 if (input.isList()) { 564 return DataResult.success(action -> { 565 for (ConfigurationNode child : input.childrenList()) { 566 action.accept(guardOutputRead(child)); 567 } 568 }); 569 } else { 570 return DataResult.error("Input node is not a list"); 571 } 572 } 573 574 /** 575 * Get the contents of list node {@code input} as a {@link Stream} of nodes. 576 * 577 * @param input data source 578 * @return if node is empty or a list, stream of nodes 579 */ 580 @Override 581 public DataResult<Stream<ConfigurationNode>> getStream(final ConfigurationNode input) { 582 if (input.empty() || input.isList()) { 583 final Stream<ConfigurationNode> stream = input.childrenList().stream().map(this::guardOutputRead); 584 return DataResult.success(stream); 585 } 586 587 return DataResult.error("Not a list: " + input); 588 } 589 590 /** 591 * Create a new node containing the map entries from the 592 * stream {@code values}. 593 * 594 * <p>Keys will be interpreted as a single Object, and can only 595 * currently access direct children. 596 * 597 * @param values entries in the map 598 * @return newly created node 599 */ 600 @Override 601 public ConfigurationNode createMap(final Stream<Pair<ConfigurationNode, ConfigurationNode>> values) { 602 final ConfigurationNode ret = empty(); 603 604 values.forEach(p -> ret.node(keyFrom(p.getFirst())).from(p.getSecond())); 605 606 return ret; 607 } 608 609 /** 610 * Create a new node containing the map entries from the 611 * map {@code values}. 612 * 613 * <p>Keys will be interpreted as a single Object, and can only 614 * currently access direct children. 615 * 616 * @param values unwrapped node map 617 * @return newly created node 618 */ 619 @Override 620 public ConfigurationNode createMap(final Map<ConfigurationNode, ConfigurationNode> values) { 621 final ConfigurationNode ret = empty(); 622 623 for (Map.Entry<ConfigurationNode, ConfigurationNode> entry : values.entrySet()) { 624 ret.node(keyFrom(entry.getKey())).from(entry.getValue()); 625 } 626 627 return ret; 628 } 629 630 /** 631 * Create a new node containing values emitted by {@code input} as 632 * list elements. 633 * 634 * @param input data source 635 * @return newly created node 636 */ 637 @Override 638 public ConfigurationNode createList(final Stream<ConfigurationNode> input) { 639 final ConfigurationNode ret = empty(); 640 input.forEach(it -> ret.appendListNode().from(it)); 641 return ret; 642 } 643 644 /** 645 * Get a copy of {@code input} without the value at node {@code key}. 646 * 647 * <p>If the input node is not a map, the input node will be returned. 648 * 649 * @param input data source 650 * @param key key to the node to be removed 651 * @return if node removed, a copy of the input without node, 652 * otherwise input 653 */ 654 @Override 655 public ConfigurationNode remove(final ConfigurationNode input, final String key) { 656 if (input.isMap()) { 657 final ConfigurationNode ret = guardInputWrite(input); 658 ret.node(key).raw(null); 659 return ret; 660 } 661 662 return input; 663 } 664 665 /** 666 * Attempt to get the child of {@code input} at {@code key}. 667 * 668 * @param input data source 669 * @param key child key 670 * @return success containing child if child is non-virtual, 671 * otherwise failure 672 */ 673 @Override 674 public DataResult<ConfigurationNode> get(final ConfigurationNode input, final String key) { 675 final ConfigurationNode ret = input.node(key); 676 return ret.virtual() ? DataResult.error("No element " + key + " in the map " + input) : DataResult.success(guardOutputRead(ret)); 677 } 678 679 /** 680 * Get a child of the provided node at {@code key}. 681 * 682 * <p>Keys will be interpreted as a single Object, and can only 683 * currently access direct children. 684 * 685 * @param input parent node 686 * @param key wrapped key of child 687 * @return success containing child if child is non-virtual, 688 * otherwise failure 689 */ 690 @Override 691 public DataResult<ConfigurationNode> getGeneric(final ConfigurationNode input, final ConfigurationNode key) { 692 final ConfigurationNode ret = input.node(keyFrom(key)); 693 return ret.virtual() ? DataResult.error("No element " + key + " in the map " + input) : DataResult.success(guardOutputRead(ret)); 694 } 695 696 /** 697 * Update a copy of {@code input} with {@code value} at path {@code key}. 698 * 699 * @param input data source 700 * @param key key of child node 701 * @param value value for child node 702 * @return updated parent node 703 */ 704 @Override 705 public ConfigurationNode set(final ConfigurationNode input, final String key, final ConfigurationNode value) { 706 final ConfigurationNode ret = guardInputWrite(input); 707 ret.node(key).from(value); 708 return ret; 709 } 710 711 /** 712 * Copies the input node and transform its child at {@code key}. 713 * 714 * <p>Return a copy of the input node with the child at {@code key} 715 * transformed by the provided function 716 * 717 * <p>If there is no value at {@code key}, the input node will be 718 * returned unmodified. 719 * 720 * @param input base value 721 * @param key key to change 722 * @param function function to process the node at {@code wrappedKey} 723 * @return an updated copy of input node 724 */ 725 @Override 726 public ConfigurationNode update(final ConfigurationNode input, final String key, final Function<ConfigurationNode, ConfigurationNode> function) { 727 if (input.node(key).virtual()) { 728 return input; 729 } 730 731 final ConfigurationNode ret = guardInputWrite(input); 732 final ConfigurationNode child = ret.node(key); 733 child.from(function.apply(child)); 734 return ret; 735 } 736 737 /** 738 * Copies the input node and transform the node at {@code wrappedKey}. 739 * 740 * <p>Return a copy of the input node with the child at {@code wrappedKey} 741 * transformed by the provided function 742 * 743 * <p>If there is no value at {@code wrappedKey}, the input node will be 744 * returned unmodified. 745 * 746 * <p>Keys will be interpreted as a single Object, and can only 747 * currently access direct children. 748 * 749 * @param input base value 750 * @param wrappedKey key to change 751 * @param function function to process the node at {@code wrappedKey} 752 * @return an updated copy of input node 753 */ 754 @Override 755 public ConfigurationNode updateGeneric(final ConfigurationNode input, final ConfigurationNode wrappedKey, 756 final Function<ConfigurationNode, ConfigurationNode> function) { 757 final Object key = keyFrom(wrappedKey); 758 if (input.node(key).virtual()) { 759 return input; 760 } 761 762 final ConfigurationNode ret = guardInputWrite(input); 763 764 final ConfigurationNode child = ret.node(key); 765 child.from(function.apply(child)); 766 return ret; 767 } 768 769 @Override 770 public String toString() { 771 return "Configurate"; 772 } 773 774 /** 775 * Protection level for configuration node accesses through ops instance. 776 * 777 * @since 4.0.0 778 */ 779 public enum Protection { 780 /** 781 * When an operation is executed on the node, make a deep copy of the 782 * result. 783 */ 784 COPY_DEEP, 785 786 /** 787 * Directly pass on nodes, still attached to their original structure. 788 */ 789 NONE 790 791 } 792 793}