diff --git a/5.7_RELEASE_NOTES.md b/5.7_RELEASE_NOTES.md index 00e47c3561..93c0615076 100644 --- a/5.7_RELEASE_NOTES.md +++ b/5.7_RELEASE_NOTES.md @@ -31,3 +31,5 @@ Additionally to the new implemented interfaces, the types itself throw more desc * `JSONSyntaxException` * `JSONTypeMismatchException` * `JSONValueNotFoundException` + +The class `JSONCollectors` provides access to `java.util.streams.Collector` for `JSONArray` and `JSONObject`. diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONCollectors.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONCollectors.java new file mode 100644 index 0000000000..f82e6625f3 --- /dev/null +++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONCollectors.java @@ -0,0 +1,76 @@ +// Copyright 2021 The Apache Software Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.apache.tapestry5.json; + +import java.util.function.Function; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +/** + * Implementations of {@link java.util.streams.Collector} that implement reductions + * to {@code JSONArray} and to {@code JSONObject}. + * + * @since 5.7 + */ + +public final class JSONCollectors +{ + private JSONCollectors() + { + } + + /** + * Returns a {@code Collector} that accumulates the input elements into a + * new {@code JSONArray}. + * + * @return a {@code Collector} which collects all the input elements into a + * {@code JSONArray}, in encounter order + * @since 5.7 + */ + public static Collector toArray() + { + return Collector.of(JSONArray::new, // + JSONArray::add, JSONArray::putAll); + } + + /** + * Returns a {@code Collector} that accumulates elements into a + * {@code JSONObject} whose keys and values are the result of applying the provided + * mapping functions to the input elements. + * + * In case of duplicate keys an {@code IllegalStateException} is + * thrown when the collection operation is performed. + * + * @param keyMapper + * a mapping function to produce String keys + * @param valueMapper + * a mapping function to produce values + * @return a {@code Collector} which collects elements into a {@code JSONObject} + * whose keys and values are the result of applying mapping functions to + * the input elements + * @since 5.7 + */ + public static Collector toMap(Function keyMapper, + Function valueMapper) + { + return Collectors.toMap(keyMapper, + valueMapper, + (u, v) -> { + throw new IllegalStateException(String.format("Duplicate key %s", u)); + }, + JSONObject::new); + } + +} diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONObject.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONObject.java index 724cab725e..b639b85967 100644 --- a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONObject.java +++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONObject.java @@ -22,7 +22,9 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.function.BiFunction; import org.apache.tapestry5.json.exceptions.JSONTypeMismatchException; import org.apache.tapestry5.json.exceptions.JSONValueNotFoundException; @@ -1106,6 +1108,46 @@ public Set> entrySet() { return nameValuePairs.entrySet(); } - -} \ No newline at end of file + /** + * If the specified key is not already associated with a value or is + * associated with null, associates it with the given non-null value. + * + * Otherwise, replaces the associated value with the results of the given + * remapping function, or removes if the result is {@code null}. + * + * @param key + * key with which the resulting value is to be associated + * @param value + * the non-null value to be merged with the existing value + * associated with the key or, if no existing value or a null value + * is associated with the key, to be associated with the key + * @param remappingFunction + * the function to recompute a value if present + * @return the new value associated with the specified key, or null if no + * value is associated with the key + * @throws NullPointerException + * if the specified key is null or the value or remappingFunction + * is null + * @since 5.7 + */ + @Override + public Object merge(String key, Object value, + BiFunction remappingFunction) + { + // We need to override the merge method due to the default implementation using + // #get(String) to check for the key, which will throw a {@code JSONValueNotFoundException} + // if not found. + + Objects.requireNonNull(remappingFunction); + Objects.requireNonNull(value); + + Object oldValue = opt(key); + Object newValue = (oldValue == null) ? value : remappingFunction.apply(oldValue, value); + + put(key, newValue); + + return newValue; + } + +} diff --git a/tapestry-json/src/test/groovy/json/specs/JSONCollectorsSpec.groovy b/tapestry-json/src/test/groovy/json/specs/JSONCollectorsSpec.groovy new file mode 100644 index 0000000000..a8c4523120 --- /dev/null +++ b/tapestry-json/src/test/groovy/json/specs/JSONCollectorsSpec.groovy @@ -0,0 +1,101 @@ +package json.specs + +import org.apache.tapestry5.json.JSONCollectors +import org.apache.tapestry5.json.exceptions.JSONInvalidTypeException + +import java.util.stream.Stream + +import spock.lang.Specification + +class JSONCollectorsSpec extends Specification { + + def "collect stream to array"() { + + given: + + def stringValue = "a string value" + def longValue = 3L + + def stream = Stream.of(stringValue, longValue) + + when: + + def collected = stream.collect(JSONCollectors.toArray()); + + then: + + collected.size() == 2 + collected.get(0) == stringValue + collected.get(1) == longValue + } + + def "collect stream to array invalid type"() { + + given: + + def stringValue = "a string value" + def longValue = 3L + def invalidValue = new java.util.Date() + + def stream = Stream.of(stringValue, longValue, invalidValue) + + when: + + def collected = stream.collect(JSONCollectors.toArray()); + + then: + + JSONInvalidTypeException e = thrown() + } + + def "collect stream to map"() { + + given: + + def first = new Tuple("first key", "a string value") + def second = new Tuple("second key", 3L) + + def stream = Stream.of(first, second) + + when: + + def collected = stream.collect(JSONCollectors.toMap({ t -> t.get(0) }, { t -> t.get(1) })); + + then: + + collected.size() == 2 + collected.get(first.get(0)) == first.get(1) + collected.get(second.get(0)) == second.get(1) + } + + def "collect stream to map invalid type"() { + + given: + + def first = new Tuple("first key", "a string value") + def second = new Tuple("second key", 3L) + def third = new Tuple("invalid type", new java.util.Date()) + + def stream = Stream.of(first, second, third) + + when: + + def collected = stream.collect(JSONCollectors.toMap({ t -> t.get(0) }, { t -> t.get(1) })); + + then: + + JSONInvalidTypeException e = thrown() + } + + def "collect stream to map duplicate key"() { + + when: + + def collected = Stream.of("first", "second", "first").collect(JSONCollectors.toMap({ v -> v }, { v -> v })) + + then: + + IllegalStateException e = thrown() + } + +}