Add yangtools.concepts.CheckedValue 65/70265/13
authorRobert Varga <robert.varga@pantheon.tech>
Tue, 3 Apr 2018 07:42:21 +0000 (09:42 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Mon, 16 Apr 2018 23:41:30 +0000 (01:41 +0200)
There are quite a few places where we would like to conditionally
create a value or report an error. These currently rely on thrown
exceptions, which has a performance penalty.

Introduce CheckedValue, which acts as a combined holder object
for either a value or an Exception. These objects can act as
intermediaries between callers and callees.

The underlying concept here is that of a variant value, which contains
either a value of type A or a value of type B. This patch adds
the concept as a utility Variant<T, U>.

Change-Id: Ibc6f562e22805b38402436cd9c0368dfcbb775fa
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
common/concepts/pom.xml
common/concepts/src/main/java/org/opendaylight/yangtools/concepts/CheckedValue.java [new file with mode: 0644]
common/concepts/src/main/java/org/opendaylight/yangtools/concepts/Variant.java [new file with mode: 0644]
common/concepts/src/test/java/org/opendaylight/yangtools/concepts/CheckedValueTest.java [new file with mode: 0644]

index 20921457c5776d3e4b52dff86072fc3ed91f1c56..cf8a2903bac316d2a0fc673b7793dc2b9185ff05 100644 (file)
     <name>${project.artifactId}</name>
     <description>Common concepts</description>
 
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.opendaylight.yangtools</groupId>
+                <artifactId>yangtools-artifacts</artifactId>
+                <version>${project.version}</version>
+                <scope>import</scope>
+                <type>pom</type>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
     <dependencies>
         <dependency>
             <groupId>com.google.guava</groupId>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.opendaylight.yangtools</groupId>
+            <artifactId>mockito-configuration</artifactId>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/common/concepts/src/main/java/org/opendaylight/yangtools/concepts/CheckedValue.java b/common/concepts/src/main/java/org/opendaylight/yangtools/concepts/CheckedValue.java
new file mode 100644 (file)
index 0000000..413ef74
--- /dev/null
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2018 Pantheon Technologies, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.concepts;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.Beta;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import javax.annotation.concurrent.ThreadSafe;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Utility holder similar to {@link java.util.Optional}, except the empty case contains an Exception, which should be
+ * reported, for example via throwing it. It provides analogous methods such as {@link #isPresent()},
+ * {@link #ifPresent(Consumer)}, {@link #get()}, {@link #orElse(Object)}, {@link #orElseGet(Supplier)},
+ * {@link #orElseThrow(Function)}.
+ *
+ * @param <T> Value type
+ * @param <E> Exception type
+ * @author Robert Varga
+ */
+@Beta
+@NonNullByDefault
+@ThreadSafe
+public final class CheckedValue<T, E extends Exception> extends Variant<T, E> {
+    private CheckedValue(final T value) {
+        super(value);
+    }
+
+    private CheckedValue(final E violation, final @Nullable Void dummy) {
+        super(violation, dummy);
+    }
+
+    /**
+     * Create a new instance containing an {@link Exception}.
+     *
+     * @param cause Throwable
+     * @return A new instance
+     * @throws NullPointerException if {@code cause} is null
+     */
+    public static <T, E extends Exception> CheckedValue<T, E> ofException(final E cause) {
+        return new CheckedValue<>(cause, null);
+    }
+
+    /**
+     * Create a new instance containing specified value.
+     *
+     * @param value Value
+     * @return A new instance
+     * @throws NullPointerException if {@code value} is null
+     */
+    public static <T, E extends Exception> CheckedValue<T, E> ofValue(final T value) {
+        return new CheckedValue<>(value);
+    }
+
+    /**
+     * Convert a Variant into a {@link CheckedValue}, converting the second value into an exception.
+     *
+     * @param variant Input variant
+     * @return Resulting {@link CheckedValue}
+     */
+    public static <T, U, E extends Exception> CheckedValue<T, E> ofVariant(final Variant<T, U> variant,
+            final Function<U, E> mapper) {
+        requireNonNull(mapper);
+        return variant.isFirst() ? new CheckedValue(variant.first())
+                : new CheckedValue(mapper.apply(variant.second()), null);
+    }
+
+    /**
+     * Return the contained value if {@link #isPresent()} would return true, throws {@link IllegalStateException}
+     * otherwise.
+     *
+     * @return Contained value
+     * @throws IllegalStateException if an error string is present.
+     */
+    public T get() {
+        if (isFirst()) {
+            return first();
+        }
+        throw new IllegalStateException("Value is not present", second());
+    }
+
+    /**
+     * Return the contained error string if {@link #isPresent()} would return false, throws
+     * {@link IllegalStateException} otherwise.
+     *
+     * @return Throwable which was used to instantiate this object, or absent if it was instantiated using an error
+     *         string.
+     * @throws IllegalStateException if a value is present.
+     */
+    public E getException() {
+        if (isSecond()) {
+            return second();
+        }
+        throw new IllegalStateException("Value " + first() + " is present");
+    }
+
+    /**
+     * Return true if a value is present.
+     *
+     * @return True if a value is present.
+     */
+    public boolean isPresent() {
+        return isFirst();
+    }
+
+    /**
+     * If a value is present, invoke the specified consumer with the value, otherwise do nothing.
+     *
+     * @param consumer block to be executed if a value is present
+     * @throws NullPointerException if value is present and {@code consumer} is null
+     */
+    public void ifPresent(final Consumer<? super T> consumer) {
+        if (isFirst()) {
+            consumer.accept(first());
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public <U> CheckedValue<U, E> map(final Function<? super T, U> mapper) {
+        requireNonNull(mapper);
+        return isFirst() ? new CheckedValue<>(mapper.apply(first())) : (CheckedValue<U, E>) this;
+    }
+
+    @SuppressWarnings("unchecked")
+    public <X extends Exception> CheckedValue<T, X> mapException(final Function<? super E, X> mapper) {
+        requireNonNull(mapper);
+        if (isFirst()) {
+            return (CheckedValue<T, X>) this;
+        }
+        return new CheckedValue<>(mapper.apply(second()), null);
+    }
+
+
+    @SuppressWarnings("unchecked")
+    public <U> CheckedValue<U, E> flatMap(final Function<? super T, CheckedValue<U, E>> mapper) {
+        requireNonNull(mapper);
+        return isFirst() ? requireNonNull(mapper.apply(first())) : (CheckedValue<U, E>) this;
+    }
+
+    /**
+     * Return contained value if present, otherwise return supplied value.
+     *
+     * @param other Replacement value
+     * @return Contained value or {code other}
+     */
+    public T orElse(final T other) {
+        return isFirst() ? first() : other;
+    }
+
+    /**
+     * Return contained value if present, otherwise return the value produced by a supplier.
+     *
+     * @param supplier Replacement value supplier
+     * @return Contained value or supplier's value
+     * @throws NullPointerException if {@code supplier} is null
+     */
+    public T orElseGet(final Supplier<T> supplier) {
+        requireNonNull(supplier);
+        return isFirst() ? first() : supplier.get();
+    }
+
+    public <X extends Throwable> T orElseThrow() throws E {
+        if (isFirst()) {
+            return first();
+        }
+        throw second();
+    }
+
+    public <X extends Throwable> T orElseThrow(final Function<E, X> exceptionMapper) throws X {
+        requireNonNull(exceptionMapper);
+        if (isFirst()) {
+            return first();
+        }
+        throw exceptionMapper.apply(second());
+    }
+
+    public <X extends Throwable> T orElseThrow(final Supplier<X> supplier) throws X {
+        requireNonNull(supplier);
+        if (isFirst()) {
+            return first();
+        }
+        throw supplier.get();
+    }
+}
diff --git a/common/concepts/src/main/java/org/opendaylight/yangtools/concepts/Variant.java b/common/concepts/src/main/java/org/opendaylight/yangtools/concepts/Variant.java
new file mode 100644 (file)
index 0000000..1d01956
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+ * Copyright (c) 2018 Pantheon Technologies, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.concepts;
+
+import static com.google.common.base.Verify.verifyNotNull;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.MoreObjects.ToStringHelper;
+import java.util.Objects;
+import java.util.Optional;
+import javax.annotation.concurrent.ThreadSafe;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Utility holder of a two-variant value. The class design treats both variants as equal.
+ *
+ * @param <T> First alternative type
+ * @param <U> Second alternative type
+ * @author Robert Varga
+ */
+@Beta
+@NonNullByDefault
+@ThreadSafe
+public class Variant<T, U> {
+    private final @Nullable T first;
+    private final @Nullable U second;
+
+    Variant(final T first) {
+        this.first = requireNonNull(first);
+        second = null;
+    }
+
+    Variant(final U second, final @Nullable Void dummy) {
+        first = null;
+        this.second = requireNonNull(second);
+    }
+
+    final T first() {
+        return verifyNotNull(first);
+    }
+
+    final U second() {
+        return verifyNotNull(second);
+    }
+
+    /**
+     * Create a new instance containing specified value.
+     *
+     * @param value Value
+     * @return A new instance
+     * @throws NullPointerException if {@code value} is null
+     */
+    public static <T, U> Variant<T, U> ofFirst(final T value) {
+        return new Variant<>(value);
+    }
+
+    /**
+     * Create a new instance containing specified value.
+     *
+     * @param value Value
+     * @return A new instance
+     * @throws NullPointerException if {@code value} is null
+     */
+    public static <T, U> Variant<T, U> ofSecond(final U value) {
+        return new Variant<>(value, null);
+    }
+
+    public final boolean isFirst() {
+        return first != null;
+    }
+
+    public final T getFirst() {
+        return tryFirst().get();
+    }
+
+    public final Optional<T> tryFirst() {
+        return Optional.ofNullable(first);
+    }
+
+    public final boolean isSecond() {
+        return second != null;
+    }
+
+    public final U getSecond() {
+        return trySecond().get();
+    }
+
+    public final Optional<U> trySecond() {
+        return Optional.ofNullable(second);
+    }
+
+    @Override
+    public final int hashCode() {
+        return Objects.hash(first, second);
+    }
+
+    @Override
+    public final boolean equals(final @Nullable Object obj) {
+        if (obj == this) {
+            return true;
+        }
+        if (obj == null || !getClass().equals(obj.getClass())) {
+            return false;
+        }
+        final Variant<?, ?> other = (Variant<?, ?>) obj;
+        return Objects.equals(first, other.first) && Objects.equals(second, other.second);
+    }
+
+    @Override
+    public final String toString() {
+        return addToString(MoreObjects.toStringHelper(this).omitNullValues()).toString();
+    }
+
+    protected ToStringHelper addToString(final ToStringHelper helper) {
+        return helper.add("first", first).add("second", second);
+    }
+}
diff --git a/common/concepts/src/test/java/org/opendaylight/yangtools/concepts/CheckedValueTest.java b/common/concepts/src/test/java/org/opendaylight/yangtools/concepts/CheckedValueTest.java
new file mode 100644 (file)
index 0000000..382c8cb
--- /dev/null
@@ -0,0 +1,165 @@
+/*
+ * Copyright (c) 2018 Pantheon Technologies, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.concepts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import org.junit.Test;
+
+public class CheckedValueTest {
+    @Test(expected = NullPointerException.class)
+    public void testNullValue() {
+        CheckedValue.ofValue(null);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testExceptionGet() {
+        CheckedValue.ofException(new Exception()).get();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testValueException() {
+        CheckedValue.ofValue("foo").getException();
+    }
+
+    @Test
+    public void testGet() {
+        final String value = "foo";
+        final CheckedValue<String, ?> val = CheckedValue.ofValue(value);
+        assertTrue(val.isPresent());
+        assertSame(value, val.get());
+    }
+
+    @Test
+    public void testThrowableGetThrowable() {
+        final Exception cause = new Exception();
+        final CheckedValue<?, ?> val = CheckedValue.ofException(cause);
+        assertFalse(val.isPresent());
+        assertSame(cause, val.getException());
+    }
+
+    @Test
+    public void testToString() {
+        assertEquals("CheckedValue{first=foo}", CheckedValue.ofValue("foo").toString());
+        assertEquals("CheckedValue{second=java.lang.NullPointerException: foo}",
+            CheckedValue.ofException(new NullPointerException("foo")).toString());
+    }
+
+    @Test
+    public void testEqualsHashCode() {
+        final CheckedValue<String, ?> fooVal = CheckedValue.ofValue("foo");
+        final CheckedValue<String, ?> fooVal2 = CheckedValue.ofValue("foo");
+        final CheckedValue<Integer, ?> oneVal = CheckedValue.ofValue(1);
+        final CheckedValue<?, ?> errBar = CheckedValue.ofException(new NullPointerException("bar"));
+        final CheckedValue<?, ?> errFoo = CheckedValue.ofException(new NullPointerException("foo"));
+        final CheckedValue<?, ?> errFoo2 = CheckedValue.ofException(new NullPointerException("foo"));
+
+        assertFalse(fooVal.equals(null));
+        assertFalse(fooVal.equals("foo"));
+        assertTrue(fooVal.equals(fooVal));
+        assertTrue(fooVal.equals(fooVal2));
+        assertFalse(fooVal.equals(oneVal));
+        assertEquals(fooVal.hashCode(), fooVal2.hashCode());
+
+        assertFalse(errFoo.equals(null));
+        assertFalse(errFoo.equals("foo"));
+        assertTrue(errFoo.equals(errFoo));
+
+        assertFalse(errBar.equals(errFoo));
+    }
+
+    @Test
+    public void testIfPresent() {
+        final String foo = "foo";
+        final Consumer<Object> consumer = mock(Consumer.class);
+        doNothing().when(consumer).accept(any(Object.class));
+        CheckedValue.ofValue(foo).ifPresent(consumer);
+        verify(consumer).accept(foo);
+    }
+
+    @Test
+    public void testThrowableIfPresent() {
+        final Consumer<Object> consumer = mock(Consumer.class);
+        doNothing().when(consumer).accept(any(Object.class));
+        CheckedValue.ofException(new NullPointerException()).ifPresent(consumer);
+        verifyZeroInteractions(consumer);
+    }
+
+    @Test
+    public void testOrElse() {
+        final String foo = "foo";
+        final String bar = "bar";
+        assertSame(foo, CheckedValue.ofValue(foo).orElse(bar));
+        assertSame(bar, CheckedValue.ofException(new NullPointerException()).orElse(bar));
+    }
+
+    @Test
+    public void testMap() {
+        final String foo = "foo";
+        final String bar = "bar";
+        final CheckedValue<Object, ?> errVal = CheckedValue.ofValue(foo);
+        final Function<Object, Object> mapper = mock(Function.class);
+        doReturn(bar).when(mapper).apply(any(Object.class));
+        assertSame(bar, errVal.map(mapper).get());
+        verify(mapper).apply(foo);
+    }
+
+    @Test
+    public void testExceptionMap() {
+        final CheckedValue<Object, ?> errVal = CheckedValue.ofException(new NullPointerException());
+        final Function<Object, Object> mapper = mock(Function.class);
+        doReturn(null).when(mapper).apply(any(Object.class));
+        assertSame(errVal, errVal.map(mapper));
+        verifyZeroInteractions(mapper);
+    }
+
+    @Test
+    public void testOrElseThrow() {
+        final String foo = "foo";
+        assertSame(foo, CheckedValue.ofValue(foo)
+            .orElseThrow((Supplier<NullPointerException>)NullPointerException::new));
+    }
+
+    @Test(expected = InterruptedException.class)
+    public void testThrowableOrElseThrow() throws InterruptedException {
+        final String foo = "foo";
+        final Exception cause = new NullPointerException(foo);
+        CheckedValue.ofException(cause).orElseThrow((Supplier<InterruptedException>)InterruptedException::new);
+    }
+
+    @Test
+    public void testOrElseGet() {
+        final String foo = "foo";
+        final Supplier<String> supplier = mock(Supplier.class);
+        doReturn(null).when(supplier).get();
+        assertSame(foo, CheckedValue.ofValue(foo).orElseGet(supplier));
+        verifyZeroInteractions(supplier);
+    }
+
+    @Test
+    public void testExceptionOrElseGet() {
+        final String bar = "bar";
+        final Supplier<Object> supplier = mock(Supplier.class);
+        doReturn(bar).when(supplier).get();
+
+        assertSame(bar, CheckedValue.ofException(new NullPointerException()).orElseGet(supplier));
+        verify(supplier).get();
+    }
+}