Bug 1392: New yang-common/util classes 40/9240/5
authorThomas Pantelis <tpanteli@brocade.com>
Mon, 14 Jul 2014 16:13:08 +0000 (12:13 -0400)
committerRobert Varga <rovarga@cisco.com>
Mon, 28 Jul 2014 06:15:12 +0000 (08:15 +0200)
Needed for https://git.opendaylight.org/gerrit/#/c/9178/

OperationFailedException:

   Generalized exception class that contains an RpcError list.

MappingCheckedFuture:

   Futures#addCallback and Futures#transform call Future#get but
   CheckedFuture doesn't override the get methods to translate exceptions
   to the checked type. Thus you don't get the checked exception when using
   those Futures methods. This kind of defeats the purpose of CheckedFuture.

   The new MappingCheckedFuture class overrides the get methods to translate exceptions.

ExceptionMapper:

   Generalized class for use with CheckedFuture to translate exceptions to a
   specified exception type.

Change-Id: Idb4c7e8b5c2cf17795de2f3af4278989ac7bd116
Signed-off-by: tpantelis <tpanteli@brocade.com>
Signed-off-by: Robert Varga <rovarga@cisco.com>
common/util/src/main/java/org/opendaylight/yangtools/util/concurrent/ExceptionMapper.java [new file with mode: 0644]
common/util/src/main/java/org/opendaylight/yangtools/util/concurrent/MappingCheckedFuture.java [new file with mode: 0644]
common/util/src/test/java/org/opendaylight/yangtools/util/concurrent/MappingCheckedFutureTest.java [new file with mode: 0644]
yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/OperationFailedException.java [new file with mode: 0644]

diff --git a/common/util/src/main/java/org/opendaylight/yangtools/util/concurrent/ExceptionMapper.java b/common/util/src/main/java/org/opendaylight/yangtools/util/concurrent/ExceptionMapper.java
new file mode 100644 (file)
index 0000000..af51032
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2014 Brocade Communications Systems, Inc. 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.util.concurrent;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Utility exception mapper which translates an Exception to a specified type of Exception.
+ *
+ * This mapper is intended to be used with {@link com.google.common.util.concurrent.Futures#makeChecked(com.google.common.util.concurrent.ListenableFuture, Function)}
+ * <ul>
+ * <li>if exception is the specified type or one of its subclasses, it returns original exception.
+ * <li>if exception is {@link ExecutionException} and the cause is of the specified type, it returns the cause
+ * <li>otherwise returns an instance of the specified exception type with original exception as the cause.
+ * </ul>
+ *
+ * @author Thomas Pantelis
+ *
+ * @param <X> the exception type
+ */
+public abstract class ExceptionMapper<X extends Exception> implements Function<Exception, X> {
+    private final Class<X> exceptionType;
+    private final String opName;
+
+    /**
+     * Constructor.
+     *
+     * @param opName the String prefix for exception messages.
+     * @param exceptionType the exception type to which to translate.
+     */
+    public ExceptionMapper(final String opName, final Class<X> exceptionType) {
+        this.exceptionType = Preconditions.checkNotNull(exceptionType);
+        this.opName = Preconditions.checkNotNull(opName);
+    }
+
+    /**
+     * Invoked to create a new exception instance of the specified type.
+     *
+     * @param message the message for the new exception.
+     * @param cause the cause for the new exception.
+     *
+     * @return an instance of the exception type.
+     */
+    protected abstract X newWithCause(String message, Throwable cause);
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public X apply(final Exception e) {
+
+        // If exception is of the specified type,return it.
+        if (exceptionType.isAssignableFrom( e.getClass())) {
+            return (X) e;
+        }
+
+        // If exception is ExecutionException whose cause is of the specified
+        // type, return the cause.
+        if (e instanceof ExecutionException && e.getCause() != null) {
+            if (exceptionType.isAssignableFrom( e.getCause().getClass())) {
+                return (X) e.getCause();
+            } else {
+                return newWithCause(opName + " execution failed", e.getCause());
+            }
+        }
+
+        // Otherwise return an instance of the specified type with the original
+        // cause.
+
+        if (e instanceof InterruptedException) {
+            return newWithCause( opName + " was interupted.", e);
+        }
+
+        if (e instanceof CancellationException ) {
+            return newWithCause( opName + " was cancelled.", e);
+        }
+
+        // We really shouldn't get here but need to cover it anyway for completeness.
+        return newWithCause(opName + " encountered an unexpected failure", e);
+    }
+}
diff --git a/common/util/src/main/java/org/opendaylight/yangtools/util/concurrent/MappingCheckedFuture.java b/common/util/src/main/java/org/opendaylight/yangtools/util/concurrent/MappingCheckedFuture.java
new file mode 100644 (file)
index 0000000..48f69e2
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2014 Brocade Communications Systems, Inc. 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.util.concurrent;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.util.concurrent.AbstractCheckedFuture;
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * An implementation of CheckedFuture that provides similar behavior for the <code>get</code> methods
+ * that the <code>checkedGet</code> methods provide.
+ * <p>
+ * For {@link CancellationException} and {@link InterruptedException}, the specified exception mapper
+ * is invoked to translate them to the checked exception type.
+ * <p>
+ * For {@link ExecutionException}, the mapper is invoked to translate the cause to the checked exception
+ * and a new ExecutionException is thrown with the translated cause.
+ *
+ * @author Thomas Pantelis
+ *
+ * @param <V> The result type returned by this Future's get method
+ * @param <X> The checked exception type
+ */
+public class MappingCheckedFuture<V, X extends Exception> extends AbstractCheckedFuture<V, X> {
+
+    private final Function<Exception, X> mapper;
+
+    private MappingCheckedFuture( ListenableFuture<V> delegate, Function<Exception, X> mapper ) {
+        super( delegate );
+        this.mapper = Preconditions.checkNotNull( mapper );
+    }
+
+    /**
+     * Creates a new <code>MappingCheckedFuture</code> that wraps the given {@link ListenableFuture}
+     * delegate.
+     *
+     * @param delegate the {@link ListenableFuture} to wrap
+     * @param mapper the mapping {@link Function} used to translate exceptions from the delegate
+     * @return a new <code>MappingCheckedFuture</code>
+     */
+    public static <V, X extends Exception> MappingCheckedFuture<V, X> create(
+            ListenableFuture<V> delegate, Function<Exception, X> mapper ) {
+        return new MappingCheckedFuture<V, X>( delegate, mapper );
+    }
+
+    @Override
+    protected X mapException( Exception e ) {
+        return mapper.apply( e );
+    }
+
+    private ExecutionException wrapInExecutionException( String message, final Exception e ) {
+        return new ExecutionException( message, mapException( e ) );
+    }
+
+    @Override
+    public V get() throws InterruptedException, ExecutionException {
+        try {
+            return super.get();
+        } catch( InterruptedException e ) {
+            Thread.currentThread().interrupt();
+            throw wrapInExecutionException( "Operation was interrupted", e );
+        } catch( CancellationException e ) {
+            throw wrapInExecutionException( "Operation was cancelled", e );
+        } catch( ExecutionException e ) {
+            throw wrapInExecutionException( e.getMessage(), e );
+        }
+    }
+
+    @Override
+    public V get( long timeout, TimeUnit unit )
+            throws InterruptedException, ExecutionException, TimeoutException {
+        try {
+            return super.get( timeout, unit );
+        } catch( InterruptedException e ) {
+            Thread.currentThread().interrupt();
+            throw wrapInExecutionException( "Operation was interrupted", e );
+        } catch( CancellationException e ) {
+            throw wrapInExecutionException( "Operation was cancelled", e );
+        } catch( ExecutionException e ) {
+            throw wrapInExecutionException( e.getMessage(), e );
+        }
+    }
+}
diff --git a/common/util/src/test/java/org/opendaylight/yangtools/util/concurrent/MappingCheckedFutureTest.java b/common/util/src/test/java/org/opendaylight/yangtools/util/concurrent/MappingCheckedFutureTest.java
new file mode 100644 (file)
index 0000000..45b9e3c
--- /dev/null
@@ -0,0 +1,250 @@
+/*
+ * Copyright (c) 2014 Brocade Communications Systems, Inc. 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.util.concurrent;
+
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.Test;
+import com.google.common.util.concurrent.CheckedFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+/**
+ * Unit tests for MappingCheckedFuture.
+ *
+ * @author Thomas Pantelis
+ */
+public class MappingCheckedFutureTest {
+
+    interface FutureInvoker {
+        void invokeGet( CheckedFuture<?,?> future ) throws Exception;
+        Throwable extractWrappedTestEx( Exception from );
+    }
+
+    @SuppressWarnings("serial")
+    static class TestException extends Exception {
+        TestException( String message, Throwable cause ) {
+            super( message, cause );
+        }
+    }
+
+    static final ExceptionMapper<TestException> MAPPER =  new ExceptionMapper<TestException>(
+                                                                      "Test", TestException.class ) {
+
+        @Override
+        protected TestException newWithCause( String message, Throwable cause ) {
+            return new TestException( message, cause );
+        }
+    };
+
+    static final FutureInvoker GET = new FutureInvoker() {
+        @Override
+        public void invokeGet( CheckedFuture<?,?> future ) throws Exception {
+            future.get();
+        }
+
+        @Override
+        public Throwable extractWrappedTestEx( Exception from ) {
+            if( from instanceof ExecutionException ) {
+                return ((ExecutionException)from).getCause();
+            }
+
+            return from;
+        }
+    };
+
+    static final FutureInvoker TIMED_GET = new FutureInvoker() {
+        @Override
+        public void invokeGet( CheckedFuture<?,?> future ) throws Exception {
+            future.get( 1, TimeUnit.HOURS );
+        }
+
+        @Override
+        public Throwable extractWrappedTestEx( Exception from ) {
+            if( from instanceof ExecutionException ) {
+                return ((ExecutionException)from).getCause();
+            }
+
+            return from;
+        }
+    };
+
+    static final FutureInvoker CHECKED_GET = new FutureInvoker() {
+        @Override
+        public void invokeGet( CheckedFuture<?,?> future ) throws Exception {
+            future.checkedGet();
+        }
+
+        @Override
+        public Throwable extractWrappedTestEx( Exception from ) {
+            return from;
+        }
+    };
+
+    static final FutureInvoker TIMED_CHECKED_GET = new FutureInvoker() {
+        @Override
+        public void invokeGet( CheckedFuture<?,?> future ) throws Exception {
+            future.checkedGet( 50, TimeUnit.MILLISECONDS );
+        }
+
+        @Override
+        public Throwable extractWrappedTestEx( Exception from ) {
+            return from;
+        }
+    };
+
+    @Test
+    public void testGet() throws Exception {
+
+        SettableFuture<String> delegate = SettableFuture.create();
+        MappingCheckedFuture<String,TestException> future = MappingCheckedFuture.create( delegate, MAPPER );
+        delegate.set( "test" );
+        assertEquals( "get", "test", future.get() );
+    }
+
+    @Test
+    public void testGetWithExceptions() throws Exception {
+
+        testExecutionException( GET, new RuntimeException() );
+        testExecutionException( GET, new TestException( "mock", null ) );
+        testCancellationException( GET );
+        testInterruptedException( GET );
+    }
+
+    @Test
+    public void testTimedGet() throws Exception {
+
+        SettableFuture<String> delegate = SettableFuture.create();
+        MappingCheckedFuture<String,TestException> future = MappingCheckedFuture.create( delegate, MAPPER );
+        delegate.set( "test" );
+        assertEquals( "get", "test", future.get( 50, TimeUnit.MILLISECONDS ) );
+    }
+
+    @Test
+    public void testTimedGetWithExceptions() throws Exception {
+
+        testExecutionException( TIMED_GET, new RuntimeException() );
+        testCancellationException( TIMED_GET );
+        testInterruptedException( TIMED_GET );
+    }
+
+    @Test
+    public void testCheckedGetWithExceptions() throws Exception {
+
+        testExecutionException( CHECKED_GET, new RuntimeException() );
+        testCancellationException( CHECKED_GET );
+        testInterruptedException( CHECKED_GET );
+    }
+
+    @Test
+    public void testTimedCheckedWithExceptions() throws Exception {
+
+        testExecutionException( TIMED_CHECKED_GET, new RuntimeException() );
+        testCancellationException( TIMED_CHECKED_GET );
+        testInterruptedException( TIMED_CHECKED_GET );
+    }
+
+    private void testExecutionException( FutureInvoker invoker, Throwable cause ) {
+
+        SettableFuture<String> delegate = SettableFuture.create();
+        MappingCheckedFuture<String,TestException> mappingFuture =
+                                                        MappingCheckedFuture.create( delegate, MAPPER );
+
+        delegate.setException( cause );
+
+        try {
+            invoker.invokeGet( mappingFuture );
+            fail( "Expected exception thrown" );
+        } catch( Exception e ) {
+            Throwable expectedTestEx = invoker.extractWrappedTestEx( e );
+            assertNotNull( "Expected returned exception is null", expectedTestEx );
+            assertEquals( "Exception type", TestException.class, expectedTestEx.getClass() );
+
+            if( cause instanceof TestException ) {
+                assertNull( "Expected null cause", expectedTestEx.getCause() );
+            } else {
+                assertSame( "TestException cause", cause, expectedTestEx.getCause() );
+            }
+        }
+    }
+
+    private void testCancellationException( FutureInvoker invoker ) {
+
+        SettableFuture<String> delegate = SettableFuture.create();
+        MappingCheckedFuture<String,TestException> mappingFuture =
+                                                        MappingCheckedFuture.create( delegate, MAPPER );
+
+        mappingFuture.cancel( false );
+
+        try {
+            invoker.invokeGet( mappingFuture );
+            fail( "Expected exception thrown" );
+        } catch( Exception e ) {
+            Throwable expectedTestEx = invoker.extractWrappedTestEx( e );
+            assertNotNull( "Expected returned exception is null", expectedTestEx );
+            assertEquals( "Exception type", TestException.class, expectedTestEx.getClass() );
+            assertEquals( "TestException cause type", CancellationException.class,
+                          expectedTestEx.getCause().getClass() );
+        }
+    }
+
+    private void testInterruptedException( final FutureInvoker invoker ) throws Exception {
+
+        SettableFuture<String> delegate = SettableFuture.create();
+        final MappingCheckedFuture<String,TestException> mappingFuture =
+                                                        MappingCheckedFuture.create( delegate, MAPPER );
+
+        final AtomicReference<AssertionError> assertError = new AtomicReference<>();
+        final CountDownLatch doneLatch = new CountDownLatch( 1 );
+        Thread thread = new Thread() {
+
+            @Override
+            public void run() {
+                try {
+                    doInvoke();
+                } catch( AssertionError e ) {
+                    assertError.set( e );
+                } finally {
+                    doneLatch.countDown();
+                }
+            }
+
+            void doInvoke() {
+                try {
+                    invoker.invokeGet( mappingFuture );
+                    fail( "Expected exception thrown" );
+                } catch( Exception e ) {
+                    Throwable expectedTestEx = invoker.extractWrappedTestEx( e );
+                    assertNotNull( "Expected returned exception is null", expectedTestEx );
+                    assertEquals( "Exception type", TestException.class, expectedTestEx.getClass() );
+                    assertEquals( "TestException cause type", InterruptedException.class,
+                                  expectedTestEx.getCause().getClass() );
+                }
+            }
+        };
+        thread.start();
+
+        thread.interrupt();
+        assertEquals( "get call completed", true, doneLatch.await( 5, TimeUnit.SECONDS ) );
+
+        if( assertError.get() != null ) {
+            throw assertError.get();
+        }
+    }
+}
diff --git a/yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/OperationFailedException.java b/yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/OperationFailedException.java
new file mode 100644 (file)
index 0000000..e1f0b71
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2014 Brocade Communications Systems, Inc. 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.yang.common;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.opendaylight.yangtools.yang.common.RpcError;
+import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
+import org.opendaylight.yangtools.yang.common.RpcError.ErrorType;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * A general base exception for an operation failure.
+ *
+ * @author Thomas Pantelis
+ */
+public class OperationFailedException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+
+    private final List<RpcError> errorList;
+
+    /**
+     * Constructs a new instance with the specified detail message and errors.
+     *
+     * @param message the detail message
+     * @param errors {@link RpcError} instances that provide additional error information about
+     *               this exception
+     */
+    public OperationFailedException(final String message, final RpcError... errors) {
+        this(message, null, errors);
+    }
+
+    /**
+     * Constructs a new instance with the specified detail message, cause and errors.
+     *
+     * @param message the detail message
+     * @param cause the cause
+     * @param errors {@link RpcError} instances that provide additional error information about
+     *               this exception
+     */
+    public OperationFailedException(final String message, final Throwable cause,
+                                    final RpcError... errors) {
+        super(Preconditions.checkNotNull(message), cause);
+
+        if( errors != null && errors.length > 0 ) {
+            errorList = ImmutableList.<RpcError>copyOf( Arrays.asList( errors ) );
+        }
+        else {
+            // Add a default RpcError.
+            errorList = ImmutableList.of(RpcResultBuilder.newError(ErrorType.APPLICATION, null,
+                    getMessage(), null, null, getCause()));
+        }
+    }
+
+    /**
+     * Returns additional error information about this exception.
+     *
+     * @return a List of RpcErrors. There is always at least one RpcError.
+     */
+    public List<RpcError> getErrorList() {
+        return errorList;
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toStringHelper( this ).add( "message", getMessage() )
+                .add( "errorList", errorList ).toString();
+    }
+}