Skip to content

Commit

Permalink
TAP5-2640: Added JSONCollectors, modified JSONObject#merge
Browse files Browse the repository at this point in the history
For simpler usage with streams the class JSONCollectors provides static helper methods.

During testing it became clear that the default Map#merge(...)  wasn't viable for the general JSONObject logic (exception on unknow keys), so it had to be overriden and adapted.
  • Loading branch information
benweidig committed Jan 2, 2021
1 parent c9969fd commit dbd66fd
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 2 deletions.
2 changes: 2 additions & 0 deletions 5.7_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Original file line number Diff line number Diff line change
@@ -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<?, ?, JSONArray> 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<?, ?, JSONObject> toMap(Function<?, String> keyMapper,
Function<?, Object> valueMapper)
{
return Collectors.toMap(keyMapper,
valueMapper,
(u, v) -> {
throw new IllegalStateException(String.format("Duplicate key %s", u));
},
JSONObject::new);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1106,6 +1108,46 @@ public Set<Entry<String, Object>> entrySet()
{
return nameValuePairs.entrySet();
}


}
/**
* 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<? super Object, ? super Object, ? extends Object> 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;
}

}
101 changes: 101 additions & 0 deletions tapestry-json/src/test/groovy/json/specs/JSONCollectorsSpec.groovy
Original file line number Diff line number Diff line change
@@ -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()
}

}

0 comments on commit dbd66fd

Please sign in to comment.