TAP5-2743: PerThreadValue<T> convenience methods

diff --git a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/PerThreadValue.java b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/PerThreadValue.java
index bce9097..5b23069 100644
--- a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/PerThreadValue.java
+++ b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/PerThreadValue.java
@@ -14,6 +14,11 @@
 
 package org.apache.tapestry5.ioc.services;
 
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
 /**
  * Provides access to per-thread (and, by extension, per-request) data, managed by the {@link PerthreadManager}.
  * A PerThreadValue stores a particular type of information.
@@ -43,4 +48,83 @@
      * Sets the current per-thread value, then returns that value.
      */
     T set(T newValue);
+
+    /**
+     * If no value is currently stored (checked by {@link #exists()}), the value
+     * provided by the supplier function is set and return.
+     * Otherwise, the current value is returned.
+     *
+     * @param fn the value supplier function
+     * @return The current (existing or computed) value
+     * @throws NullPointerException if the supplier function is null
+     * @since 5.8.3
+     */
+    default T computeIfAbsent(Supplier<? extends T> fn) {
+        Objects.requireNonNull(fn);
+        if (exists()) {
+            return get();
+        }
+
+        T newValue = fn.get();
+        set(newValue);
+
+        return newValue;
+    }
+
+    /**
+     * If a value is currently stored (checked by {@link #exists()}), this value
+     * is used to compute a new one with the given mapping function.
+     * Otherwise, null is returned.
+     *
+     * @param fn the mapping function to compute the new value
+     * @return The new computed value, or null if none was present
+     * @throws NullPointerException if the mapping function is null
+     * @since 5.8.3
+     */
+    default T computeIfPresent(Function<? super T, ? extends T> fn) {
+        Objects.requireNonNull(fn);
+        if (!exists()) {
+            return null;
+        }
+        
+        T newValue = fn.apply(get());
+        set(newValue);
+
+        return newValue;
+    }
+
+    /**
+     * Computes a new value with the help of the current one, which is returned.
+     *
+     * @param fn the mapping function to compute the new value
+     * @return The new computed value
+     * @throws NullPointerException if the mapping function is null
+     * @since 5.8.3
+     */
+    default T compute(Function<? super T, ? extends T> fn) {
+        Objects.requireNonNull(fn);
+
+        T newValue = fn.apply(get());
+        set(newValue);
+
+        return newValue;
+    }
+
+    /**
+     * If a value is set, performs the given action with it, otherwise it
+     * does nothing.
+     *
+     * @param action performed action if a value is set
+     * @throws NullPointerException if the action is null
+     * @since 5.8.3
+     */
+    default void ifSet(Consumer<? super T> action) {
+        Objects.requireNonNull(action);
+
+        if (!exists()) {
+            return;
+        }
+
+        action.accept(get());
+    }
 }
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/PerThreadValueSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/PerThreadValueSpec.groovy
new file mode 100644
index 0000000..3687b74
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/PerThreadValueSpec.groovy
@@ -0,0 +1,188 @@
+package ioc.specs
+
+import java.util.function.Supplier
+
+import org.apache.tapestry5.ioc.internal.services.PerthreadManagerImpl
+import org.apache.tapestry5.ioc.services.PerThreadValue
+import org.slf4j.Logger
+
+import spock.lang.Specification
+
+class PerThreadValueSpec extends Specification {
+
+  def manager
+
+  def setup(){
+    Logger logger = Mock()
+    manager = new PerthreadManagerImpl(logger)
+  }
+
+  def "computeIfAbsent - no value"() {
+    given:
+    def newValue = "a computed value"
+    PerThreadValue<String> perThreadValue = manager.createValue();
+
+    when:
+    perThreadValue.computeIfAbsent({ newValue })
+    
+
+    then:
+    perThreadValue.exists() == true
+    perThreadValue.get() == newValue
+  }
+
+  def "computeIfAbsent - pre-existing value"() {
+    given:
+    def currentValue = "initial value"
+    def newValue = "a computed value"
+    PerThreadValue<String> perThreadValue = manager.createValue();
+    perThreadValue.set(currentValue)
+
+    when:
+    perThreadValue.computeIfAbsent({ newValue })
+
+    then:
+    perThreadValue.exists() == true
+    perThreadValue.get() == currentValue
+    perThreadValue.get() != newValue
+  }
+
+  def "computeIfAbsent - supplier null"() {
+    given:
+    PerThreadValue<String> perThreadValue = manager.createValue();
+
+    when:
+    perThreadValue.computeIfAbsent(null)
+
+    then:
+    thrown NullPointerException
+  }
+
+  def "computeIfPresent - no value"() {
+    given:
+    def currentValue = "initial value"
+    def newValue = "a computed value"
+    PerThreadValue<String> perThreadValue = manager.createValue();
+
+    when:
+    perThreadValue.computeIfPresent({ current -> newValue })
+
+    then:
+    perThreadValue.exists() == false
+    perThreadValue.get() == null
+  }
+
+  def "computeIfPresent - pre-existing value"() {
+    given:
+    def currentValue = "initial value"
+    def newValue = "a computed value"
+    PerThreadValue<String> perThreadValue = manager.createValue();
+    perThreadValue.set(currentValue)
+
+    when:
+    perThreadValue.computeIfPresent({ current -> newValue })
+
+    then:
+    perThreadValue.get() != currentValue
+    perThreadValue.get() == newValue
+  }
+
+  def "computeIfPresent- mapper null"() {
+    given:
+    PerThreadValue<String> perThreadValue = manager.createValue();
+
+    when:
+    perThreadValue.computeIfPresent(null)
+
+    then:
+    thrown NullPointerException
+  }
+
+  def "compute - no value"() {
+    given:
+    def newValue = "a computed value"
+    PerThreadValue<String> perThreadValue = manager.createValue();
+
+    when:
+    perThreadValue.compute({ current -> newValue })
+
+    then:
+    perThreadValue.exists() == true
+    perThreadValue.get() == newValue
+  }
+
+  def "compute - pre-existing value"() {
+    given:
+    def currentValue = "initial value"
+    def newValue = "a computed value"
+    PerThreadValue<String> perThreadValue = manager.createValue();
+    perThreadValue.set(currentValue)
+
+    when:
+    perThreadValue.compute({ current -> newValue })
+
+    then:
+    perThreadValue.get() != currentValue
+    perThreadValue.get() == newValue
+  }
+
+  def "compute - mapper null"() {
+    given:
+    PerThreadValue<String> perThreadValue = manager.createValue();
+
+    when:
+    perThreadValue.compute(null)
+  
+    then:
+    thrown NullPointerException
+  }
+
+  def "ifSet - value is set"() {
+    given:
+    PerThreadValue<String> perThreadValue = manager.createValue();
+    perThreadValue.set("a value");
+    def hasRun = false
+
+    when:
+    perThreadValue.ifSet { hasRun = true }
+
+    then:
+    hasRun == true
+  }
+
+  def "ifSet - no value set"() {
+    given:
+    PerThreadValue<String> perThreadValue = manager.createValue();
+    def hasRun = false
+
+    when:
+    perThreadValue.ifSet { hasRun = true }
+
+    then:
+    hasRun == false
+  }
+
+  def "ifSet - action is null with no value set"() {
+    given:
+    PerThreadValue<String> perThreadValue = manager.createValue();
+
+    when:
+    perThreadValue.ifSet(null)
+
+    then:
+    thrown NullPointerException
+  }
+
+  def "ifSet - action is null with value set"() {
+    given:
+    PerThreadValue<String> perThreadValue = manager.createValue();
+    perThreadValue.set("a value")
+
+    when:
+    perThreadValue.ifSet(null)
+
+    then:
+    thrown NullPointerException
+  }
+
+}
\ No newline at end of file
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/PerthreadManagerImplSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/PerthreadManagerImplSpec.groovy
index b349a7f..a49f535 100644
--- a/tapestry-ioc/src/test/groovy/ioc/specs/PerthreadManagerImplSpec.groovy
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/PerthreadManagerImplSpec.groovy
@@ -178,6 +178,4 @@
     !value.exists()
 
   }
-
-
 }