Introduce computeAttribute() in AttributeAccessor
This commit introduces computeAttribute() as an interface default method in the AttributeAccessor API. This serves as a convenience analogous to the computeIfAbsent() method in java.util.Map. Closes gh-26281
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 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,20 +16,24 @@
|
||||
|
||||
package org.springframework.core;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Interface defining a generic contract for attaching and accessing metadata
|
||||
* to/from arbitrary objects.
|
||||
*
|
||||
* @author Rob Harrop
|
||||
* @author Sam Brannen
|
||||
* @since 2.0
|
||||
*/
|
||||
public interface AttributeAccessor {
|
||||
|
||||
/**
|
||||
* Set the attribute defined by {@code name} to the supplied {@code value}.
|
||||
* If {@code value} is {@code null}, the attribute is {@link #removeAttribute removed}.
|
||||
* <p>If {@code value} is {@code null}, the attribute is {@link #removeAttribute removed}.
|
||||
* <p>In general, users should take care to prevent overlaps with other
|
||||
* metadata attributes by using fully-qualified names, perhaps using
|
||||
* class or package names as prefix.
|
||||
@@ -40,16 +44,48 @@ public interface AttributeAccessor {
|
||||
|
||||
/**
|
||||
* Get the value of the attribute identified by {@code name}.
|
||||
* Return {@code null} if the attribute doesn't exist.
|
||||
* <p>Return {@code null} if the attribute doesn't exist.
|
||||
* @param name the unique attribute key
|
||||
* @return the current value of the attribute, if any
|
||||
*/
|
||||
@Nullable
|
||||
Object getAttribute(String name);
|
||||
|
||||
/**
|
||||
* Compute a new value for the attribute identified by {@code name} if
|
||||
* necessary and {@linkplain #setAttribute set} the new value in this
|
||||
* {@code AttributeAccessor}.
|
||||
* <p>If a value for the attribute identified by {@code name} already exists
|
||||
* in this {@code AttributeAccessor}, the existing value will be returned
|
||||
* without applying the supplied compute function.
|
||||
* <p>The default implementation of this method is not thread safe but can
|
||||
* overridden by concrete implementations of this interface.
|
||||
* @param <T> the type of the attribute value
|
||||
* @param name the unique attribute key
|
||||
* @param computeFunction a function that computes a new value for the attribute
|
||||
* name; the function must not return a {@code null} value
|
||||
* @return the existing value or newly computed value for the named attribute
|
||||
* @see #getAttribute(String)
|
||||
* @see #setAttribute(String, Object)
|
||||
* @since 5.3.3
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
default <T> T computeAttribute(String name, Function<String, T> computeFunction) {
|
||||
Assert.notNull(name, "Name must not be null");
|
||||
Assert.notNull(computeFunction, "Compute function must not be null");
|
||||
Object value = getAttribute(name);
|
||||
if (value == null) {
|
||||
value = computeFunction.apply(name);
|
||||
Assert.state(value != null,
|
||||
() -> String.format("Compute function must not return null for attribute named '%s'", name));
|
||||
setAttribute(name, value);
|
||||
}
|
||||
return (T) value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the attribute identified by {@code name} and return its value.
|
||||
* Return {@code null} if no attribute under {@code name} is found.
|
||||
* <p>Return {@code null} if no attribute under {@code name} is found.
|
||||
* @param name the unique attribute key
|
||||
* @return the last value of the attribute, if any
|
||||
*/
|
||||
@@ -58,7 +94,7 @@ public interface AttributeAccessor {
|
||||
|
||||
/**
|
||||
* Return {@code true} if the attribute identified by {@code name} exists.
|
||||
* Otherwise return {@code false}.
|
||||
* <p>Otherwise return {@code false}.
|
||||
* @param name the unique attribute key
|
||||
*/
|
||||
boolean hasAttribute(String name);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 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.
|
||||
@@ -19,6 +19,7 @@ package org.springframework.core;
|
||||
import java.io.Serializable;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -32,6 +33,7 @@ import org.springframework.util.StringUtils;
|
||||
*
|
||||
* @author Rob Harrop
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 2.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
@@ -59,6 +61,17 @@ public abstract class AttributeAccessorSupport implements AttributeAccessor, Ser
|
||||
return this.attributes.get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T computeAttribute(String name, Function<String, T> computeFunction) {
|
||||
Assert.notNull(name, "Name must not be null");
|
||||
Assert.notNull(computeFunction, "Compute function must not be null");
|
||||
Object value = this.attributes.computeIfAbsent(name, computeFunction);
|
||||
Assert.state(value != null,
|
||||
() -> String.format("Compute function must not return null for attribute named '%s'", name));
|
||||
return (T) value;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Object removeAttribute(String name) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 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.
|
||||
@@ -17,39 +17,61 @@
|
||||
package org.springframework.core;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link AttributeAccessorSupport}.
|
||||
*
|
||||
* @author Rob Harrop
|
||||
* @author Sam Brannen
|
||||
* @since 2.0
|
||||
*/
|
||||
class AttributeAccessorSupportTests {
|
||||
|
||||
private static final String NAME = "foo";
|
||||
private static final String NAME = "name";
|
||||
|
||||
private static final String VALUE = "bar";
|
||||
private static final String VALUE = "value";
|
||||
|
||||
private final AttributeAccessor attributeAccessor = new SimpleAttributeAccessorSupport();
|
||||
|
||||
private AttributeAccessor attributeAccessor = new SimpleAttributeAccessorSupport();
|
||||
|
||||
@Test
|
||||
void setAndGet() throws Exception {
|
||||
void setAndGet() {
|
||||
this.attributeAccessor.setAttribute(NAME, VALUE);
|
||||
assertThat(this.attributeAccessor.getAttribute(NAME)).isEqualTo(VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void setAndHas() throws Exception {
|
||||
void setAndHas() {
|
||||
assertThat(this.attributeAccessor.hasAttribute(NAME)).isFalse();
|
||||
this.attributeAccessor.setAttribute(NAME, VALUE);
|
||||
assertThat(this.attributeAccessor.hasAttribute(NAME)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void remove() throws Exception {
|
||||
void computeAttribute() {
|
||||
AtomicInteger atomicInteger = new AtomicInteger();
|
||||
Function<String, String> computeFunction = name -> "computed-" + atomicInteger.incrementAndGet();
|
||||
|
||||
assertThat(this.attributeAccessor.hasAttribute(NAME)).isFalse();
|
||||
this.attributeAccessor.computeAttribute(NAME, computeFunction);
|
||||
assertThat(this.attributeAccessor.getAttribute(NAME)).isEqualTo("computed-1");
|
||||
this.attributeAccessor.computeAttribute(NAME, computeFunction);
|
||||
assertThat(this.attributeAccessor.getAttribute(NAME)).isEqualTo("computed-1");
|
||||
|
||||
this.attributeAccessor.removeAttribute(NAME);
|
||||
assertThat(this.attributeAccessor.hasAttribute(NAME)).isFalse();
|
||||
this.attributeAccessor.computeAttribute(NAME, computeFunction);
|
||||
assertThat(this.attributeAccessor.getAttribute(NAME)).isEqualTo("computed-2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void remove() {
|
||||
assertThat(this.attributeAccessor.hasAttribute(NAME)).isFalse();
|
||||
this.attributeAccessor.setAttribute(NAME, VALUE);
|
||||
assertThat(this.attributeAccessor.removeAttribute(NAME)).isEqualTo(VALUE);
|
||||
@@ -57,13 +79,13 @@ class AttributeAccessorSupportTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void attributeNames() throws Exception {
|
||||
void attributeNames() {
|
||||
this.attributeAccessor.setAttribute(NAME, VALUE);
|
||||
this.attributeAccessor.setAttribute("abc", "123");
|
||||
String[] attributeNames = this.attributeAccessor.attributeNames();
|
||||
Arrays.sort(attributeNames);
|
||||
assertThat(Arrays.binarySearch(attributeNames, NAME) > -1).isTrue();
|
||||
assertThat(Arrays.binarySearch(attributeNames, "abc") > -1).isTrue();
|
||||
assertThat(Arrays.binarySearch(attributeNames, "abc")).isEqualTo(0);
|
||||
assertThat(Arrays.binarySearch(attributeNames, NAME)).isEqualTo(1);
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 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.
|
||||
@@ -19,6 +19,7 @@ package org.springframework.test.context.support;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
@@ -200,6 +201,17 @@ public class DefaultTestContext implements TestContext {
|
||||
return this.attributes.get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T computeAttribute(String name, Function<String, T> computeFunction) {
|
||||
Assert.notNull(name, "Name must not be null");
|
||||
Assert.notNull(computeFunction, "Compute function must not be null");
|
||||
Object value = this.attributes.computeIfAbsent(name, computeFunction);
|
||||
Assert.state(value != null,
|
||||
() -> String.format("Compute function must not return null for attribute named '%s'", name));
|
||||
return (T) value;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Object removeAttribute(String name) {
|
||||
|
||||
Reference in New Issue
Block a user