GH-291 Provide an option to create a mutable Tuple

This fixes spring-cloud/spring-cloud-stream#290
This commit is contained in:
Eric Bottard
2016-01-27 14:05:23 +01:00
committed by Mark Pollack
parent 7477fcbee9
commit 7fb834981a
9 changed files with 333 additions and 41 deletions

View File

@@ -25,6 +25,8 @@ import java.util.Collections;
import java.util.List;
import java.util.Properties;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.Registration;
import org.junit.Before;
import org.junit.Test;
@@ -44,9 +46,6 @@ import org.springframework.messaging.support.GenericMessage;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.Registration;
/**
* @author Gary Russell
* @author David Turanski
@@ -177,7 +176,7 @@ public class MessageChannelBinderSupportTests {
@Test
public void testTupleSerialization() {
Tuple payload = TupleBuilder.tuple().of("foo", "bar");
MessageValues convertedValues = binder.serializePayloadIfNecessary(new GenericMessage<Tuple>(payload));
MessageValues convertedValues = binder.serializePayloadIfNecessary(new GenericMessage<>(payload));
Message<?> converted = convertedValues.toMessage();
MimeType mimeType = contentTypeResolver.resolve(converted.getHeaders());
assertEquals("application", mimeType.getType());

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2016 the original author or authors.
*
* 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.springframework.cloud.stream.tuple;
import java.util.List;
import org.springframework.core.convert.support.ConfigurableConversionService;
/**
* Extension of {@link DefaultTuple} that also allows tuple {@link MutableTuple mutation}.
*
* <p>Note that this implementation is not threadsafe.</p>
*
* @author Eric Bottard
*/
public class DefaultMutableTuple extends DefaultTuple implements MutableTuple {
/**
* Construct a new DefaultMutableTuple, given a list of names and object values along
* with a conversion service.
* @param names The list of String names to associate with the list of Object values
* @param values The list of Object values to associate with the list of String names
* @param configurableConversionService A conversion service instance that can Converting strings to Java types.
*/
public DefaultMutableTuple(List<String> names, List<Object> values, ConfigurableConversionService configurableConversionService) {
super(names, values, configurableConversionService);
}
@Override
public void setValue(int index, Object value) {
if (index < 0 || index >= size()) {
throw new IndexOutOfBoundsException();
}
values.set(index, value);
}
@Override
public void setValue(String name, Object value) {
int index = indexOf(name);
if (index != -1) {
setValue(index, value);
} else {
names.add(name);
values.add(value);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2015 the original author or authors.
* Copyright 2013-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -36,22 +36,31 @@ import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Default implementation of Tuple interface
* Default implementation of Tuple interface.
*
* @author Mark Pollack
* @author David Turanski
* @author Michael Minella
*/
public class DefaultTuple implements Tuple {
private List<String> names;
protected List<String> names;
private List<Object> values;
protected List<Object> values;
private transient ConfigurableConversionService configurableConversionService;
protected transient ConfigurableConversionService configurableConversionService;
private transient Converter<Tuple, String> tupleToStringConverter = new TupleToJsonStringConverter();
protected transient Converter<Tuple, String> tupleToStringConverter = new TupleToJsonStringConverter();
public DefaultTuple(List<String> names, List<Object> values, ConfigurableConversionService
/**
* Construct a new DefaultMutableTuple, given a list of names and object values along
* with a conversion service.
*
* @param names The list of names to associate with the list of values
* @param values The list of values to associate with the list of names
* @param configurableConversionService A conversion service instance that can Converting strings to Java types.
*/
public DefaultTuple(List<String> names, List<Object> values, ConfigurableConversionService
configurableConversionService) {
Assert.notNull(names);
Assert.notNull(values);
@@ -521,22 +530,12 @@ public class DefaultTuple implements Tuple {
}
}
/*
* (non-Javadoc)
*
* @see org.springframework.cloud.stream.tuple.Tuple#getValue(java.lang.String, java.lang.Class)
*/
@Override
public <T> T getValue(String name, Class<T> valueClass) {
Object value = values.get(indexOf(name));
return convert(value, valueClass);
}
/*
* (non-Javadoc)
*
* @see org.springframework.cloud.stream.tuple.Tuple#getValue(int, java.lang.Class)
*/
@Override
public <T> T getValue(int index, Class<T> valueClass) {
return convert(values.get(index), valueClass);

View File

@@ -18,13 +18,13 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.core.convert.converter.Converter;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.convert.converter.Converter;
/**
* @author David Turanski
*/

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2016 the original author or authors.
*
* 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.springframework.cloud.stream.tuple;
/**
* Extension of {@link Tuple} that allows mutating and addition of fields.
*
* @author Eric Bottard
*/
public interface MutableTuple extends Tuple {
/**
* Sets the value of an already existing property, given its index.
*/
void setValue(int index, Object value);
/**
* Sets the value of a property, by name. If this Tuple does not currently hold
* a property under that name, a new mapping is added at the end and the size of this
* Tuple is extended by 1.
*/
void setValue(String name, Object value);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,11 +26,12 @@ import org.springframework.core.convert.ConversionFailedException;
* Data structure that stores a fixed number of ordered key-value pairs and adds convenience methods to access values in
* a type-safe manner.
*
* The structure is immutable once created and values to not need to be of the same type. When accessing values,
* Values do not need to be of the same type. When accessing values,
* Spring's type conversion system is used to convert the value type to the requested type. The type conversion system
* is extensible.
*
* Tuples are created using the TupleBuilder class.
* This interface only allows querying properties. Concrete Tuples can also implement {@link MutableTuple} to allow
* mutation. Tuples are created using the TupleBuilder class.
*
* @author Mark Pollack
*

View File

@@ -47,7 +47,7 @@ public class TupleBuilder {
private List<Object> values = new ArrayList<>();
private static final ConfigurableConversionService defaultConversionService;
private final boolean mutable;
private ConfigurableConversionService customConversionService = null;
@@ -55,6 +55,8 @@ public class TupleBuilder {
private final static Locale DEFAULT_LOCALE = Locale.US;
private static final ConfigurableConversionService defaultConversionService;
private static Converter<Tuple, String> tupleToStringConverter = new TupleToJsonStringConverter();
private static Converter<String, Tuple> stringToTupleConverter = new JsonStringToTupleConverter();
@@ -68,8 +70,22 @@ public class TupleBuilder {
defaultConversionService.addConverter(new StringToDateConverter(dateFormat));
}
/**
* Return a new builder that will create immutable Tuples.
*/
public static TupleBuilder mutableTuple() {
return new TupleBuilder(true);
}
/**
* Return a new builder that will create {@link MutableTuple}.
*/
public static TupleBuilder tuple() {
return new TupleBuilder();
return new TupleBuilder(false);
}
private TupleBuilder(boolean mutable) {
this.mutable = mutable;
}
public Tuple of(String k1, Object v1) {
@@ -161,13 +177,10 @@ public class TupleBuilder {
protected Tuple newTuple(List<String> names, List<Object> values) {
DefaultTuple tuple;
if (customConversionService != null) {
tuple = new DefaultTuple(names, values, customConversionService);
}
else {
tuple = new DefaultTuple(names, values, defaultConversionService);
}
ConfigurableConversionService conversionService = customConversionService != null ? customConversionService : defaultConversionService;
tuple = mutable
? new DefaultMutableTuple(names, values, conversionService)
: new DefaultTuple(names, values, conversionService);
tuple.setTupleToStringConverter(tupleToStringConverter);
return tuple;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
package org.springframework.cloud.stream.tuple.spel;
import org.springframework.cloud.stream.tuple.MutableTuple;
import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.PropertyAccessor;
@@ -24,15 +25,16 @@ import org.springframework.cloud.stream.tuple.Tuple;
/**
* A {@link PropertyAccessor} implementation that enables reading of {@link Tuple} values using dot notation within SpEL
* expressions. Writing is not supported since {@link Tuple}s are immutable.
* expressions.
*
* @author Mark Fisher
* @author Eric Bottard
*/
public class TuplePropertyAccessor implements PropertyAccessor {
@Override
public Class<?>[] getSpecificTargetClasses() {
return new Class<?>[] { Tuple.class };
return new Class<?>[] { Tuple.class, MutableTuple.class};
}
@Override
@@ -85,12 +87,18 @@ public class TuplePropertyAccessor implements PropertyAccessor {
@Override
public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException {
return false;
return target instanceof MutableTuple;
}
@Override
public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException {
throw new UnsupportedOperationException("Tuple is immutable");
MutableTuple tuple = (MutableTuple) target;
Integer index = maybeIndex(name, tuple);
if (index != null) {
tuple.setValue(index, newValue);
} else {
tuple.setValue(name, newValue);
}
}
/**

View File

@@ -0,0 +1,171 @@
/*
* Copyright 2016 the original author or authors.
*
* 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.springframework.cloud.stream.tuple.spel;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import org.hamcrest.CoreMatchers;
import org.junit.Before;
import org.junit.Test;
import org.springframework.cloud.stream.tuple.Tuple;
import org.springframework.cloud.stream.tuple.TupleBuilder;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
/**
* Unit tests for {@link TuplePropertyAccessor}.
*
* @author Eric Bottard
* @author Mark Fisher
*/
public class TuplePropertyAccessorTests {
private final SpelExpressionParser parser = new SpelExpressionParser();
private final StandardEvaluationContext context = new StandardEvaluationContext();
@Before
public void setup() {
this.context.addPropertyAccessor(new TuplePropertyAccessor());
}
@Test
public void testSimplePropertyByFieldName() {
Tuple tuple = TupleBuilder.tuple().of("foo", "bar");
String result = evaluate("foo", tuple, String.class);
assertThat(result, is("bar"));
}
@Test
public void testSimplePropertyByIndex() {
Tuple tuple = TupleBuilder.tuple().of("foo", "bar");
String result = evaluate("['0']", tuple, String.class);
assertThat(result, is("bar"));
}
@Test(expected = SpelEvaluationException.class)
public void failOnNegativeIndex() {
Tuple tuple = TupleBuilder.tuple().of("foo", "bar");
evaluate("['-3']", tuple, String.class);
}
@Test
public void testNestedPropertyByFieldName() {
Tuple child = TupleBuilder.tuple().of("b", 123);
Tuple tuple = TupleBuilder.tuple().of("a", child);
int result = evaluate("a.b", tuple, Integer.class);
assertThat(result, is(123));
}
@Test
public void testNestedPropertyByIndex() {
Tuple child = TupleBuilder.tuple().of("b", 123);
Tuple tuple = TupleBuilder.tuple().of("a", child);
int result = evaluate("a['0']", tuple, Integer.class);
assertThat(result, is(123));
}
@Test
public void testNestedPropertyByIndexOnly() {
Tuple child = TupleBuilder.tuple().of("b", 123);
Tuple tuple = TupleBuilder.tuple().of("a", child);
int result = evaluate("['0']['0']", tuple, Integer.class);
assertThat(result, is(123));
}
@Test
public void testArrayPropertyByFieldName() {
Tuple tuple = TupleBuilder.tuple().of("numbers", new Integer[] { 1, 2, 3 });
int result = evaluate("numbers[1]", tuple, Integer.class);
assertThat(result, is(2));
}
@Test
public void testArrayPropertyByIndex() {
Tuple tuple = TupleBuilder.tuple().of("numbers", new Integer[] { 1, 2, 3 });
int result = evaluate("['0'][0]", tuple, Integer.class);
assertThat(result, is(1));
}
@Test
public void testNestedArrayPropertyByFieldName() {
Tuple child = TupleBuilder.tuple().of("numbers", new Integer[] { 7, 8, 9 });
Tuple tuple = TupleBuilder.tuple().of("child", child);
int result = evaluate("child.numbers[1]", tuple, Integer.class);
assertThat(result, is(8));
}
@Test
public void testNestedArrayPropertyByIndex() {
Tuple child = TupleBuilder.tuple().of("numbers", new Integer[] { 7, 8, 9 });
Tuple tuple = TupleBuilder.tuple().of("child", child);
int result = evaluate("child['0'][2]", tuple, Integer.class);
assertThat(result, is(9));
}
@Test
public void testNestedArrayPropertyByIndexOnly() {
Tuple child = TupleBuilder.tuple().of("numbers", new Integer[] { 7, 8, 9 });
Tuple tuple = TupleBuilder.tuple().of("child", child);
int result = evaluate("['0']['0'][1]", tuple, Integer.class);
assertThat(result, is(8));
}
// Write
@Test
public void testSimpleWriteOnExistingProperty() {
Tuple tuple = TupleBuilder.mutableTuple().of("foo", "bar");
write("foo", tuple, 123);
assertThat(tuple.getInt("foo"), is(123));
}
@Test
public void testSimpleWriteOnNewProperty() {
Tuple tuple = TupleBuilder.mutableTuple().of("foo", "bar");
write("other", tuple, 123);
assertThat(tuple.getInt("other"), is(123));
assertThat(tuple.getValue("foo"), CoreMatchers.<Object>is("bar"));
}
@Test
public void testSimpleWriteOnExistingPropertyByIndex() {
Tuple tuple = TupleBuilder.mutableTuple().of("foo", "bar");
write("['0']", tuple, 123);
assertThat(tuple.getInt("foo"), is(123));
}
@Test
public void testSimpleWriteOnNewPropertyByIndex() {
Tuple tuple = TupleBuilder.mutableTuple().of("foo", "bar");
write("['12']", tuple, 123);
assertThat(tuple.getInt("12"), is(123));
assertThat(tuple.getValue("foo"), CoreMatchers.<Object>is("bar"));
}
private <T> T evaluate(String expression, Tuple tuple, Class<T> expectedType) {
return parser.parseExpression(expression).getValue(this.context, tuple, expectedType);
}
private void write(String expression, Tuple tuple, Object value) {
parser.parseExpression(expression).setValue(this.context, tuple, value);
}
}