diff --git a/jrugged-core/src/test/java/org/fishwife/jrugged/TestCircuitBreaker.java b/jrugged-core/src/test/java/org/fishwife/jrugged/TestCircuitBreaker.java index 02337698..31524020 100644 --- a/jrugged-core/src/test/java/org/fishwife/jrugged/TestCircuitBreaker.java +++ b/jrugged-core/src/test/java/org/fishwife/jrugged/TestCircuitBreaker.java @@ -1,3 +1,464 @@ +<<<<<<< HEAD +/* TestCircuitBreaker.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged; + +import java.util.concurrent.Callable; + +import junit.framework.Assert; +import org.junit.Before; +import org.junit.Test; + +import static junit.framework.Assert.assertNull; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class TestCircuitBreaker { + private CircuitBreaker impl; + private Callable mockCallable; + private Runnable mockRunnable; + + Status theStatus; + + @SuppressWarnings("unchecked") + @Before + public void setUp() { + impl = new CircuitBreaker(); + mockCallable = createMock(Callable.class); + mockRunnable = createMock(Runnable.class); + } + + @Test + public void testInvokeWithRunnableResultAndResultReturnsResult() throws Exception { + final Object result = new Object(); + + mockRunnable.run(); + replay(mockRunnable); + + Object theReturned = impl.invoke(mockRunnable, result); + + verify(mockRunnable); + assertSame(result, theReturned); + } + + @Test + public void testInvokeWithRunnableResultAndByPassReturnsResult() throws Exception { + final Object result = new Object(); + impl.setByPassState(true); + + mockRunnable.run(); + replay(mockRunnable); + + Object theReturned = impl.invoke(mockRunnable, result); + + verify(mockRunnable); + assertSame(result, theReturned); + } + + @Test(expected = CircuitBreakerException.class) + public void testInvokeWithRunnableResultAndTripHardReturnsException() throws Exception { + final Object result = new Object(); + impl.tripHard(); + + mockRunnable.run(); + replay(mockRunnable); + + impl.invoke(mockRunnable, result); + + verify(mockRunnable); + } + + @Test + public void testInvokeWithRunnableDoesNotError() throws Exception { + mockRunnable.run(); + replay(mockRunnable); + + impl.invoke(mockRunnable); + + verify(mockRunnable); + } + + @Test + public void testInvokeWithRunnableAndByPassDoesNotError() throws Exception { + impl.setByPassState(true); + + mockRunnable.run(); + replay(mockRunnable); + + impl.invoke(mockRunnable); + + verify(mockRunnable); + } + + @Test(expected = CircuitBreakerException.class) + public void testInvokeWithRunnableAndTripHardReturnsException() throws Exception { + impl.tripHard(); + + mockRunnable.run(); + replay(mockRunnable); + + impl.invoke(mockRunnable); + + verify(mockRunnable); + } + + @Test + public void testStaysClosedOnSuccess() throws Exception { + impl.state = CircuitBreaker.BreakerState.CLOSED; + final Object obj = new Object(); + expect(mockCallable.call()).andReturn(obj); + replay(mockCallable); + + Object result = impl.invoke(mockCallable); + + verify(mockCallable); + assertSame(obj, result); + assertEquals(CircuitBreaker.BreakerState.CLOSED, impl.state); + } + + @Test + public void testOpensOnFailure() throws Exception { + long start = System.currentTimeMillis(); + impl.state = CircuitBreaker.BreakerState.OPEN; + expect(mockCallable.call()).andThrow(new RuntimeException()); + replay(mockCallable); + + try { + impl.invoke(mockCallable); + fail("should have thrown an exception"); + } catch (RuntimeException expected) { + } + + long end = System.currentTimeMillis(); + + verify(mockCallable); + assertEquals(CircuitBreaker.BreakerState.OPEN, impl.state); + assertTrue(impl.lastFailure.get() >= start); + assertTrue(impl.lastFailure.get() <= end); + } + + @Test + public void testOpenDuringCooldownThrowsCBException() + throws Exception { + + impl.state = CircuitBreaker.BreakerState.OPEN; + impl.lastFailure.set(System.currentTimeMillis()); + replay(mockCallable); + + try { + impl.invoke(mockCallable); + fail("should have thrown an exception"); + } catch (CircuitBreakerException expected) { + } + + verify(mockCallable); + assertEquals(CircuitBreaker.BreakerState.OPEN, impl.state); + } + + @Test + public void testOpenAfterCooldownGoesHalfClosed() + throws Exception { + + impl.state = CircuitBreaker.BreakerState.OPEN; + impl.resetMillis.set(1000); + impl.lastFailure.set(System.currentTimeMillis() - 2000); + + assertEquals(Status.DEGRADED, impl.getStatus()); + assertEquals(CircuitBreaker.BreakerState.HALF_CLOSED, impl.state); + } + + @Test + public void testHalfClosedFailureOpensAgain() + throws Exception { + + impl.state = CircuitBreaker.BreakerState.HALF_CLOSED; + impl.resetMillis.set(1000); + impl.lastFailure.set(System.currentTimeMillis() - 2000); + + long start = System.currentTimeMillis(); + + expect(mockCallable.call()).andThrow(new RuntimeException()); + replay(mockCallable); + + try { + impl.invoke(mockCallable); + fail("should have thrown exception"); + } catch (RuntimeException expected) { + } + + long end = System.currentTimeMillis(); + + verify(mockCallable); + assertEquals(CircuitBreaker.BreakerState.OPEN, impl.state); + assertTrue(impl.lastFailure.get() >= start); + assertTrue(impl.lastFailure.get() <= end); + } + + @Test + public void testGetStatusNotUpdatingIsAttemptLive() throws Exception { + + impl.resetMillis.set(50); + impl.trip(); + assertEquals(CircuitBreaker.BreakerState.OPEN, impl.state); + assertEquals(false, impl.isAttemptLive); + + Thread.sleep(200); + + // The getStatus()->canAttempt() call also updated isAttemptLive to true + assertEquals(Status.DEGRADED.getValue(), impl.getStatus().getValue()); + assertEquals(false, impl.isAttemptLive); + } + + @Test + public void testManualTripAndReset() throws Exception { + impl.state = CircuitBreaker.BreakerState.OPEN; + final Object obj = new Object(); + expect(mockCallable.call()).andReturn(obj); + replay(mockCallable); + + impl.trip(); + try { + impl.invoke(mockCallable); + fail("Manual trip method failed."); + } catch (CircuitBreakerException e) { + } + + impl.reset(); + + Object result = impl.invoke(mockCallable); + + verify(mockCallable); + assertSame(obj, result); + assertEquals(CircuitBreaker.BreakerState.CLOSED, impl.state); + } + + @Test + public void testTripHard() throws Exception { + expect(mockCallable.call()).andReturn("hi"); + + replay(mockCallable); + + impl.tripHard(); + try { + impl.invoke(mockCallable); + fail("exception expected after CircuitBreaker.tripHard()"); + } catch (CircuitBreakerException e) { + } + assertEquals(CircuitBreaker.BreakerState.OPEN, impl.state); + + impl.reset(); + impl.invoke(mockCallable); + assertEquals(CircuitBreaker.BreakerState.CLOSED, impl.state); + + verify(mockCallable); + } + + @Test + public void testGetTripCount() throws Exception { + long tripCount1 = impl.getTripCount(); + + impl.tripHard(); + long tripCount2 = impl.getTripCount(); + assertEquals(tripCount1 + 1, tripCount2); + + impl.tripHard(); + assertEquals(tripCount2, impl.getTripCount()); + } + + @Test + public void testGetStatusWhenOpen() { + impl.state = CircuitBreaker.BreakerState.OPEN; + Assert.assertEquals(Status.DOWN, impl.getStatus()); + } + + @Test + public void testGetStatusWhenHalfClosed() { + impl.state = CircuitBreaker.BreakerState.HALF_CLOSED; + assertEquals(Status.DEGRADED, impl.getStatus()); + } + + @Test + public void testGetStatusWhenOpenBeforeReset() { + impl.state = CircuitBreaker.BreakerState.CLOSED; + impl.resetMillis.set(1000); + impl.lastFailure.set(System.currentTimeMillis() - 50); + + assertEquals(Status.UP, impl.getStatus()); + } + + @Test + public void testGetStatusWhenOpenAfterReset() { + impl.state = CircuitBreaker.BreakerState.OPEN; + impl.resetMillis.set(1000); + impl.lastFailure.set(System.currentTimeMillis() - 2000); + + assertEquals(Status.DEGRADED, impl.getStatus()); + } + + @Test + public void testGetStatusAfterHardTrip() { + impl.tripHard(); + impl.resetMillis.set(1000); + impl.lastFailure.set(System.currentTimeMillis() - 2000); + + assertEquals(Status.DOWN, impl.getStatus()); + } + + @Test + public void testStatusIsByPassWhenSet() { + impl.setByPassState(true); + assertEquals(Status.DEGRADED, impl.getStatus()); + } + + @Test + public void testByPassIgnoresCurrentBreakerStateWhenSet() { + impl.state = CircuitBreaker.BreakerState.OPEN; + assertEquals(Status.DOWN, impl.getStatus()); + + impl.setByPassState(true); + assertEquals(Status.DEGRADED, impl.getStatus()); + + impl.setByPassState(false); + assertEquals(Status.DOWN, impl.getStatus()); + } + + @Test + public void testByPassIgnoresBreakerStateAndCallsWrappedMethod() throws Exception { + expect(mockCallable.call()).andReturn("hi").anyTimes(); + + replay(mockCallable); + + impl.tripHard(); + impl.setByPassState(true); + + try { + impl.invoke(mockCallable); + } catch (CircuitBreakerException e) { + fail("exception not expected when CircuitBreaker is bypassed."); + } + assertEquals(CircuitBreaker.BreakerState.OPEN, impl.state); + assertEquals(Status.DEGRADED, impl.getStatus()); + + impl.reset(); + impl.setByPassState(false); + impl.invoke(mockCallable); + assertEquals(CircuitBreaker.BreakerState.CLOSED, impl.state); + + verify(mockCallable); + } + + @Test + public void testNotificationCallback() throws Exception { + + CircuitBreakerNotificationCallback cb = new CircuitBreakerNotificationCallback() { + public void notify(Status s) { + theStatus = s; + } + }; + + impl.addListener(cb); + impl.trip(); + + assertNotNull(theStatus); + assertEquals(Status.DOWN, theStatus); + } + + @Test(expected = Throwable.class) + public void circuitBreakerKeepsExceptionThatTrippedIt() throws Throwable { + + try { + impl.invoke(new FailingCallable("broken")); + } catch (Exception e) { + + } + + Throwable tripException = impl.getTripException(); + assertEquals("broken", tripException.getMessage()); + throw tripException; + } + + @Test(expected = Throwable.class) + public void resetCircuitBreakerStillHasTripException() throws Throwable { + + try { + impl.invoke(new FailingCallable("broken")); + } catch (Exception e) { + + } + impl.reset(); + + Throwable tripException = impl.getTripException(); + assertEquals("broken", tripException.getMessage()); + throw tripException; + } + + @Test + public void circuitBreakerReturnsExceptionAsString() { + + try { + impl.invoke(new FailingCallable("broken")); + } catch (Exception e) { + + } + + Throwable tripException = impl.getTripException(); + + String s = impl.getTripExceptionAsString(); + + assertTrue(impl.getTripExceptionAsString().startsWith("java.lang.Exception: broken")); + assertTrue(impl.getTripExceptionAsString().contains("at org.fishwife.jrugged.TestCircuitBreaker$FailingCallable.call")); + assertTrue(impl.getTripExceptionAsString().contains("Caused by: java.lang.Exception: The Cause")); + } + + @Test + public void neverTrippedCircuitBreakerReturnsNullForTripException() throws Exception { + + impl.invoke(mockCallable); + + Throwable tripException = impl.getTripException(); + + assertNull(tripException); + } + + private class FailingCallable implements Callable { + + private final String exceptionMessage; + + public FailingCallable(String exceptionMessage) { + this.exceptionMessage = exceptionMessage; + } + + Exception causeException = new Exception("The Cause"); + + public Object call() throws Exception { + throw new Exception(exceptionMessage, causeException); + } + } + +} +======= /* TestCircuitBreaker.java * * Copyright 2009-2019 Comcast Interactive Media, LLC. @@ -426,9 +887,9 @@ public void circuitBreakerReturnsExceptionAsString() { String s = impl.getTripExceptionAsString(); - assertTrue(impl.getTripExceptionAsString().startsWith("java.lang.Exception: broken\n")); + assertTrue(impl.getTripExceptionAsString().startsWith("java.lang.Exception: broken")); assertTrue(impl.getTripExceptionAsString().contains("at org.fishwife.jrugged.TestCircuitBreaker$FailingCallable.call")); - assertTrue(impl.getTripExceptionAsString().contains("Caused by: java.lang.Exception: The Cause\n")); + assertTrue(impl.getTripExceptionAsString().contains("Caused by: java.lang.Exception: The Cause")); } @Test @@ -457,3 +918,4 @@ public Object call() throws Exception { } } +>>>>>>> f7dea133a9c7315155f1d3ec94f8f53856be8a87 diff --git a/jrugged-examples/pom.xml b/jrugged-examples/pom.xml index 4ec8edeb..25d7243e 100644 --- a/jrugged-examples/pom.xml +++ b/jrugged-examples/pom.xml @@ -280,7 +280,7 @@ - 4.1.3.RELEASE + 4.3.3.RELEASE 2.0 4.4 3.0.1 diff --git a/jrugged-spring-V5/pom.xml b/jrugged-spring-V5/pom.xml new file mode 100644 index 00000000..d79813b3 --- /dev/null +++ b/jrugged-spring-V5/pom.xml @@ -0,0 +1,209 @@ + + + + + org.fishwife + jrugged + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + jrugged-spring-V5 + jar + jrugged-spring-V5 + https://github.com/Comcast/jrugged + + + 5.1.5.RELEASE + 18.0 + 1.10.19 + 1.2.1 + + + + + org.fishwife + jrugged-core + 4.0.0-SNAPSHOT + compile + + + org.fishwife + jrugged-aspects + 4.0.0-SNAPSHOT + compile + + + org.springframework + spring-context + ${spring.version} + compile + + + commons-logging + commons-logging + + + + + org.springframework + spring-core + ${spring.version} + compile + + + commons-logging + commons-logging + + + + + org.springframework + spring-beans + ${spring.version} + compile + + + commons-logging + commons-logging + + + + + org.springframework + spring-aop + ${spring.version} + compile + + + commons-logging + commons-logging + + + + + org.springframework.retry + spring-retry + 1.1.0.RELEASE + + + commons-logging + commons-logging + + + + + org.springframework + spring-test + ${spring.version} + test + + + commons-logging + commons-logging + + + + + com.google.guava + guava + ${guava.version} + compile + + + cglib + cglib + 2.2 + compile + + + asm + asm + 3.1 + compile + + + aopalliance + aopalliance + 1.0 + compile + + + commons-logging + commons-logging + 1.1 + compile + + + log4j + log4j + + + + + javax.servlet + javax.servlet-api + 3.0.1 + provided + + + org.springframework + spring-webmvc + ${spring.version} + compile + + + org.mockito + mockito-all + ${mockito.version} + test + + + org.hamcrest + hamcrest-core + ${hamcrest.version} + test + + + org.hamcrest + hamcrest-library + ${hamcrest.version} + test + + + + + + + maven-eclipse-plugin + 2.7 + + + org.springframework.ide.eclipse.core.springnature + + + org.springframework.ide.eclipse.core.springbuilder + + true + true + 1.5 + none + + + + + diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/AnnotatedMethodFilter.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/AnnotatedMethodFilter.java new file mode 100644 index 00000000..b4ea9ab0 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/AnnotatedMethodFilter.java @@ -0,0 +1,56 @@ +/* Copyright 2009-2019 Comcast Interactive Media, LLC. + + 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.fishwife.jrugged.spring; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.util.Set; + +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.MethodMetadata; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.TypeFilter; + +/** + * TypeFilter to find classes based on annotations on + * {@link java.lang.reflect.Method}s. + */ +public class AnnotatedMethodFilter implements TypeFilter { + + private final Class annotatedClass; + + /** + * Create filter for classes with {@link java.lang.reflect.Method}s + * annotated with specified annotation. + * + * @param annotatedClass The annotated Class + */ + public AnnotatedMethodFilter(Class annotatedClass) { + this.annotatedClass = annotatedClass; + } + + /** + * {@inheritDoc} + */ + public boolean match(MetadataReader metadataReader, + MetadataReaderFactory metadataReaderFactory) throws IOException { + AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata(); + Set annotatedMethods = annotationMetadata + .getAnnotatedMethods(annotatedClass.getCanonicalName()); + return !annotatedMethods.isEmpty(); + } + +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/AnnotatedMethodScanner.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/AnnotatedMethodScanner.java new file mode 100644 index 00000000..6cbd6fe5 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/AnnotatedMethodScanner.java @@ -0,0 +1,82 @@ +/* Copyright 2009-2019 Comcast Interactive Media, LLC. + + 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.fishwife.jrugged.spring; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; + +public class AnnotatedMethodScanner { + + private final ClassLoader classLoader; + private final ClassPathScanningCandidateComponentProvider provider; + + public AnnotatedMethodScanner() { + classLoader = AnnotatedMethodScanner.class.getClassLoader(); + provider = new ClassPathScanningCandidateComponentProvider(false); + } + + // package private for testing only + AnnotatedMethodScanner(ClassLoader classLoader, ClassPathScanningCandidateComponentProvider provider) { + this.classLoader = classLoader; + this.provider = provider; + } + + /** + * Find all methods on classes under scanBase that are annotated with annotationClass. + * + * @param scanBase Package to scan recursively, in dot notation (ie: org.jrugged...) + * @param annotationClass Class of the annotation to search for + * @return Set<Method> The set of all @{java.lang.reflect.Method}s having the annotation + */ + public Set findAnnotatedMethods(String scanBase, Class annotationClass) { + Set filteredComponents = findCandidateBeans(scanBase, annotationClass); + return extractAnnotatedMethods(filteredComponents, annotationClass); + } + + Set extractAnnotatedMethods( + Set filteredComponents, + Class annoClass) { + Set annotatedMethods = new HashSet(); + for (BeanDefinition bd : filteredComponents) { + try { + String className = bd.getBeanClassName(); + Class beanClass = classLoader.loadClass(className); + for (Method m : beanClass.getMethods()) { + if (m.getAnnotation(annoClass) != null) { + annotatedMethods.add(m); + } + } + } catch (ClassNotFoundException cnfe) { + // no-op + } + } + + return annotatedMethods; + } + + synchronized Set findCandidateBeans(String scanBase, + Class annotatedClass) { + provider.resetFilters(false); + provider.addIncludeFilter(new AnnotatedMethodFilter(annotatedClass)); + + String basePackage = scanBase.replace('.', '/'); + return provider.findCandidateComponents(basePackage); + } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/AsyncCircuitBreaker.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/AsyncCircuitBreaker.java new file mode 100644 index 00000000..96abb458 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/AsyncCircuitBreaker.java @@ -0,0 +1,133 @@ +package org.fishwife.jrugged.spring; + +import java.util.concurrent.Callable; + +import org.fishwife.jrugged.CircuitBreakerException; +import org.fishwife.jrugged.CircuitBreakerExceptionMapper; +import org.fishwife.jrugged.DefaultFailureInterpreter; +import org.fishwife.jrugged.FailureInterpreter; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.ListenableFutureCallback; +import org.springframework.util.concurrent.SettableListenableFuture; + +public class AsyncCircuitBreaker extends org.fishwife.jrugged.CircuitBreaker { + + /** Creates a {@link AsyncCircuitBreaker} with a {@link + * DefaultFailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link CircuitBreakerException}). */ + public AsyncCircuitBreaker() { + } + + /** Creates a {@link AsyncCircuitBreaker} with a {@link + * DefaultFailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link CircuitBreakerException}). + * @param name the name for the {@link AsyncCircuitBreaker}. + */ + public AsyncCircuitBreaker(String name) { + this.name = name; + } + + /** Creates a {@link AsyncCircuitBreaker} with the specified {@link + * FailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link CircuitBreakerException}). + * @param fi the FailureInterpreter to use when + * determining whether a specific failure ought to cause the + * breaker to trip + */ + public AsyncCircuitBreaker(FailureInterpreter fi) { + failureInterpreter = fi; + } + + /** Creates a {@link AsyncCircuitBreaker} with the specified {@link + * FailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link CircuitBreakerException}). + * @param name the name for the {@link AsyncCircuitBreaker}. + * @param fi the FailureInterpreter to use when + * determining whether a specific failure ought to cause the + * breaker to trip + */ + public AsyncCircuitBreaker(String name, FailureInterpreter fi) { + this.name = name; + failureInterpreter = fi; + } + + /** Creates a {@link AsyncCircuitBreaker} with a {@link + * DefaultFailureInterpreter} and using the supplied {@link + * CircuitBreakerExceptionMapper} when client calls are made + * while the breaker is tripped. + * @param name the name for the {@link AsyncCircuitBreaker}. + * @param mapper helper used to translate a {@link + * CircuitBreakerException} into an application-specific one */ + public AsyncCircuitBreaker(String name, CircuitBreakerExceptionMapper mapper) { + this.name = name; + exceptionMapper = mapper; + } + + /** Creates a {@link AsyncCircuitBreaker} with the provided {@link + * FailureInterpreter} and using the provided {@link + * CircuitBreakerExceptionMapper} when client calls are made + * while the breaker is tripped. + * @param name the name for the {@link AsyncCircuitBreaker}. + * @param fi the FailureInterpreter to use when + * determining whether a specific failure ought to cause the + * breaker to trip + * @param mapper helper used to translate a {@link + * CircuitBreakerException} into an application-specific one */ + public AsyncCircuitBreaker(String name, + FailureInterpreter fi, + CircuitBreakerExceptionMapper mapper) { + this.name = name; + failureInterpreter = fi; + exceptionMapper = mapper; + } + + /** Wrap the given service call with the {@link AsyncCircuitBreaker} protection logic. + * @param callable the {@link java.util.concurrent.Callable} to attempt + * @param The result of a future call + * @return {@link ListenableFuture} of whatever callable would return + * @throws org.fishwife.jrugged.CircuitBreakerException if the + * breaker was OPEN or HALF_CLOSED and this attempt wasn't the + * reset attempt + * @throws Exception if the {@link org.fishwife.jrugged.CircuitBreaker} is in OPEN state + */ + public ListenableFuture invokeAsync(Callable> callable) throws Exception { + + final SettableListenableFuture response = new SettableListenableFuture(); + ListenableFutureCallback callback = new ListenableFutureCallback() { + @Override + public void onSuccess(T result) { + close(); + response.set(result); + } + + @Override + public void onFailure(Throwable ex) { + try { + handleFailure(ex); + } catch (Exception e) { + response.setException(e); + } + } + }; + + if (!byPass) { + if (!allowRequest()) { + throw mapException(new CircuitBreakerException()); + } + + try { + isAttemptLive = true; + callable.call().addCallback(callback); + return response; + } catch (Throwable cause) { + // This shouldn't happen because Throwables are handled in the async onFailure callback + handleFailure(cause); + } + throw new IllegalStateException("not possible"); + } + else { + callable.call().addCallback(callback); + return response; + } + } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/CircuitBreakerBean.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/CircuitBreakerBean.java new file mode 100644 index 00000000..fe6f6ce0 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/CircuitBreakerBean.java @@ -0,0 +1,305 @@ +/* Copyright 2009-2019 Comcast Interactive Media, LLC. + + 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.fishwife.jrugged.spring; + +import org.fishwife.jrugged.CircuitBreaker; +import org.fishwife.jrugged.CircuitBreakerExceptionMapper; +import org.fishwife.jrugged.DefaultFailureInterpreter; +import org.fishwife.jrugged.FailureInterpreter; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jmx.export.annotation.ManagedAttribute; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedResource; + +/** + * This is basically a {@link CircuitBreaker} that adds JMX + * annotations to some of the methods so that the core library + * doesn't have to depend on spring-context. + */ +@ManagedResource +public class CircuitBreakerBean extends CircuitBreaker implements InitializingBean { + + private boolean disabledAtStart = false; + + /** + * {@inheritDoc} + */ + public void afterPropertiesSet() throws Exception { + if (disabledAtStart) tripHard(); + } + + /** + * Creates a {@link CircuitBreakerBean} with a + * {@link DefaultFailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link org.fishwife.jrugged.CircuitBreakerException}). + */ + public CircuitBreakerBean() { super(); } + + /** + * Creates a {@link CircuitBreakerBean} with a + * {@link DefaultFailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link org.fishwife.jrugged.CircuitBreakerException}). + * @param name the name for the {@link CircuitBreakerBean} + */ + public CircuitBreakerBean(String name) { super(name); } + + /** + * Creates a {@link CircuitBreakerBean} with the specified + * {@link FailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link org.fishwife.jrugged.CircuitBreakerException}). + * @param fi the FailureInterpreter to use when + * determining whether a specific failure ought to cause the + * breaker to trip + */ + public CircuitBreakerBean(FailureInterpreter fi) { + super(fi); + } + + /** + * Creates a {@link CircuitBreakerBean} with the specified + * {@link FailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link org.fishwife.jrugged.CircuitBreakerException}). + * @param name the name for the {@link CircuitBreakerBean} + * @param fi the FailureInterpreter to use when + * determining whether a specific failure ought to cause the + * breaker to trip + */ + public CircuitBreakerBean(String name, FailureInterpreter fi) { + super(name, fi); + } + + /** + * Creates a {@link CircuitBreaker} with a {@link + * DefaultFailureInterpreter} and using the supplied {@link + * CircuitBreakerExceptionMapper} when client calls are made + * while the breaker is tripped. + * @param name the name for the {@link CircuitBreakerBean} + * @param mapper helper used to translate a {@link + * org.fishwife.jrugged.CircuitBreakerException} into an application-specific one + */ + public CircuitBreakerBean(String name, CircuitBreakerExceptionMapper mapper) { + super(name, mapper); + } + + /** + * Creates a {@link CircuitBreaker} with the provided {@link + * FailureInterpreter} and using the provided {@link + * CircuitBreakerExceptionMapper} when client calls are made + * while the breaker is tripped. + * + * @param name the name for the {@link CircuitBreakerBean} + * @param fi the FailureInterpreter to use when + * determining whether a specific failure ought to cause the + * breaker to trip + * @param mapper helper used to translate a {@link + * org.fishwife.jrugged.CircuitBreakerException} into an application-specific one + */ + public CircuitBreakerBean(String name, FailureInterpreter fi, + CircuitBreakerExceptionMapper mapper) { + super(name, fi, mapper); + } + + /** + * Manually trips the CircuitBreaker until {@link #reset()} is invoked. + */ + @ManagedOperation + @Override + public void tripHard() { + super.tripHard(); + } + + /** + * Manually trips the CircuitBreaker until {@link #reset()} is invoked. + */ + @ManagedOperation + @Override + public void trip() { + super.trip(); + } + + /** + * Returns the last time the breaker tripped OPEN, measured in + * milliseconds since the Epoch. + * @return long the last failure time + */ + @ManagedAttribute + @Override + public long getLastTripTime() { + return super.getLastTripTime(); + } + + /** + * Returns the number of times the breaker has tripped OPEN during + * its lifetime. + * @return long the number of times the circuit breaker tripped + */ + @ManagedAttribute + @Override + public long getTripCount() { + return super.getTripCount(); + } + + /** + * When called with true - causes the {@link CircuitBreaker} to byPass + * its functionality allowing requests to be executed unmolested + * until the CircuitBreaker is reset or the byPass + * is manually set to false. + */ + @ManagedAttribute + @Override + public void setByPassState(boolean b) { + super.setByPassState(b); + } + + /** + * Get the current state of the {@link CircuitBreaker} byPass + * + * @return boolean the byPass flag's current value + */ + @ManagedAttribute + @Override + public boolean getByPassState() { + return super.getByPassState(); + } + + /** + * Manually set the breaker to be reset and ready for use. This + * is only useful after a manual trip otherwise the breaker will + * trip automatically again if the service is still unavailable. + * Just like a real breaker. WOOT!!! + */ + @ManagedOperation + @Override + public void reset() { + super.reset(); + } + + /** + * Returns the cooldown period in milliseconds. + * + * @return long + */ + @ManagedAttribute + @Override + public long getResetMillis() { + return super.getResetMillis(); + } + + /** Sets the reset period to the given number of milliseconds. The + * default is 15,000 (make one retry attempt every 15 seconds). + * + * @param l number of milliseconds to "cool down" after tripping + * before allowing a "test request" through again + */ + @ManagedAttribute + @Override + public void setResetMillis(long l) { + super.setResetMillis(l); + } + + /** + * Returns a {@link String} representation of the breaker's + * status; potentially useful for exposing to monitoring software. + * + * @return String which is "GREEN" if + * the breaker is CLOSED; "YELLOW" if the breaker + * is HALF_CLOSED; and "RED" if the breaker is + * OPEN (tripped). + */ + @ManagedAttribute + @Override + public String getHealthCheck() { return super.getHealthCheck(); } + + /** + * Gets the failure tolerance limit for the {@link DefaultFailureInterpreter} that + * comes with a {@link CircuitBreaker} by default. + * + * @see DefaultFailureInterpreter + * + * @return the number of tolerated failures in a window + */ + @ManagedAttribute + public int getLimit() { + return ((DefaultFailureInterpreter) super.getFailureInterpreter()).getLimit(); + } + + /** + * Specifies the failure tolerance limit for the {@link + * DefaultFailureInterpreter} that comes with a {@link + * CircuitBreaker} by default. + * + * @see DefaultFailureInterpreter + * + * @param limit the number of tolerated failures in a window + */ + @ManagedAttribute + @Override + public void setLimit(int limit) { + ((DefaultFailureInterpreter) super.getFailureInterpreter()).setLimit(limit); + } + + /** + * Gets the tolerance window in milliseconds for the {@link DefaultFailureInterpreter} + * that comes with a {@link CircuitBreaker} by default. + * + * @see DefaultFailureInterpreter + * + * @return length of the window in milliseconds + */ + @ManagedAttribute + public long getWindowMillis() { + return ((DefaultFailureInterpreter) super.getFailureInterpreter()).getWindowMillis(); + } + + /** + * Specifies the tolerance window in milliseconds for the {@link + * DefaultFailureInterpreter} that comes with a {@link + * CircuitBreaker} by default. + * + * @see DefaultFailureInterpreter + * + * @param windowMillis length of the window in milliseconds + */ + @ManagedAttribute + @Override + public void setWindowMillis(long windowMillis) { + super.setWindowMillis(windowMillis); + } + + /** + * returns a {@link String} representation of the breaker's + * last known exception that caused it to OPEN (i.e. when the breaker + * opens, it will record the specific exception that caused it to open) + * + * @return String which is the full stack trace. + */ + @ManagedAttribute + @Override + public String getTripExceptionAsString() { + return super.getTripExceptionAsString(); + } + + /** + * Specifies whether the associated CircuitBreaker should be tripped + * at startup time. + * + * @param b true if the CircuitBreaker should start + * open (tripped); false if the CircuitBreaker should start + * closed (not tripped). + */ + public void setDisabled(boolean b) { + disabledAtStart = b; + } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/CircuitBreakerBeanFactory.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/CircuitBreakerBeanFactory.java new file mode 100644 index 00000000..b1df6c46 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/CircuitBreakerBeanFactory.java @@ -0,0 +1,148 @@ +/* Copyright 2009-2019 Comcast Interactive Media, LLC. + + 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.fishwife.jrugged.spring; + +import java.lang.reflect.Method; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.fishwife.jrugged.CircuitBreaker; +import org.fishwife.jrugged.CircuitBreakerConfig; +import org.fishwife.jrugged.CircuitBreakerFactory; +import org.fishwife.jrugged.DefaultFailureInterpreter; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jmx.export.MBeanExportOperations; +import org.springframework.jmx.export.MBeanExporter; + +/** + * Factory to create new {@link CircuitBreakerBean} instances and keep track of + * them. If a {@link MBeanExportOperations} is set, then the CircuitBreakerBean will be + * automatically exported as a JMX MBean. + */ +public class CircuitBreakerBeanFactory extends CircuitBreakerFactory implements InitializingBean { + + @Autowired(required = false) + private MBeanExportOperations mBeanExportOperations; + + private String packageScanBase; + + /** + * {@inheritDoc} + */ + public void afterPropertiesSet() { + buildAnnotatedCircuitBreakers(); + } + + /** + * Set the {@link MBeanExporter} to use to export {@link CircuitBreakerBean} + * instances as JMX MBeans. + * @param mBeanExporter the {@link MBeanExporter} to set. + */ + @Deprecated + public void setMBeanExporter(MBeanExporter mBeanExporter) { + setMBeanExportOperations(mBeanExporter); + } + + /** + * Set the {@link MBeanExportOperations} to use to export {@link CircuitBreakerBean} + * instances as JMX MBeans. + * @param mBeanExportOperations the {@link MBeanExportOperations} to set. + */ + public void setMBeanExportOperations(MBeanExportOperations mBeanExportOperations) { + this.mBeanExportOperations = mBeanExportOperations; + } + + /** + * If specified, CircuitBreakerBeanFactory will scan all classes + * under packageScanBase for methods with the + * {@link org.fishwife.jrugged.aspects.CircuitBreaker} annotation + * and initialize circuitbreakers for them. + * + * @param packageScanBase Where should the scan for annotations begin + */ + public void setPackageScanBase(String packageScanBase) { + this.packageScanBase = packageScanBase; + } + + /** + * If packageScanBase is defined will search packages for {@link org.fishwife.jrugged.aspects.CircuitBreaker} + * annotations and create circuitbreakers for them. + */ + public void buildAnnotatedCircuitBreakers() { + if (packageScanBase != null) { + AnnotatedMethodScanner methodScanner = new AnnotatedMethodScanner(); + for (Method m : methodScanner.findAnnotatedMethods(packageScanBase, org.fishwife.jrugged.aspects.CircuitBreaker.class)) { + org.fishwife.jrugged.aspects.CircuitBreaker circuitBreakerAnnotation = m.getAnnotation(org.fishwife.jrugged.aspects.CircuitBreaker.class); + DefaultFailureInterpreter dfi = new DefaultFailureInterpreter(circuitBreakerAnnotation.ignore(), circuitBreakerAnnotation.limit(), circuitBreakerAnnotation.windowMillis()); + CircuitBreakerConfig config = new CircuitBreakerConfig(circuitBreakerAnnotation.resetMillis(), dfi); + createCircuitBreaker(circuitBreakerAnnotation.name(), config); + } + } + + } + + /** + * Create a new {@link CircuitBreakerBean} and map it to the provided value. + * If the {@link MBeanExportOperations} is set, then the CircuitBreakerBean will be + * exported as a JMX MBean. + * If the CircuitBreaker already exists, then the existing instance is + * returned. + * @param name the value for the {@link org.fishwife.jrugged.CircuitBreaker} + * @param config the {@link org.fishwife.jrugged.CircuitBreakerConfig} + */ + public synchronized CircuitBreaker createCircuitBreaker(String name, CircuitBreakerConfig config) { + + CircuitBreaker circuitBreaker = findCircuitBreaker(name); + + if (circuitBreaker == null) { + circuitBreaker = new CircuitBreakerBean(name); + + configureCircuitBreaker(name, circuitBreaker, config); + + if (mBeanExportOperations != null) { + ObjectName objectName; + + try { + objectName = new ObjectName("org.fishwife.jrugged.spring:type=CircuitBreakerBean," + "name=" + name); + } catch (MalformedObjectNameException e) { + throw new IllegalArgumentException("Invalid MBean Name " + name, e); + + } + + mBeanExportOperations.registerManagedResource(circuitBreaker, objectName); + } + + addCircuitBreakerToMap(name, circuitBreaker); + } + + return circuitBreaker; + } + + /** + * Find an existing {@link CircuitBreakerBean} + * @param name the value for the {@link CircuitBreakerBean} + * @return the found {@link CircuitBreakerBean}, or null if it is not found. + */ + public CircuitBreakerBean findCircuitBreakerBean(String name) { + CircuitBreaker circuitBreaker = findCircuitBreaker(name); + + if (circuitBreaker instanceof CircuitBreakerBean) { + return (CircuitBreakerBean) circuitBreaker; + } + return null; + } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/FlowRegulatorBean.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/FlowRegulatorBean.java new file mode 100644 index 00000000..8c28e9ac --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/FlowRegulatorBean.java @@ -0,0 +1,44 @@ +/* FlowRegulatorBean.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring; + +import org.fishwife.jrugged.ConstantFlowRegulator; +import org.springframework.jmx.export.annotation.ManagedAttribute; + +public class FlowRegulatorBean extends ConstantFlowRegulator { + public FlowRegulatorBean() { + super(); + } + + @ManagedAttribute + @Override + /** + * {@inheritDoc} + */ + public void setRequestPerSecondThreshold(int i) { + super.setRequestPerSecondThreshold(i); + } + + @ManagedAttribute + @Override + /** + * {@inheritDoc} + */ + public int getRequestPerSecondThreshold() { + return super.getRequestPerSecondThreshold(); + } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/PerformanceMonitorBean.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/PerformanceMonitorBean.java new file mode 100644 index 00000000..355d46df --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/PerformanceMonitorBean.java @@ -0,0 +1,316 @@ +/* PerformanceMonitor.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring; + +import org.fishwife.jrugged.PerformanceMonitor; +import org.springframework.jmx.export.annotation.ManagedAttribute; +import org.springframework.jmx.export.annotation.ManagedResource; + +/** The {@link PerformanceMonitorBean} is a straightforward wrapper + * around a {@link PerformanceMonitor} that allows for leveraging + * automated exposure of the information via Spring's JMX annotations. + */ +@ManagedResource +public class PerformanceMonitorBean extends PerformanceMonitor { + + /** Constructs a PerformanceMonitorBean. */ + public PerformanceMonitorBean() { + super(); + } + + @ManagedAttribute + @Override + public double getAverageSuccessLatencyLastMinute() { + return super.getAverageSuccessLatencyLastMinute(); + } + + @ManagedAttribute + @Override + public double getAverageSuccessLatencyLastHour() { + return super.getAverageSuccessLatencyLastHour(); + } + + @ManagedAttribute + @Override + public double getAverageSuccessLatencyLastDay() { + return super.getAverageSuccessLatencyLastDay(); + } + + @ManagedAttribute + @Override + public double getAverageFailureLatencyLastMinute() { + return super.getAverageFailureLatencyLastMinute(); + } + + @ManagedAttribute + @Override + public double getAverageFailureLatencyLastHour() { + return super.getAverageFailureLatencyLastHour(); + } + + @ManagedAttribute + @Override + public double getAverageFailureLatencyLastDay() { + return super.getAverageFailureLatencyLastDay(); + } + + @ManagedAttribute + @Override + public double getTotalRequestsPerSecondLastMinute() { + return super.getTotalRequestsPerSecondLastMinute(); + } + + @ManagedAttribute + @Override + public double getSuccessRequestsPerSecondLastMinute() { + return super.getSuccessRequestsPerSecondLastMinute(); + } + + @ManagedAttribute + @Override + public double getFailureRequestsPerSecondLastMinute() { + return super.getFailureRequestsPerSecondLastMinute(); + } + + @ManagedAttribute + @Override + public double getTotalRequestsPerSecondLastHour() { + return super.getTotalRequestsPerSecondLastHour(); + } + + @ManagedAttribute + @Override + public double getSuccessRequestsPerSecondLastHour() { + return super.getSuccessRequestsPerSecondLastHour(); + } + + @ManagedAttribute + @Override + public double getFailureRequestsPerSecondLastHour() { + return super.getFailureRequestsPerSecondLastHour(); + } + + @ManagedAttribute + @Override + public double getTotalRequestsPerSecondLastDay() { + return super.getTotalRequestsPerSecondLastDay(); + } + + @ManagedAttribute + @Override + public double getSuccessRequestsPerSecondLastDay() { + return super.getSuccessRequestsPerSecondLastDay(); + } + + @ManagedAttribute + @Override + public double getFailureRequestsPerSecondLastDay() { + return super.getFailureRequestsPerSecondLastDay(); + } + + @ManagedAttribute + @Override + public double getTotalRequestsPerSecondLifetime() { + return super.getTotalRequestsPerSecondLifetime(); + } + + @ManagedAttribute + @Override + public double getSuccessRequestsPerSecondLifetime() { + return super.getSuccessRequestsPerSecondLifetime(); + } + + @ManagedAttribute + @Override + public double getFailureRequestsPerSecondLifetime() { + return super.getFailureRequestsPerSecondLifetime(); + } + + @ManagedAttribute + @Override + public long getRequestCount() { + return super.getRequestCount(); + } + + @ManagedAttribute + @Override + public long getSuccessCount() { + return super.getSuccessCount(); + } + + @ManagedAttribute + @Override + public long getFailureCount() { + return super.getFailureCount(); + } + + @ManagedAttribute + @Override + public long getMedianPercentileSuccessLatencyLifetime() { + return super.getMedianPercentileSuccessLatencyLifetime(); + } + + @ManagedAttribute + @Override + public long get95thPercentileSuccessLatencyLifetime() { + return super.get95thPercentileSuccessLatencyLifetime(); + } + + @ManagedAttribute + @Override + public long get99thPercentileSuccessLatencyLifetime() { + return super.get99thPercentileSuccessLatencyLifetime(); + } + + @ManagedAttribute + @Override + public long getMaxSuccessLatencyLifetime() { + return super.getMaxSuccessLatencyLifetime(); + } + + @ManagedAttribute + @Override + public long getMedianPercentileSuccessLatencyLastMinute() { + return super.getMedianPercentileSuccessLatencyLastMinute(); + } + + @ManagedAttribute + @Override + public long get95thPercentileSuccessLatencyLastMinute() { + return super.get95thPercentileSuccessLatencyLastMinute(); + } + + @ManagedAttribute + @Override + public long get99thPercentileSuccessLatencyLastMinute() { + return super.get99thPercentileSuccessLatencyLastMinute(); + } + + @ManagedAttribute + @Override + public long getMedianPercentileSuccessfulLatencyLastHour() { + return super.getMedianPercentileSuccessfulLatencyLastHour(); + } + + @ManagedAttribute + @Override + public long get95thPercentileSuccessLatencyLastHour() { + return super.get95thPercentileSuccessLatencyLastHour(); + } + + @ManagedAttribute + @Override + public long get99thPercentileSuccessLatencyLastHour() { + return super.get99thPercentileSuccessLatencyLastHour(); + } + + @ManagedAttribute + @Override + public long getMedianPercentileSuccessLatencyLastDay() { + return super.getMedianPercentileSuccessLatencyLastDay(); + } + + @ManagedAttribute + @Override + public long get95thPercentileSuccessLatencyLastDay() { + return super.get95thPercentileSuccessLatencyLastDay(); + } + + @ManagedAttribute + @Override + public long get99thPercentileSuccessLatencyLastDay() { + return super.get99thPercentileSuccessLatencyLastDay(); + } + + @ManagedAttribute + @Override + public long getMedianPercentileFailureLatencyLifetime() { + return super.getMedianPercentileFailureLatencyLifetime(); + } + + @ManagedAttribute + @Override + public long get95thPercentileFailureLatencyLifetime() { + return super.get95thPercentileFailureLatencyLifetime(); + } + + @ManagedAttribute + @Override + public long get99thPercentileFailureLatencyLifetime() { + return super.get99thPercentileFailureLatencyLifetime(); + } + + @ManagedAttribute + @Override + public long getMaxFailureLatencyLifetime() { + return super.getMaxFailureLatencyLifetime(); + } + + @ManagedAttribute + @Override + public long getMedianPercentileFailureLatencyLastMinute() { + return super.getMedianPercentileFailureLatencyLastMinute(); + } + + @ManagedAttribute + @Override + public long get95thPercentileFailureLatencyLastMinute() { + return super.get95thPercentileFailureLatencyLastMinute(); + } + + @ManagedAttribute + @Override + public long get99thPercentileFailureLatencyLastMinute() { + return super.get99thPercentileFailureLatencyLastMinute(); + } + + @ManagedAttribute + @Override + public long getMedianPercentileFailureLatencyLastHour() { + return super.getMedianPercentileFailureLatencyLastHour(); + } + + @ManagedAttribute + @Override + public long get95thPercentileFailureLatencyLastHour() { + return super.get95thPercentileFailureLatencyLastHour(); + } + + @ManagedAttribute + @Override + public long get99thPercentileFailureLatencyLastHour() { + return super.get99thPercentileFailureLatencyLastHour(); + } + + @ManagedAttribute + @Override + public long getMedianPercentileFailureLatencyLastDay() { + return super.getMedianPercentileFailureLatencyLastDay(); + } + + @ManagedAttribute + @Override + public long get95thPercentileFailureLatencyLastDay() { + return super.get95thPercentileFailureLatencyLastDay(); + } + + @ManagedAttribute + @Override + public long get99thPercentileFailureLatencyLastDay() { + return super.get99thPercentileFailureLatencyLastDay(); + } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/PerformanceMonitorBeanFactory.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/PerformanceMonitorBeanFactory.java new file mode 100644 index 00000000..073a7f2c --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/PerformanceMonitorBeanFactory.java @@ -0,0 +1,172 @@ +/* Copyright 2009-2019 Comcast Interactive Media, LLC. + + 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.fishwife.jrugged.spring; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.fishwife.jrugged.PerformanceMonitor; +import org.fishwife.jrugged.PerformanceMonitorFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jmx.export.MBeanExportOperations; +import org.springframework.jmx.export.MBeanExporter; + +/** + * Factory to create new {@link PerformanceMonitorBean} instances and keep track + * of the created instances. If the {@link MBeanExportOperations} is set, then the + * PerformanceMonitorBean will be automatically exported as a JMX MBean. + */ +public class PerformanceMonitorBeanFactory extends PerformanceMonitorFactory implements InitializingBean { + + @Autowired(required=false) + private MBeanExportOperations mBeanExportOperations; + + private List initialPerformanceMonitorList; + + private String packageScanBase; + + /** + * Constructor. + */ + public PerformanceMonitorBeanFactory() { + initialPerformanceMonitorList = new ArrayList(); + packageScanBase = null; + } + + /** + * {@inheritDoc} + */ + public void afterPropertiesSet() { + createInitialPerformanceMonitors(); + } + + /** + * Set the list of initial {@link PerformanceMonitorBean} instances to create. + * @param initialPerformanceMonitors the list of {@link PerformanceMonitorBean} names + */ + public void setInitialPerformanceMonitors(List initialPerformanceMonitors) { + if (initialPerformanceMonitors != null) { + initialPerformanceMonitorList.addAll(initialPerformanceMonitors); + } + } + + /** + * If specified, PerformanceMonitorBeanFactory will scan all classes + * under packageScanBase for methods with the + * {@link org.fishwife.jrugged.aspects.PerformanceMonitor} annotation + * and initialize performance monitors for them. + * + * @param packageScanBase Where should the scan for annotations begin + */ + public void setPackageScanBase(String packageScanBase) { + this.packageScanBase = packageScanBase; + } + + /** + * Create the initial {@link PerformanceMonitorBean} instances. + */ + public void createInitialPerformanceMonitors() { + if (packageScanBase != null) { + AnnotatedMethodScanner methodScanner = new AnnotatedMethodScanner(); + for (Method m : methodScanner.findAnnotatedMethods(packageScanBase, org.fishwife.jrugged.aspects.PerformanceMonitor.class)) { + org.fishwife.jrugged.aspects.PerformanceMonitor performanceMonitorAnnotation = m.getAnnotation(org.fishwife.jrugged.aspects.PerformanceMonitor.class); + initialPerformanceMonitorList.add(performanceMonitorAnnotation.value()); + } + } + for (String name: initialPerformanceMonitorList) { + createPerformanceMonitor(name); + } + } + + /** + * Set the {@link MBeanExporter} to use to export + * {@link PerformanceMonitorBean} instances as JMX MBeans. + * @param mBeanExporter the {@link MBeanExporter} to set. + */ + @Deprecated + public void setMBeanExporter(MBeanExporter mBeanExporter) { + setMBeanExportOperations(mBeanExporter); + } + + /** + * Set the {@link MBeanExportOperations} to use to export + * {@link PerformanceMonitorBean} instances as JMX MBeans. + * @param mBeanExportOperations the {@link MBeanExportOperations} to set. + */ + public void setMBeanExportOperations(MBeanExportOperations mBeanExportOperations) { + this.mBeanExportOperations = mBeanExportOperations; + } + + /** + * Create a new {@link PerformanceMonitorBean} and map it to the provided + * name. If the {@link MBeanExportOperations} is set, then the + * PerformanceMonitorBean will be exported as a JMX MBean. + * If the PerformanceMonitor already exists, then the existing instance is + * returned. + * @param name the value for the {@link PerformanceMonitorBean} + * @return the created {@link PerformanceMonitorBean} + * @throws IllegalArgumentException if the MBean value is invalid. + */ + public synchronized PerformanceMonitor createPerformanceMonitor( + String name) { + PerformanceMonitorBean performanceMonitor = + findPerformanceMonitorBean(name); + + if (performanceMonitor == null) { + performanceMonitor = new PerformanceMonitorBean(); + + if (mBeanExportOperations != null) { + ObjectName objectName; + + try { + objectName = new ObjectName( + "org.fishwife.jrugged.spring:type=" + + "PerformanceMonitorBean,name=" + name); + } + catch (MalformedObjectNameException e) { + throw new IllegalArgumentException( + "Invalid MBean Name " + name, e); + + } + + mBeanExportOperations.registerManagedResource( + performanceMonitor, objectName); + } + addPerformanceMonitorToMap(name, performanceMonitor); + } + + return performanceMonitor; + } + + /** + * Find an existing {@link PerformanceMonitorBean} + * @param name the value for the {@link PerformanceMonitorBean} + * @return the found {@link PerformanceMonitorBean}, or null if it is not + * found. + */ + public PerformanceMonitorBean findPerformanceMonitorBean(String name) { + PerformanceMonitor performanceMonitor = findPerformanceMonitor(name); + + if (performanceMonitor instanceof PerformanceMonitorBean) { + return (PerformanceMonitorBean)performanceMonitor; + } + return null; + } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/PerformanceMonitorFilter.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/PerformanceMonitorFilter.java new file mode 100644 index 00000000..50bb22ba --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/PerformanceMonitorFilter.java @@ -0,0 +1,75 @@ +/* PerformanceMonitorFilter.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.fishwife.jrugged.WrappedException; +import org.springframework.jmx.export.annotation.ManagedResource; + +/** + * This class is a standard Servlet filter that can be configured in web.xml to wrap all + * request handling in a {@link org.fishwife.jrugged.PerformanceMonitor}. In order to get + * useful access to the statistics, however, it is most convenient to make use of Spring's + * {@link org.springframework.web.filter.DelegatingFilterProxy} in web.xml and + * instantiate this filter within a Spring application context. This will allow the JMX + * annotations inherited from {@link PerformanceMonitorBean} to take effect, with the result + * that you can get a high-level performance monitor wrapped around all of your application's + * request handling. + */ +@ManagedResource +public class PerformanceMonitorFilter extends PerformanceMonitorBean implements Filter { + + public void doFilter(final ServletRequest req, final ServletResponse resp, + final FilterChain chain) throws IOException, ServletException { + try { + invoke(new Runnable() { + public void run() { + try { + chain.doFilter(req, resp); + } catch (IOException e) { + throw new WrappedException(e); + } catch (ServletException e) { + throw new WrappedException(e); + } + } + }); + } catch (WrappedException e) { + Throwable wrapped = e.getCause(); + if (wrapped instanceof IOException) { + throw (IOException)wrapped; + } else if (wrapped instanceof ServletException) { + throw (ServletException)wrapped; + } else { + throw new IllegalStateException("unknown wrapped exception", wrapped); + } + } catch (Exception e) { + throw new IllegalStateException("unknown checked exception", e); + } + } + + public void init(FilterConfig config) throws ServletException { } + + public void destroy() { } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/RequestCounter.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/RequestCounter.java new file mode 100644 index 00000000..5be4f1f2 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/RequestCounter.java @@ -0,0 +1,32 @@ +package org.fishwife.jrugged.spring; + +import java.util.concurrent.Callable; + +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.ListenableFutureCallback; +import org.springframework.util.concurrent.SettableListenableFuture; + +public class RequestCounter extends org.fishwife.jrugged.RequestCounter implements ServiceWrapper { + + @Override + public ListenableFuture invokeAsync(Callable> callable) throws Exception { + + final SettableListenableFuture response = new SettableListenableFuture(); + ListenableFutureCallback callback = new ListenableFutureCallback() { + @Override + public void onSuccess(T result) { + succeed(); + response.set(result); + } + + @Override + public void onFailure(Throwable ex) { + fail(); + response.setException(ex); + } + }; + + callable.call().addCallback(callback); + return response; + } +} \ No newline at end of file diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/RequestCounterBean.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/RequestCounterBean.java new file mode 100644 index 00000000..33f70940 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/RequestCounterBean.java @@ -0,0 +1,36 @@ +/* RequestCounterBean.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring; + +import org.fishwife.jrugged.RequestCounter; +import org.springframework.jmx.export.annotation.ManagedOperation; + +public class RequestCounterBean extends RequestCounter { + public RequestCounterBean() { + super(); + } + + @ManagedOperation + @Override + /** + * {@inheritDoc} + */ + public synchronized long[] sample() { + return super.sample(); + } + +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/ServiceWrapper.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/ServiceWrapper.java new file mode 100644 index 00000000..0fdd37cb --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/ServiceWrapper.java @@ -0,0 +1,16 @@ +package org.fishwife.jrugged.spring; + +import java.util.concurrent.Callable; + +import org.springframework.util.concurrent.ListenableFuture; + +public interface ServiceWrapper extends org.fishwife.jrugged.ServiceWrapper { + + /** Wraps a {@link java.util.concurrent.Callable} in some fashion. + * @param callable the service call to wrap + * @param The return value for a future call + * @return {@link ListenableFuture} of whatever callable would normally return + * @throws Exception because it's part of the {@link Callable#call()} method signature. Exceptions must be handled in the callback methods. + */ + ListenableFuture invokeAsync(Callable> callable) throws Exception; +} \ No newline at end of file diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/ServiceWrapperInterceptor.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/ServiceWrapperInterceptor.java new file mode 100644 index 00000000..6c5fa0df --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/ServiceWrapperInterceptor.java @@ -0,0 +1,91 @@ +/* ServiceWrapperInterceptor.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring; + +import java.util.Map; +import java.util.concurrent.Callable; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.fishwife.jrugged.CircuitBreaker; +import org.fishwife.jrugged.PerformanceMonitor; +import org.fishwife.jrugged.ServiceWrapper; + +/** + * A Spring interceptor that allows wrapping a method invocation with + * a {@link ServiceWrapper} (for example, a {@link CircuitBreaker} or + * {@link PerformanceMonitor}). + */ +public class ServiceWrapperInterceptor implements MethodInterceptor { + + private Map methodMap; + + /** See if the given method invocation is one that needs to be + * called through a {@link ServiceWrapper}, and if so, do so. + * @param invocation the {@link MethodInvocation} in question + * @return whatever the underlying method call would normally + * return + * @throws Throwable that the method call would generate, or + * that the {@link ServiceWrapper} would generate when tripped. + */ + public Object invoke(final MethodInvocation invocation) throws Throwable { + String methodName = invocation.getMethod().getName(); + + if (!shouldWrapMethodCall(methodName)) { + return invocation.proceed(); + } + else { + ServiceWrapper wrapper = methodMap.get(methodName); + + return wrapper.invoke(new Callable() { + public Object call() throws Exception { + try { + return invocation.proceed(); + } catch (Throwable e) { + if (e instanceof Exception) + throw (Exception) e; + else if (e instanceof Error) + throw (Error) e; + else + throw new RuntimeException(e); + } + } + }); + } + } + + private boolean shouldWrapMethodCall(String methodName) { + if (methodMap == null) { + return true; // Wrap all by default + } + + if (methodMap.get(methodName) != null) { + return true; //Wrap a specific method + } + + // If I get to this point, I should not wrap the call. + return false; + } + + /** Specifies which methods will be wrapped with which ServiceWrappers. + * @param methodMap the mapping! + */ + public void setMethods(Map methodMap) { + this.methodMap = methodMap; + } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/StatusController.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/StatusController.java new file mode 100644 index 00000000..c9aaca4c --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/StatusController.java @@ -0,0 +1,107 @@ +/* StatusController.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.fishwife.jrugged.MonitoredService; +import org.fishwife.jrugged.Status; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; + +/** + * This is a convenient {@link Controller} that can be used to implement a + * "heartbeat" URL in a web application. The StatusController + * has a particular {@link MonitoredService} associated with it (a common use + * case would be to inject a {@link org.fishwife.jrugged.RolledUpMonitoredService} for + * overall system health. The StatusController writes the + * current status out in the response body and sets an appropriate HTTP + * response code. This is useful in a load balancer setting where the load + * balancer periodically pings a pool of application servers to see if they + * are "OK" and removes them from the pool if they are not. If the + * Monitorable is capable of serving requests (i.e. is GREEN + * or YELLOW) then we return a 2XX response code; otherwise we return a 5XX + * response code. + */ +public class StatusController implements Controller { + + private static Map responseCodeMap; + static { + responseCodeMap = new HashMap(); + responseCodeMap.put(Status.FAILED, HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + responseCodeMap.put(Status.INIT, HttpServletResponse.SC_SERVICE_UNAVAILABLE); + responseCodeMap.put(Status.DOWN, HttpServletResponse.SC_SERVICE_UNAVAILABLE); + responseCodeMap.put(Status.DEGRADED, HttpServletResponse.SC_OK); + responseCodeMap.put(Status.BYPASS, HttpServletResponse.SC_OK); + responseCodeMap.put(Status.UP, HttpServletResponse.SC_OK); + } + + private MonitoredService monitoredService; + + public StatusController(MonitoredService monitoredService) { + this.monitoredService = monitoredService; + } + + public ModelAndView handleRequest(HttpServletRequest req, + HttpServletResponse resp) throws Exception { + Status currentStatus = monitoredService.getServiceStatus().getStatus(); + setResponseCode(currentStatus, resp); + setAppropriateWarningHeaders(resp, currentStatus); + setCachingHeaders(resp); + writeOutCurrentStatusInResponseBody(resp, currentStatus); + return null; + } + + private void setCachingHeaders(HttpServletResponse resp) { + long now = System.currentTimeMillis(); + resp.setDateHeader("Date", now); + resp.setDateHeader("Expires", now); + resp.setHeader("Cache-Control","no-cache"); + } + + private void setAppropriateWarningHeaders(HttpServletResponse resp, + Status currentStatus) { + if (Status.DEGRADED.equals(currentStatus)) { + resp.addHeader("Warning", "199 jrugged \"Status degraded\""); + } + } + + private void writeOutCurrentStatusInResponseBody(HttpServletResponse resp, + Status currentStatus) throws IOException { + resp.setHeader("Content-Type","text/plain;charset=utf-8"); + String body = currentStatus + "\n"; + byte[] bytes = body.getBytes(); + resp.setHeader("Content-Length", bytes.length + ""); + OutputStream out = resp.getOutputStream(); + out.write(bytes); + out.flush(); + out.close(); + } + + private void setResponseCode(Status currentStatus, HttpServletResponse resp) { + if (responseCodeMap.containsKey(currentStatus)) { + resp.setStatus(responseCodeMap.get(currentStatus)); + } + } + +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/aspects/RetryTemplate.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/aspects/RetryTemplate.java new file mode 100644 index 00000000..d023ef43 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/aspects/RetryTemplate.java @@ -0,0 +1,27 @@ +package org.fishwife.jrugged.spring.aspects; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/*** + * Annotation which enables the use of a RetryTemplate at the method level. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface RetryTemplate { + /*** + * The name of the spring bean where the appropriate retryTemplate can be found. + * @return String the name of the template + */ + String name() default "retryTemplate"; + + /*** + * The name of the spring bean where a recovery callback method can be found. + * @return String the callback name or "" + */ + String recoveryCallbackName() default ""; +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/aspects/RetryTemplateAspect.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/aspects/RetryTemplateAspect.java new file mode 100644 index 00000000..e95a0916 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/aspects/RetryTemplateAspect.java @@ -0,0 +1,94 @@ +package org.fishwife.jrugged.spring.aspects; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Required; +import org.springframework.retry.RecoveryCallback; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; + +import com.google.common.base.Strings; + +@Aspect +public class RetryTemplateAspect implements BeanFactoryAware { + + private static final Logger logger = + LoggerFactory.getLogger(RetryTemplateAspect.class); + + private BeanFactory beanFactory; + + /** Default constructor. */ + public RetryTemplateAspect() { + } + + @Autowired + @Required + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + /** Runs a method call through the spring managed + * {@link org.springframework.retry.support.RetryTemplate} instance indicated + * by the annotations "name" attribute. + * + * @param pjp a {@link org.aspectj.lang.ProceedingJoinPoint} representing an annotated + * method call. + * @param retryTemplateAnnotation the {@link org.fishwife.jrugged.spring.aspects.RetryTemplate} annotation + * that wrapped the method. + * @throws Throwable if the method invocation itself or the wrapping + * {@link org.springframework.retry.support.RetryTemplate} throws one during execution. + * @return The return value from the method call. + */ + @Around("@annotation(retryTemplateAnnotation)") + public Object retry(final ProceedingJoinPoint pjp, + final RetryTemplate retryTemplateAnnotation) throws Throwable { + final String name = retryTemplateAnnotation.name(); + final String recoveryCallbackName = retryTemplateAnnotation.recoveryCallbackName(); + + org.springframework.retry.support.RetryTemplate retryTemplate = + beanFactory.getBean(name, org.springframework.retry.support.RetryTemplate.class); + + RecoveryCallback recoveryCallback = null; + if (! Strings.isNullOrEmpty(recoveryCallbackName)) { + recoveryCallback = + beanFactory.getBean(recoveryCallbackName, org.springframework.retry.RecoveryCallback.class); + } + + + if (logger.isDebugEnabled()) { + logger.debug("Have @RetryTemplate method with retryTemplate name {} and callback name {}, " + + "wrapping call on method {} of target object {}", + new Object[]{ + name, + recoveryCallbackName, + pjp.getSignature().getName(), + pjp.getTarget()}); + } + + return retryTemplate.execute( + new RetryCallback() { + public Object doWithRetry(RetryContext context) throws Exception { + try { + return pjp.proceed(); + } + catch (Error e) { + throw e; + } + catch (Exception e) { + throw e; + } + catch (Throwable e) { + throw new RuntimeException(e); + } + } + }, + recoveryCallback); + } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/config/JRuggedNamespaceHandler.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/config/JRuggedNamespaceHandler.java new file mode 100644 index 00000000..8aa0863d --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/config/JRuggedNamespaceHandler.java @@ -0,0 +1,45 @@ +/* JRuggedNamespaceHandler.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.config; + +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +/** + * Handler class for the JRugged Spring namespace. This class registers + * custom parsers and decorators for the new perform element and the + * perfmon and methods attributes on bean elements. + * + * This class is associated with the http://www.fishwife.org/schema/jrugged + * namespace via the META-INF/spring.handlers file. + */ +public class JRuggedNamespaceHandler extends NamespaceHandlerSupport { + + /** + * Called by Spring to register any parsers and decorators. + */ + public void init() { + registerBeanDefinitionParser("perfmon", + new PerformanceMonitorBeanDefinitionParser()); + + registerBeanDefinitionDecoratorForAttribute("perfmon", + new PerformanceMonitorBeanDefinitionDecorator()); + + registerBeanDefinitionDecoratorForAttribute("methods", + new MonitorMethodInterceptorDefinitionDecorator()); + } + +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/config/MonitorMethodInterceptorDefinitionDecorator.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/config/MonitorMethodInterceptorDefinitionDecorator.java new file mode 100644 index 00000000..f8b17bf3 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/config/MonitorMethodInterceptorDefinitionDecorator.java @@ -0,0 +1,135 @@ +/* MonitorMethodInterceptorDefinitionDecorator.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.config; + +import java.util.ArrayList; +import java.util.List; + +import org.fishwife.jrugged.spring.PerformanceMonitorBean; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.xml.BeanDefinitionDecorator; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.StringUtils; +import org.w3c.dom.Attr; +import org.w3c.dom.Node; + +/** + * This class is invoked when Spring encounters the jrugged:methods attribute + * on a bean. It parses the attribute value with is a comma delimited list + * of method names to wrap with the PerformanceMonitor. It tells Spring to + * create a SingleServiceWrapperInterceptor named after the bean with + * "PerformanceMonitorInterceptor" appended to the name. It also defines + * a PerformanceMonitorBean named after the bean with "PerformanceMonitor" + * appended to it. The interceptor has a reference to the PerformanceMonitor. + */ +public class MonitorMethodInterceptorDefinitionDecorator implements + BeanDefinitionDecorator { + + /** + * Method called by Spring when it encounters the custom jrugged:methods + * attribute. Registers the performance monitor and interceptor. + */ + public BeanDefinitionHolder decorate(Node source, + BeanDefinitionHolder holder, + ParserContext context) { + + String beanName = holder.getBeanName(); + BeanDefinitionRegistry registry = context.getRegistry(); + registerPerformanceMonitor(beanName, registry); + registerInterceptor(source, beanName, registry); + + return holder; + } + + /** + * Register a new SingleServiceWrapperInterceptor for the bean being + * wrapped, associate it with the PerformanceMonitor and tell it which methods + * to intercept. + * + * @param source An Attribute node from the spring configuration + * @param beanName The name of the bean that this performance monitor is wrapped around + * @param registry The registry where all the spring beans are registered + */ + private void registerInterceptor(Node source, + String beanName, + BeanDefinitionRegistry registry) { + List methodList = buildMethodList(source); + + BeanDefinitionBuilder initializer = + BeanDefinitionBuilder.rootBeanDefinition(SingleServiceWrapperInterceptor.class); + initializer.addPropertyValue("methods", methodList); + + String perfMonitorName = beanName + "PerformanceMonitor"; + initializer.addPropertyReference("serviceWrapper", perfMonitorName); + + String interceptorName = beanName + "PerformanceMonitorInterceptor"; + registry.registerBeanDefinition(interceptorName, initializer.getBeanDefinition()); + } + + /** + * Parse the jrugged:methods attribute into a List of strings of method + * names + * + * @param source An Attribute node from the spring configuration + * + * @return List<String> + */ + private List buildMethodList(Node source) { + Attr attribute = (Attr)source; + String methods = attribute.getValue(); + return parseMethodList(methods); + } + + /** + * Parse a comma-delimited list of method names into a List of strings. + * Whitespace is ignored. + * + * @param methods the comma delimited list of methods from the spring configuration + * + * @return List<String> + */ + public List parseMethodList(String methods) { + + String[] methodArray = StringUtils.delimitedListToStringArray(methods, ","); + + List methodList = new ArrayList(); + + for (String methodName : methodArray) { + methodList.add(methodName.trim()); + } + return methodList; + } + + /** + * Register a new PerformanceMonitor with Spring if it does not already exist. + * + * @param beanName The name of the bean that this performance monitor is wrapped around + * @param registry The registry where all the spring beans are registered + */ + private void registerPerformanceMonitor(String beanName, + BeanDefinitionRegistry registry) { + + String perfMonitorName = beanName + "PerformanceMonitor"; + if (!registry.containsBeanDefinition(perfMonitorName)) { + BeanDefinitionBuilder initializer = + BeanDefinitionBuilder.rootBeanDefinition(PerformanceMonitorBean.class); + registry.registerBeanDefinition(perfMonitorName, initializer.getBeanDefinition()); + } + } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/config/PerformanceMonitorBeanDefinitionDecorator.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/config/PerformanceMonitorBeanDefinitionDecorator.java new file mode 100644 index 00000000..f9b80b7e --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/config/PerformanceMonitorBeanDefinitionDecorator.java @@ -0,0 +1,95 @@ +/* PerformanceMonitorBeanDefinitionDecorator.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.config; + +import org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.xml.BeanDefinitionDecorator; +import org.springframework.beans.factory.xml.ParserContext; +import org.w3c.dom.Attr; +import org.w3c.dom.Node; + +/** + * This class is invoked when Spring encounters the jrugged:perfmon attribute. + * If the attribute is set to true, then it registers a BeanNameAutoProxyCreator + * that gets associated with the SingleServiceWrapperInterceptor created by + * the jrugged:methods attribute. + */ +public class PerformanceMonitorBeanDefinitionDecorator implements + BeanDefinitionDecorator { + + /** + * Method called by Spring when it encounters the jrugged:perfmon attribute. + * Checks if the attribute is true, and if so, it registers a proxy for the + * bean. + */ + public BeanDefinitionHolder decorate(Node source, + BeanDefinitionHolder holder, + ParserContext context) { + + boolean enabled = getBooleanAttributeValue(source); + if (enabled) { + registerProxyCreator(source, holder, context); + } + + return holder; + } + + /** + * Gets the value of an attribute and returns true if it is set to "true" + * (case-insensitive), otherwise returns false. + * + * @param source An Attribute node from the spring configuration + * + * @return boolean + */ + private boolean getBooleanAttributeValue(Node source) { + Attr attribute = (Attr)source; + String value = attribute.getValue(); + return "true".equalsIgnoreCase(value); + } + + /** + * Registers a BeanNameAutoProxyCreator class that wraps the bean being + * monitored. The proxy is associated with the PerformanceMonitorInterceptor + * for the bean, which is created when parsing the methods attribute from + * the springconfiguration xml file. + * + * @param source An Attribute node from the spring configuration + * @param holder A container for the beans I will create + * @param context the context currently parsing my spring config + */ + private void registerProxyCreator(Node source, + BeanDefinitionHolder holder, + ParserContext context) { + + String beanName = holder.getBeanName(); + String proxyName = beanName + "Proxy"; + String interceptorName = beanName + "PerformanceMonitorInterceptor"; + + BeanDefinitionBuilder initializer = + BeanDefinitionBuilder.rootBeanDefinition(BeanNameAutoProxyCreator.class); + + initializer.addPropertyValue("beanNames", beanName); + initializer.addPropertyValue("interceptorNames", interceptorName); + + BeanDefinitionRegistry registry = context.getRegistry(); + registry.registerBeanDefinition(proxyName, initializer.getBeanDefinition()); + } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/config/PerformanceMonitorBeanDefinitionParser.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/config/PerformanceMonitorBeanDefinitionParser.java new file mode 100644 index 00000000..9030149d --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/config/PerformanceMonitorBeanDefinitionParser.java @@ -0,0 +1,46 @@ +/* PerformanceMonitorBeanDefinitionParser.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.config; + +import org.fishwife.jrugged.spring.PerformanceMonitorBean; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; + +import org.w3c.dom.Element; + +/** + * Simple BeanDefinitionParser that creates beans that are instances of the + * PerformanceMonitorBean class. Spring already provides an + * AbstractSingleBeanDefinitionParser that handles most of the work to do this. + */ +public class PerformanceMonitorBeanDefinitionParser extends + AbstractSingleBeanDefinitionParser { + + /** + * Return the class to instantiate. In this case it is PerformanceMonitorBean. + */ + protected Class getBeanClass(Element element) { + return PerformanceMonitorBean.class; + } + + /** + * Disables lazy loading of the bean. + */ + protected void doParse(Element element, BeanDefinitionBuilder bean) { + bean.setLazyInit(false); + } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/config/SingleServiceWrapperInterceptor.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/config/SingleServiceWrapperInterceptor.java new file mode 100644 index 00000000..29df7de9 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/config/SingleServiceWrapperInterceptor.java @@ -0,0 +1,118 @@ +/* SingleServiceWrapperInterceptor.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.config; + +import java.util.List; +import java.util.concurrent.Callable; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.fishwife.jrugged.ServiceWrapper; + +/** + * Rework of the ServiceWrapperInterceptor that uses a list of methods instead + * of a map and only supports one ServiceWrapper. + */ +public class SingleServiceWrapperInterceptor implements MethodInterceptor { + + private List methodList; + private ServiceWrapper serviceWrapper; + + /** See if the given method invocation is one that needs to be + * called through a {@link ServiceWrapper}, and if so, do so. + * @param invocation the {@link MethodInvocation} in question + * @return whatever the underlying method call would normally + * return + * @throws Throwable that the method call would generate, or + * that the {@link ServiceWrapper} would generate when tripped. + */ + public Object invoke(final MethodInvocation invocation) throws Throwable { + String methodName = invocation.getMethod().getName(); + + if (!shouldWrapMethodCall(methodName)) { + return invocation.proceed(); + } + else { + ServiceWrapper wrapper = serviceWrapper; + + return wrapper.invoke(new Callable() { + public Object call() throws Exception { + try { + return invocation.proceed(); + } catch (Throwable e) { + if (e instanceof Exception) + throw (Exception) e; + else if (e instanceof Error) + throw (Error) e; + else + throw new RuntimeException(e); + } + } + }); + } + } + + /** + * Checks if the method being invoked should be wrapped by a service. + * It looks the method name up in the methodList. If its in the list, then + * the method should be wrapped. If the list is null, then all methods + * are wrapped. + * + * @param methodName The method being called + * + * @return boolean + */ + private boolean shouldWrapMethodCall(String methodName) { + if (methodList == null) { + return true; // Wrap all by default + } + + if (methodList.contains(methodName)) { + return true; //Wrap a specific method + } + + // If I get to this point, I should not wrap the call. + return false; + } + + /** + * Specifies which methods will be wrapped with the ServiceWrapper. + * + * @param methodList the methods I intend to wrap calls around + */ + public void setMethods(List methodList) { + this.methodList = methodList; + } + + /** + * Return the ServiceWrapper being used to wrap the methods. + * + * @return ServiceWrapper + */ + public ServiceWrapper getServiceWrapper() { + return serviceWrapper; + } + + /** + * Set the ServiceWrapper to wrap the methods with. + * + * @param serviceWrapper The wrapper instance from Spring config. + */ + public void setServiceWrapper(ServiceWrapper serviceWrapper) { + this.serviceWrapper = serviceWrapper; + } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/MBeanOperationInvoker.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/MBeanOperationInvoker.java new file mode 100644 index 00000000..bac03fe5 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/MBeanOperationInvoker.java @@ -0,0 +1,73 @@ +/* MBeanOperationInvoker.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.jmx; + +import javax.management.JMException; +import javax.management.MBeanOperationInfo; +import javax.management.MBeanParameterInfo; +import javax.management.MBeanServer; +import javax.management.ObjectName; +import java.util.Map; + +/** + * The MBeanOperationInvoker is used to invoke an operation on an MBean. + */ +public class MBeanOperationInvoker { + MBeanServer mBeanServer; + ObjectName objectName; + MBeanOperationInfo operationInfo; + + /** + * Constructor. + * @param mBeanServer the {@link MBeanServer}. + * @param objectName the {@link ObjectName} for the MBean. + * @param operationInfo the {@link MBeanOperationInfo} for the Operation to invoke. + */ + public MBeanOperationInvoker(MBeanServer mBeanServer, ObjectName objectName, MBeanOperationInfo operationInfo) { + this.mBeanServer = mBeanServer; + this.objectName = objectName; + this.operationInfo = operationInfo; + } + + /** + * Invoke the operation. + * @param parameterMap the {@link Map} of parameter names to value arrays. + * @return the {@link Object} return value from the operation. + * @throws JMException Java Management Exception + */ + public Object invokeOperation(Map parameterMap) throws JMException { + MBeanParameterInfo[] parameterInfoArray = operationInfo.getSignature(); + + Object[] values = new Object[parameterInfoArray.length]; + String[] types = new String[parameterInfoArray.length]; + + MBeanValueConverter valueConverter = createMBeanValueConverter(parameterMap); + + for (int parameterNum = 0; parameterNum < parameterInfoArray.length; parameterNum++) { + MBeanParameterInfo parameterInfo = parameterInfoArray[parameterNum]; + String type = parameterInfo.getType(); + types[parameterNum] = type; + values[parameterNum] = valueConverter.convertParameterValue(parameterInfo.getName(), type); + } + + return mBeanServer.invoke(objectName, operationInfo.getName(), values, types); + } + + MBeanValueConverter createMBeanValueConverter(Map parameterMap) { + return new MBeanValueConverter(parameterMap); + } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/MBeanStringSanitizer.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/MBeanStringSanitizer.java new file mode 100644 index 00000000..76dbe2a4 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/MBeanStringSanitizer.java @@ -0,0 +1,49 @@ +/* MBeanStringSanitizer.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.jmx; + +import org.springframework.web.util.HtmlUtils; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +/** + * The MBeanStringSanitizer is used to turn MBean object, attribute, and operation names and values + * into web-friendly Strings. + */ +public class MBeanStringSanitizer { + + /** + * Convert a URL Encoded name back to the original form. + * @param name the name to URL urlDecode. + * @param encoding the string encoding to be used (i.e. UTF-8) + * @return the name in original form. + * @throws UnsupportedEncodingException if the encoding is not supported. + */ + String urlDecode(String name, String encoding) throws UnsupportedEncodingException { + return URLDecoder.decode(name, encoding); + } + + /** + * Escape a value to be HTML friendly. + * @param value the Object value. + * @return the HTML-escaped String, or if the value is null. + */ + String escapeValue(Object value) { + return HtmlUtils.htmlEscape(value != null ? value.toString() : ""); + } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/MBeanValueConverter.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/MBeanValueConverter.java new file mode 100644 index 00000000..5cca3187 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/MBeanValueConverter.java @@ -0,0 +1,63 @@ +/* MBeanValueConverter.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.jmx; + +import java.util.Map; + +/** + * The MBeanValueConverter is used to convert {@link String} parameter values stored in a {@link Map} of + * parameter names to values into their native types. + */ +public class MBeanValueConverter { + + private Map parameterMap; + + /** + * Constructor. + * @param parameterMap the {@link Map} of parameter names to {@link String} values. + */ + public MBeanValueConverter(Map parameterMap) { + this.parameterMap = parameterMap; + } + + /** + * Convert the {@link String} parameter value into its native type. + * The {@link String} '<null>' is converted into a null value. + * Only types String, Boolean, Int, Long, Float, and Double are supported. + * @param parameterName the parameter name to convert. + * @param type the native type for the parameter. + * @return the converted value. + * @throws NumberFormatException the parameter is not a number + * @throws UnhandledParameterTypeException unable to recognize the parameter type + */ + public Object convertParameterValue(String parameterName, String type) + throws NumberFormatException, UnhandledParameterTypeException { + String[] valueList = parameterMap.get(parameterName); + if (valueList == null || valueList.length == 0) return null; + String value = valueList[0]; + if (value.equals("")) return null; + if (type.equals("java.lang.String")) return value; + if (type.equals("boolean")) return Boolean.parseBoolean(value); + if (type.equals("int")) return Integer.parseInt(value); + if (type.equals("long")) return Long.parseLong(value); + if (type.equals("float")) return Float.parseFloat(value); + if (type.equals("double")) return Double.parseDouble(value); + + throw new UnhandledParameterTypeException("Cannot convert " + value + " into type " + type + + " for parameter " + parameterName); + } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/OperationNotFoundException.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/OperationNotFoundException.java new file mode 100644 index 00000000..a7fdd8fc --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/OperationNotFoundException.java @@ -0,0 +1,28 @@ +/* OperationNotFoundException.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.jmx; + +import javax.management.JMException; + +/** + * Thrown when a requested Operation is not found on an MBean. + */ +class OperationNotFoundException extends JMException { + public OperationNotFoundException(String reason) { + super(reason); + } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/UnhandledParameterTypeException.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/UnhandledParameterTypeException.java new file mode 100644 index 00000000..77563525 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/UnhandledParameterTypeException.java @@ -0,0 +1,26 @@ +/* UnhandledParameterTypeException.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.jmx; + +import javax.management.JMException; + +/** + * Thrown when an unhandled parameter type is found. + */ +public class UnhandledParameterTypeException extends JMException { + public UnhandledParameterTypeException(String reason) { super(reason); } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/WebMBeanAdapter.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/WebMBeanAdapter.java new file mode 100644 index 00000000..2ad9d23b --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/WebMBeanAdapter.java @@ -0,0 +1,186 @@ +/* WebMBeanAdapter.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.jmx; + +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.AttributeNotFoundException; +import javax.management.InstanceNotFoundException; +import javax.management.JMException; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanInfo; +import javax.management.MBeanOperationInfo; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import javax.management.ReflectionException; +import java.io.UnsupportedEncodingException; +import java.util.HashSet; +import java.util.Map; +import java.util.TreeMap; + +/** + * The WebMBeanAdapter is used to query MBean Attributes and Operations using + * web-friendly names. + * + * It should be noted that by creating a web interface the JMX beans bypasses the JMX security + * mechanisms that are built into the JVM. If there is a need to limit access to the JMX + * beans then the web interface will need to be secured. + */ +public class WebMBeanAdapter { + + private MBeanServer mBeanServer; + private ObjectName objectName; + private MBeanStringSanitizer sanitizer; + private String encoding; + private MBeanInfo mBeanInfo; + + /** + * Constructor. + * @param mBeanServer the {@link MBeanServer}. + * @param mBeanName the MBean name (can be URL-encoded). + * @param encoding the string encoding to be used (i.e. UTF-8) + * @throws JMException Java Management Exception + * @throws UnsupportedEncodingException if the encoding is not supported. + */ + public WebMBeanAdapter(MBeanServer mBeanServer, String mBeanName, String encoding) + throws JMException, UnsupportedEncodingException { + this.mBeanServer = mBeanServer; + this.encoding = encoding; + sanitizer = createMBeanStringSanitizer(); + objectName = createObjectName(sanitizer.urlDecode(mBeanName, encoding)); + mBeanInfo = mBeanServer.getMBeanInfo(objectName); + } + + /** + * Get the Attribute metadata for an MBean by name. + * @return the {@link Map} of {@link String} attribute names to {@link MBeanAttributeInfo} values. + */ + public Map getAttributeMetadata() { + + MBeanAttributeInfo[] attributeList = mBeanInfo.getAttributes(); + + Map attributeMap = new TreeMap(); + for (MBeanAttributeInfo attribute: attributeList) { + attributeMap.put(attribute.getName(), attribute); + } + return attributeMap; + } + + /** + * Get the Operation metadata for an MBean by name. + * @return the {@link Map} of {@link String} operation names to {@link MBeanOperationInfo} values. + */ + public Map getOperationMetadata() { + + MBeanOperationInfo[] operations = mBeanInfo.getOperations(); + + Map operationMap = new TreeMap(); + for (MBeanOperationInfo operation: operations) { + operationMap.put(operation.getName(), operation); + } + return operationMap; + } + + /** + * Get the Operation metadata for a single operation on an MBean by name. + * @param operationName the Operation name (can be URL-encoded). + * @return the {@link MBeanOperationInfo} for the operation. + * @throws OperationNotFoundException Method was not found + * @throws UnsupportedEncodingException if the encoding is not supported. + */ + public MBeanOperationInfo getOperationInfo(String operationName) + throws OperationNotFoundException, UnsupportedEncodingException { + + String decodedOperationName = sanitizer.urlDecode(operationName, encoding); + Map operationMap = getOperationMetadata(); + if (operationMap.containsKey(decodedOperationName)) { + return operationMap.get(decodedOperationName); + } + throw new OperationNotFoundException("Could not find operation " + operationName + " on MBean " + + objectName.getCanonicalName()); + } + + /** + * Get all the attribute values for an MBean by name. The values are HTML escaped. + * @return the {@link Map} of attribute names and values. + * @throws javax.management.AttributeNotFoundException Unable to find the 'attribute' + * @throws InstanceNotFoundException unable to find the specific bean + * @throws ReflectionException unable to interrogate the bean + */ + public Map getAttributeValues() + throws AttributeNotFoundException, InstanceNotFoundException, ReflectionException { + + HashSet attributeSet = new HashSet(); + + for (MBeanAttributeInfo attributeInfo : mBeanInfo.getAttributes()) { + attributeSet.add(attributeInfo.getName()); + } + + AttributeList attributeList = + mBeanServer.getAttributes(objectName, attributeSet.toArray(new String[attributeSet.size()])); + + Map attributeValueMap = new TreeMap(); + for (Attribute attribute : attributeList.asList()) { + attributeValueMap.put(attribute.getName(), sanitizer.escapeValue(attribute.getValue())); + } + + return attributeValueMap; + } + + /** + * Get the value for a single attribute on an MBean by name. + * @param attributeName the attribute name (can be URL-encoded). + * @return the value as a String. + * @throws JMException Java Management Exception + * @throws UnsupportedEncodingException if the encoding is not supported. + */ + public String getAttributeValue(String attributeName) + throws JMException, UnsupportedEncodingException { + String decodedAttributeName = sanitizer.urlDecode(attributeName, encoding); + return sanitizer.escapeValue(mBeanServer.getAttribute(objectName, decodedAttributeName)); + } + + /** + * Invoke an operation on an MBean by name. + * Note that only basic data types are supported for parameter values. + * @param operationName the operation name (can be URL-encoded). + * @param parameterMap the {@link Map} of parameter names and value arrays. + * @return the returned value from the operation. + * @throws JMException Java Management Exception + * @throws UnsupportedEncodingException if the encoding is not supported. + */ + public String invokeOperation(String operationName, Map parameterMap) + throws JMException, UnsupportedEncodingException { + MBeanOperationInfo operationInfo = getOperationInfo(operationName); + MBeanOperationInvoker invoker = createMBeanOperationInvoker(mBeanServer, objectName, operationInfo); + return sanitizer.escapeValue(invoker.invokeOperation(parameterMap)); + } + + MBeanStringSanitizer createMBeanStringSanitizer() { + return new MBeanStringSanitizer(); + } + + ObjectName createObjectName(String name) throws MalformedObjectNameException { + return new ObjectName(name); + } + + MBeanOperationInvoker createMBeanOperationInvoker( + MBeanServer mBeanServer, ObjectName objectName, MBeanOperationInfo operationInfo) { + return new MBeanOperationInvoker(mBeanServer, objectName, operationInfo); + } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/WebMBeanServerAdapter.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/WebMBeanServerAdapter.java new file mode 100644 index 00000000..902fbae9 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/jmx/WebMBeanServerAdapter.java @@ -0,0 +1,79 @@ +/* WebMBeanServerAdapter.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.jmx; + +import javax.management.JMException; +import javax.management.MBeanServer; +import javax.management.ObjectInstance; +import java.io.UnsupportedEncodingException; +import java.util.Set; +import java.util.TreeSet; + +/** + * The WebMBeanServerAdapter provides access to MBeans managed by an {@link MBeanServer} via + * simple string-based accessor methods. This is particularly useful for implementing a + * web interface to interact with the MBeans. Names of MBeans and returned values are sanitized + * using the {@link MBeanStringSanitizer} to make them HTML-friendly. + * + * It should be noted that creating a web interface the JMX beans bypasses the JMX security + * mechanisms that are built into the JVM. If there is a need to limit access to the JMX + * beans then the web interface will need to be secured. + */ +public class WebMBeanServerAdapter { + + private MBeanServer mBeanServer; + + private MBeanStringSanitizer sanitizer; + + /** + * Constructor. + * @param mBeanServer the {@link MBeanServer}. + */ + public WebMBeanServerAdapter(MBeanServer mBeanServer) { + this.mBeanServer = mBeanServer; + sanitizer = createMBeanStringSanitizer(); + } + + /** + * Get the {@link Set} of MBean names from the {@link MBeanServer}. The names are HTML sanitized. + * @return the {@link Set} of HTML sanitized MBean names. + */ + public Set getMBeanNames() { + Set nameSet = new TreeSet(); + for (ObjectInstance instance : mBeanServer.queryMBeans(null, null)) { + nameSet.add(sanitizer.escapeValue(instance.getObjectName().getCanonicalName())); + } + return nameSet; + } + + /** + * Create a WebMBeanAdaptor for a specified MBean name. + * @param mBeanName the MBean name (can be URL-encoded). + * @param encoding the string encoding to be used (i.e. UTF-8) + * @return the created WebMBeanAdaptor. + * @throws JMException Java Management Exception + * @throws UnsupportedEncodingException if the encoding is not supported. + */ + public WebMBeanAdapter createWebMBeanAdapter(String mBeanName, String encoding) + throws JMException, UnsupportedEncodingException { + return new WebMBeanAdapter(mBeanServer, mBeanName, encoding); + } + + MBeanStringSanitizer createMBeanStringSanitizer() { + return new MBeanStringSanitizer(); + } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/package.html b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/package.html new file mode 100644 index 00000000..2790ba01 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/package.html @@ -0,0 +1,26 @@ + + + +

If you are using the Spring +framework, this package provides more convenient access to the core +Jrugged functionality.

+

Spring interceptors are provided that allow you to easily wrap +specified methods in one of the jrugged "core" +ServiceWrappers. Currently CircuitBreakers and PerformanceMonitors are +supported.

+ + diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/retry/ClassifierSimpleRetryPolicy.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/retry/ClassifierSimpleRetryPolicy.java new file mode 100644 index 00000000..04cdd809 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/retry/ClassifierSimpleRetryPolicy.java @@ -0,0 +1,107 @@ +/* Copyright 2009-2019 Comcast Interactive Media, LLC. + + 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.fishwife.jrugged.spring.retry; + +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import org.springframework.classify.Classifier; +import org.springframework.retry.RetryContext; +import org.springframework.retry.policy.SimpleRetryPolicy; + +import java.util.Collections; + +/*** + * An extension to the existing {@link SimpleRetryPolicy} to allow for using an arbitrary + * {@link Classifier} instance to determine if a given {@link Throwable} should trigger a + * retry. + */ +public class ClassifierSimpleRetryPolicy + extends SimpleRetryPolicy { + + private static final Predicate DEFAULT_PREDICATE = Predicates.alwaysFalse(); + private static final Classifier DEFAULT_CLASSIFIER = new PredicateBinaryExceptionClassifier(DEFAULT_PREDICATE); + + private volatile Classifier classifier; + + /*** + * Constructor. + * + * Uses the default values for the {@link #maxAttempts} + * Uses the default classifier, which returns false for all exceptions. + */ + public ClassifierSimpleRetryPolicy() { + this(SimpleRetryPolicy.DEFAULT_MAX_ATTEMPTS, DEFAULT_CLASSIFIER); + } + + /*** + * Constructor. + + * Uses the default classifier, which returns false for all exceptions. + * + * @param maxAttempts The maximum number of attempts allowed + */ + public ClassifierSimpleRetryPolicy(int maxAttempts) { + this(maxAttempts, DEFAULT_CLASSIFIER); + } + + /*** + * Constructor. + * + * Uses the default values for the {@link #maxAttempts} + * + * @param classifier The classifier used to determine if an exception should trigger a retry + */ + public ClassifierSimpleRetryPolicy(Classifier classifier) { + this(SimpleRetryPolicy.DEFAULT_MAX_ATTEMPTS, classifier); + } + + /*** + * Constructor. + * + * @param maxAttempts The maximum number of attempts allowed + * @param classifier The classifier used to determine if an exception should trigger a retry + */ + public ClassifierSimpleRetryPolicy(int maxAttempts, Classifier classifier) { + super(maxAttempts, Collections.EMPTY_MAP); + this.classifier = classifier; + } + + /*** + * Get the classifier instance. + * + * @return The classifier + */ + public Classifier getClassifier() { + return classifier; + } + + /*** + * Classify the exception as triggering a retry or not. + * + * @param throwable The exception which was thrown by the attempt. + * + * @return whether or not a retry should be attempted + */ + private boolean classify(Throwable throwable) { + return (classifier == null ? DEFAULT_CLASSIFIER : classifier).classify(throwable); + } + + @Override + public boolean canRetry(RetryContext context) { + Throwable t = context.getLastThrowable(); + return (t == null || classify(t)) && context.getRetryCount() < getMaxAttempts(); + } +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/retry/ExtendedPredicates.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/retry/ExtendedPredicates.java new file mode 100644 index 00000000..546aa2e9 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/retry/ExtendedPredicates.java @@ -0,0 +1,70 @@ +/* Copyright 2009-2019 Comcast Interactive Media, LLC. + + 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.fishwife.jrugged.spring.retry; + +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; + +/*** + * Additional predicates which can be useful. + * + */ +public final class ExtendedPredicates { + private ExtendedPredicates() { + super(); + } + + /*** + * Create a predicate to see if a given object is an instance of a specific class, given that + * all objects passed to this predicate will be a subclass of some parent of that class. + * + * @param superclazz The class which all objects passed to this predicate will be a subclass of + * @param clazz The class to see if the passed in object will be an instanceof + * @param The superclass + * @param The class + * @return The predicate + */ + public static Predicate isInstanceOf(final Class superclazz, final Class clazz) { + return new Predicate() { + public boolean apply(S input) { + return Predicates.instanceOf(clazz).apply(input); + } + }; + } + + /*** + * Create a predicate to check if a throwable's error message contains a specific string. + * + * @param expected The expected string + * @param caseSensitive Is the comparison going to be case sensitive + * + * @return True if the throwable's message contains the expected string. + */ + public static Predicate throwableContainsMessage(final String expected, final boolean caseSensitive) { + return new Predicate() { + public boolean apply(Throwable input) { + String actual = input.getMessage(); + String exp = expected; + if (! caseSensitive) { + actual = actual.toLowerCase(); + exp = exp.toLowerCase(); + } + return actual.contains(exp); + } + }; + } + +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/retry/ExtendedRetryTemplate.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/retry/ExtendedRetryTemplate.java new file mode 100644 index 00000000..17b9e704 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/retry/ExtendedRetryTemplate.java @@ -0,0 +1,138 @@ +/* Copyright 2009-2019 Comcast Interactive Media, LLC. + + 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.fishwife.jrugged.spring.retry; + +import java.util.concurrent.Callable; + +import org.springframework.retry.ExhaustedRetryException; +import org.springframework.retry.RecoveryCallback; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryState; +import org.springframework.retry.support.RetryTemplate; + +/*** + * Extended version of the {@link RetryTemplate} to allow easy use of the {@link Callable} + * interface, instead of the {@link RetryCallback} + */ +public class ExtendedRetryTemplate extends RetryTemplate { + + /*** + * Constructor. + */ + public ExtendedRetryTemplate() { + super(); + } + + /*** + * Construct a {@link Callable} which wraps the given {@link RetryCallback}, + * and who's {@link java.util.concurrent.Callable#call()} method will execute + * the callback via this {@link ExtendedRetryTemplate} + * + * @param callback The callback to wrap + * @param The return type of the callback + * @return The callback as a Callable + */ + public Callable asCallable(final RetryCallback callback) { + return new Callable() { + public T call() throws Exception { + return ExtendedRetryTemplate.this.execute(callback); + + } + }; + } + + /*** + * Construct a {@link Callable} which wraps the given {@link Callable}, + * and who's {@link java.util.concurrent.Callable#call()} method will execute + * the callable via this {@link ExtendedRetryTemplate} + * + * @param callable The callable to wrap + * @param The return type of the callback + * @return The callback as a Callable + */ + public Callable asCallable(final Callable callable) { + return new Callable() { + public T call() throws Exception { + return ExtendedRetryTemplate.this.execute(new RetryCallback() { + public T doWithRetry(RetryContext retryContext) throws Exception { + return callable.call(); + } + }); + } + }; + } + + /*** + * Execute a given {@link Callable} with retry logic. + * + * @param callable The callable to execute + * @param The return type of the callable + * @return The result of the callable + * @throws Exception in the event that the callable throws + * @throws ExhaustedRetryException If all retry attempts have been exhausted + */ + public T execute(final Callable callable) throws Exception, ExhaustedRetryException { + return execute(new RetryCallback() { + public T doWithRetry(RetryContext retryContext) throws Exception { + return callable.call(); + } + }); + } + + /*** + * Execute a given {@link Callable} with retry logic. + * + * @param callable The callable to execute + * @param retryState The current retryState + * @param The return type of the callable + * @return The result of the callable + * @throws Exception in the event that the callable throws + * @throws ExhaustedRetryException If all retry attempts have been exhausted + */ + public T execute(final Callable callable, RetryState retryState) throws Exception, ExhaustedRetryException { + return execute( + new RetryCallback() { + public T doWithRetry(RetryContext retryContext) throws Exception { + return callable.call(); + } + }, + retryState); + } + + /*** + * Execute a given {@link Callable} with retry logic. + * + * @param callable The callable to execute + * @param recoveryCallback The recovery callback to execute when exceptions occur + * @param retryState The current retryState + * @param The return type of the callable + * @return The result of the callable + * @throws Exception in the event that the callable throws + * @throws ExhaustedRetryException If all retry attempts have been exhausted + */ + public T execute(final Callable callable, RecoveryCallback recoveryCallback, RetryState retryState) throws Exception, ExhaustedRetryException { + return execute( + new RetryCallback() { + public T doWithRetry(RetryContext retryContext) throws Exception { + return callable.call(); + } + }, + recoveryCallback, + retryState); + } + +} diff --git a/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/retry/PredicateBinaryExceptionClassifier.java b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/retry/PredicateBinaryExceptionClassifier.java new file mode 100644 index 00000000..fa3c0414 --- /dev/null +++ b/jrugged-spring-V5/src/main/java/org/fishwife/jrugged/spring/retry/PredicateBinaryExceptionClassifier.java @@ -0,0 +1,62 @@ +/* Copyright 2009-2019 Comcast Interactive Media, LLC. + + 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.fishwife.jrugged.spring.retry; + +import com.google.common.base.Predicate; +import org.springframework.classify.ClassifierSupport; + +/*** + * A {@link Predicate} based classifier for {@link Throwable} objects which classifies them + * as boolean values. + */ +public class PredicateBinaryExceptionClassifier extends ClassifierSupport { + + private Predicate predicate; + + /*** + * Constructor. + * + * @param predicate The predicate to use to check the exception + */ + public PredicateBinaryExceptionClassifier(Predicate predicate) { + super(Boolean.TRUE); + this.predicate = predicate; + } + + /*** + * Get the predicate that is in use. + * + * @return the predicate + */ + public Predicate getPredicate() { + return predicate; + } + + /*** + * Set the predicate that is in use + * @param predicate the predicate + */ + public void setPredicate(Predicate predicate) { + this.predicate = predicate; + } + + @Override + public Boolean classify(Throwable classifiable) { + return predicate.apply(classifiable); + } + + +} diff --git a/jrugged-spring-V5/src/main/resources/LICENSE b/jrugged-spring-V5/src/main/resources/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/jrugged-spring-V5/src/main/resources/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/jrugged-spring-V5/src/main/resources/META-INF/spring.handlers b/jrugged-spring-V5/src/main/resources/META-INF/spring.handlers new file mode 100644 index 00000000..5d515ddf --- /dev/null +++ b/jrugged-spring-V5/src/main/resources/META-INF/spring.handlers @@ -0,0 +1 @@ +http\://www.fishwife.org/schema/jrugged=org.fishwife.jrugged.spring.config.JRuggedNamespaceHandler \ No newline at end of file diff --git a/jrugged-spring-V5/src/main/resources/META-INF/spring.schemas b/jrugged-spring-V5/src/main/resources/META-INF/spring.schemas new file mode 100644 index 00000000..fed8c2a5 --- /dev/null +++ b/jrugged-spring-V5/src/main/resources/META-INF/spring.schemas @@ -0,0 +1 @@ +http\://www.fishwife.org/schema/jrugged/jrugged.xsd=jrugged.xsd \ No newline at end of file diff --git a/jrugged-spring-V5/src/main/resources/NOTICE b/jrugged-spring-V5/src/main/resources/NOTICE new file mode 100644 index 00000000..1ce3ecd4 --- /dev/null +++ b/jrugged-spring-V5/src/main/resources/NOTICE @@ -0,0 +1,5 @@ +JRugged Java Library +Copyright 2009-2014 Comcast Interactive Media, LLC. + +This product includes software developed by +Jonathan T. Moore . diff --git a/jrugged-spring-V5/src/main/resources/jrugged.xsd b/jrugged-spring-V5/src/main/resources/jrugged.xsd new file mode 100644 index 00000000..d611f833 --- /dev/null +++ b/jrugged-spring-V5/src/main/resources/jrugged.xsd @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/MonitoredServiceStub.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/MonitoredServiceStub.java new file mode 100644 index 00000000..99c12146 --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/MonitoredServiceStub.java @@ -0,0 +1,36 @@ +/* MonitorableStub.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring; + +import org.fishwife.jrugged.MonitoredService; +import org.fishwife.jrugged.ServiceStatus; +import org.fishwife.jrugged.Status; + +public class MonitoredServiceStub implements MonitoredService { + + private static final String NAME = "ServiceStub"; + private ServiceStatus status = new ServiceStatus(NAME, Status.UP); + + public void setStatus(Status status) { + this.status = new ServiceStatus(NAME, status); + } + + public ServiceStatus getServiceStatus() { + return status; + } + +} \ No newline at end of file diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/TestAnnotatedMethodFilter.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/TestAnnotatedMethodFilter.java new file mode 100644 index 00000000..a1584906 --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/TestAnnotatedMethodFilter.java @@ -0,0 +1,105 @@ +/* ServiceWrapper.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.HashSet; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.MethodMetadata; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; + +public class TestAnnotatedMethodFilter { + + private MetadataReader mockMetadataReader; + private MetadataReaderFactory mockMetadataReaderFactory; + private AnnotationMetadata mockAnnotationMetadata; + private MethodMetadata mockMethodMetadata; + + private AnnotatedMethodFilter impl; + + @Before + public void setUp() { + impl = new AnnotatedMethodFilter(SomeAnno.class); + mockMetadataReader = createMock(MetadataReader.class); + mockMetadataReaderFactory = createMock(MetadataReaderFactory.class); + mockAnnotationMetadata = createMock(AnnotationMetadata.class); + mockMethodMetadata = createMock(MethodMetadata.class); + + expect(mockMetadataReader.getAnnotationMetadata()).andReturn(mockAnnotationMetadata); + } + + @Test + public void testMatchReturnsFalseIfNoAnnotatedMethodsFound() throws IOException { + Set foundMethods = new HashSet(); + + expect(mockAnnotationMetadata.getAnnotatedMethods(SomeAnno.class.getCanonicalName())).andReturn(foundMethods); + + replayMocks(); + assertFalse(impl.match(mockMetadataReader, mockMetadataReaderFactory)); + verifyMocks(); + } + + @Test + public void testMatchReturnsFalseIfAnnotatedMethodsFound() throws IOException { + Set foundMethods = new HashSet(); + foundMethods.add(mockMethodMetadata); + + expect(mockAnnotationMetadata.getAnnotatedMethods(SomeAnno.class.getCanonicalName())).andReturn(foundMethods); + + replayMocks(); + assertTrue(impl.match(mockMetadataReader, mockMetadataReaderFactory)); + verifyMocks(); + } + + void replayMocks() { + replay(mockMetadataReader); + replay(mockMetadataReaderFactory); + replay(mockAnnotationMetadata); + replay(mockMethodMetadata); + } + + void verifyMocks() { + verify(mockMetadataReader); + verify(mockMetadataReaderFactory); + verify(mockAnnotationMetadata); + verify(mockMethodMetadata); + } + + // Dummy anno for test + @Target({ElementType.METHOD, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + private @interface SomeAnno { + + } + +} diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/TestAnnotatedMethodScanner.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/TestAnnotatedMethodScanner.java new file mode 100644 index 00000000..3e081a8a --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/TestAnnotatedMethodScanner.java @@ -0,0 +1,87 @@ +/* TestAnnotatedMethodScanner.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.easymock.EasyMock.eq; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.HashSet; +import java.util.Set; + +import org.easymock.EasyMock; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; + +public class TestAnnotatedMethodScanner { + + private ClassLoader mockClassLoader; + private ClassPathScanningCandidateComponentProvider mockProvider; + + private AnnotatedMethodScanner impl; + + @Before + public void setUp() { + mockClassLoader = createMock(ClassLoader.class); + mockProvider = createMock(ClassPathScanningCandidateComponentProvider.class); + impl = new AnnotatedMethodScanner(mockClassLoader, mockProvider); + } + + @Test + public void testFindCandidateBeansAppliesAnnotatedMethodFilter() { + String basePackage = "faux.package"; + Set filteredComponents = new HashSet(); + + mockProvider.resetFilters(false); + expectLastCall(); + mockProvider.addIncludeFilter(EasyMock.isA(AnnotatedMethodFilter.class)); + expectLastCall(); + + expect(mockProvider.findCandidateComponents(eq(basePackage.replace('.', '/')))). + andReturn(filteredComponents); + + replayMocks(); + impl.findCandidateBeans(basePackage, SomeAnno.class); + verifyMocks(); + } + + void replayMocks() { + replay(mockClassLoader); + replay(mockProvider); + } + + void verifyMocks() { + verify(mockClassLoader); + verify(mockProvider); + } + + // Dummy anno for test + @Target({ElementType.METHOD, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + private @interface SomeAnno { + + } +} diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/TestCircuitBreakerBean.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/TestCircuitBreakerBean.java new file mode 100644 index 00000000..8e2d1b7d --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/TestCircuitBreakerBean.java @@ -0,0 +1,65 @@ +/* TestCircuitBreakerBean.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring; + +import static org.junit.Assert.*; + +import java.util.concurrent.Callable; + +import org.fishwife.jrugged.CircuitBreakerException; +import org.junit.Before; +import org.junit.Test; + +public class TestCircuitBreakerBean { + + private CircuitBreakerBean impl; + private final Object out = new Object(); + private Callable call; + + @Before + public void setUp() { + impl = new CircuitBreakerBean(); + call = new Callable() { + public Object call() throws Exception { + return out; + } + }; + } + + @Test + public void startsEnabled() throws Exception { + assertSame(out, impl.invoke(call)); + } + + @Test + public void isenabledIfConfiguredAsNotDisabled() throws Exception { + impl.setDisabled(false); + impl.afterPropertiesSet(); + assertSame(out, impl.invoke(call)); + } + + @Test + public void canBeDisabled() throws Exception { + impl.setDisabled(true); + impl.afterPropertiesSet(); + try { + impl.invoke(call); + fail("Should have thrown CircuitBreakerException"); + } catch (CircuitBreakerException cbe) { + } + } +} diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/TestCircuitBreakerBeanFactory.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/TestCircuitBreakerBeanFactory.java new file mode 100644 index 00000000..3fb08524 --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/TestCircuitBreakerBeanFactory.java @@ -0,0 +1,144 @@ +/* Copyright 2009-2019 Comcast Interactive Media, LLC. + + 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.fishwife.jrugged.spring; + +import org.easymock.EasyMock; +import org.fishwife.jrugged.CircuitBreaker; +import org.fishwife.jrugged.CircuitBreakerConfig; +import org.fishwife.jrugged.DefaultFailureInterpreter; +import org.junit.Before; +import org.junit.Test; +import org.springframework.jmx.export.MBeanExporter; +import org.springframework.test.util.ReflectionTestUtils; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import java.util.concurrent.ConcurrentHashMap; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertSame; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.replay; + +public class TestCircuitBreakerBeanFactory { + + private CircuitBreakerBeanFactory factory; + private CircuitBreakerConfig config; + + MBeanExporter mockMBeanExporter; + + @Before + public void setUp() { + factory = new CircuitBreakerBeanFactory(); + config = new CircuitBreakerConfig(10000L, new DefaultFailureInterpreter(5, 30000L)); + mockMBeanExporter = createMock(MBeanExporter.class); + mockMBeanExporter.registerManagedResource(EasyMock.anyObject(), EasyMock.anyObject()); + replay(mockMBeanExporter); + } + + @Test + public void testCreateCircuitBreaker() { + CircuitBreaker createdBreaker = factory.createCircuitBreaker("testCreate", config); + assertNotNull(createdBreaker); + } + + @Test + public void testCreateDuplicateCircuitBreaker() { + String name = "testCreate"; + CircuitBreaker createdBreaker = factory.createCircuitBreaker(name, config); + CircuitBreaker secondBreaker = factory.createCircuitBreaker(name, config); + + assertSame(createdBreaker, secondBreaker); + } + + @Test + public void testFindCircuitBreakerBean() { + String breakerName = "testFind"; + CircuitBreaker createdBreaker = factory.createCircuitBreaker(breakerName, config); + CircuitBreakerBean foundBreaker = factory.findCircuitBreakerBean(breakerName); + assertNotNull(foundBreaker); + assertEquals(createdBreaker, foundBreaker); + } + + @Test + public void testFindInvalidCircuitBreakerBean() { + String breakerName = "testFindInvalid"; + + // Create a map with an invalid CircuitBreaker (non-bean) in it, and jam it in. + ConcurrentHashMap invalidMap = new ConcurrentHashMap(); + invalidMap.put(breakerName, new CircuitBreaker()); + ReflectionTestUtils.setField(factory, "circuitBreakerMap", invalidMap); + + // Try to find it. + CircuitBreakerBean foundBreaker = factory.findCircuitBreakerBean(breakerName); + assertNull(foundBreaker); + } + + @Test + public void testCreatePerformanceMonitorObjectName() throws MalformedObjectNameException, NullPointerException { + mockMBeanExporter = createMock(MBeanExporter.class); + ObjectName objectName = new ObjectName( + "org.fishwife.jrugged.spring:type=" + + "CircuitBreakerBean,name=testCreate"); + mockMBeanExporter.registerManagedResource(EasyMock.anyObject(), EasyMock.eq(objectName)); + replay(mockMBeanExporter); + + factory.setMBeanExportOperations(mockMBeanExporter); + factory.createCircuitBreaker("testCreate", config); + EasyMock.verify(mockMBeanExporter); + } + + @Test + public void testBreakerWithoutMBeanExporter() { + factory.setMBeanExportOperations(null); + CircuitBreaker createdBreaker = factory.createCircuitBreaker("testCreateWithoutMBeanExporter", config); + assertNotNull(createdBreaker); + } + + @Test + public void testBreakerWithMBeanExporter() { + factory.setMBeanExportOperations(mockMBeanExporter); + CircuitBreaker createdBreaker = factory.createCircuitBreaker("testCreateWithoutMBeanExporter", config); + assertNotNull(createdBreaker); + } + + @Test(expected = IllegalArgumentException.class) + public void testBreakerWithInvalidName() { + factory.setMBeanExportOperations(mockMBeanExporter); + factory.createCircuitBreaker("=\"", config); + } + + //--------- + // The following tests depend on org.fishwife.jrugged.spring.testonly + + @Test + public void testFactoryFindsCircuitBreakers() { + factory.setPackageScanBase("org.fishwife.jrugged.spring.testonly"); + factory.buildAnnotatedCircuitBreakers(); + + assertNotNull(factory.findCircuitBreaker("breakerA")); + assertNotNull(factory.findCircuitBreaker("breakerB")); + } + + @Test + public void testWhenPackageScanNotProvidedAnnotationsNotLoaded() { + factory.buildAnnotatedCircuitBreakers(); + assertNull(factory.findCircuitBreaker("breakerA")); + assertNull(factory.findCircuitBreaker("breakerB")); + } + +} diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/TestPerformanceMonitorBeanFactory.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/TestPerformanceMonitorBeanFactory.java new file mode 100644 index 00000000..857923b4 --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/TestPerformanceMonitorBeanFactory.java @@ -0,0 +1,187 @@ +/* Copyright 2009-2019 Comcast Interactive Media, LLC. + + 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.fishwife.jrugged.spring; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertSame; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.replay; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.easymock.EasyMock; +import org.fishwife.jrugged.PerformanceMonitor; +import org.junit.Before; +import org.junit.Test; +import org.springframework.jmx.export.MBeanExporter; +import org.springframework.test.util.ReflectionTestUtils; + +public class TestPerformanceMonitorBeanFactory { + + private PerformanceMonitorBeanFactory factory; + + private MBeanExporter mockMBeanExporter; + + @Before + public void setUp() { + factory = new PerformanceMonitorBeanFactory(); + } + + @Test + public void testCreateWithInitialPerformanceMonitors() { + String name1 = "test1"; + String name2 = "test2"; + List nameList = new ArrayList(); + nameList.add(name1); + nameList.add(name2); + + factory.setInitialPerformanceMonitors(null); + factory.createInitialPerformanceMonitors(); + + factory.setInitialPerformanceMonitors(nameList); + factory.createInitialPerformanceMonitors(); + + assertNotNull(factory.findPerformanceMonitor(name1)); + assertNotNull(factory.findPerformanceMonitor(name2)); + } + + @Test + public void testCreatePerformanceMonitor() { + PerformanceMonitor createdMonitor = + factory.createPerformanceMonitor("testCreate"); + assertNotNull(createdMonitor); + } + + @Test + public void testCreatePerformanceMonitorObjectName() throws MalformedObjectNameException, NullPointerException { + mockMBeanExporter = createMock(MBeanExporter.class); + ObjectName objectName = new ObjectName( + "org.fishwife.jrugged.spring:type=" + + "PerformanceMonitorBean,name=testCreate"); + mockMBeanExporter.registerManagedResource(EasyMock.anyObject(), EasyMock.eq(objectName)); + replay(mockMBeanExporter); + + factory.setMBeanExportOperations(mockMBeanExporter); + factory.createPerformanceMonitor("testCreate"); + EasyMock.verify(mockMBeanExporter); + } + + @Test + public void testCreateDuplicatePerformanceMonitor() { + String name = "testCreate"; + PerformanceMonitor createdMonitor = + factory.createPerformanceMonitor(name); + PerformanceMonitor secondMonitor = + factory.createPerformanceMonitor(name); + assertSame(createdMonitor, secondMonitor); + } + + @Test + public void testFindPerformanceMonitorBean() { + String monitorName = "testFind"; + PerformanceMonitor createdMonitor = + factory.createPerformanceMonitor(monitorName); + PerformanceMonitorBean foundMonitor = + factory.findPerformanceMonitorBean(monitorName); + assertNotNull(foundMonitor); + assertEquals(createdMonitor, foundMonitor); + } + + @Test + public void testFindInvalidPerformanceMonitorBean() { + String monitorName = "testFindInvalid"; + + // Create a map with an invalid PerformanceMonitor (non-bean) in it, + // and jam it in. + Map invalidMap = + new HashMap(); + invalidMap.put(monitorName, new PerformanceMonitor()); + ReflectionTestUtils.setField( + factory, "performanceMonitorMap", invalidMap); + + // Try to find it. + PerformanceMonitorBean foundMonitor = + factory.findPerformanceMonitorBean(monitorName); + assertNull(foundMonitor); + } + @Test + public void testMonitorWithoutMBeanExporter() { + factory.setMBeanExportOperations(null); + PerformanceMonitor createdMonitor = + factory.createPerformanceMonitor( + "testCreateWithoutMBeanExporter"); + assertNotNull(createdMonitor); + } + + @Test + public void testMonitorWithMBeanExporter() { + mockMBeanExporter = createMock(MBeanExporter.class); + mockMBeanExporter.registerManagedResource( + EasyMock.anyObject(), EasyMock.anyObject()); + replay(mockMBeanExporter); + + factory.setMBeanExportOperations(mockMBeanExporter); + PerformanceMonitor createdMonitor = + factory.createPerformanceMonitor( + "testCreateWithoutMBeanExporter"); + assertNotNull(createdMonitor); + } + + @Test(expected=IllegalArgumentException.class) + public void testMonitorWithInvalidName() { + mockMBeanExporter = createMock(MBeanExporter.class); + mockMBeanExporter.registerManagedResource( + EasyMock.anyObject(), EasyMock.anyObject()); + replay(mockMBeanExporter); + + factory.setMBeanExportOperations(mockMBeanExporter); + factory.createPerformanceMonitor("=\""); + } + + + //--------- + // The following tests depend on org.fishwife.jrugged.spring.testonly + + @Test + public void testFactorySeededWithPackageScanBaseFindsMonitors() { + factory.setPackageScanBase("org.fishwife.jrugged.spring.testonly"); + factory.createInitialPerformanceMonitors(); + + assertNotNull(factory.findPerformanceMonitor("monitorA")); + assertNotNull(factory.findPerformanceMonitor("monitorB")); + } + + // the idea here is that the monitors are created and no exceptions occur + @Test + public void testPackageScanStyleAndInitialMonitorStylePlayNice() { + List initialPerformanceMonitors = new ArrayList(); + initialPerformanceMonitors.add("monitorA"); + + factory.setPackageScanBase("org.fishwife.jrugged.spring.testonly"); + factory.setInitialPerformanceMonitors(initialPerformanceMonitors); + factory.createInitialPerformanceMonitors(); + + assertNotNull(factory.findPerformanceMonitor("monitorA")); + assertNotNull(factory.findPerformanceMonitor("monitorB")); + } +} diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/TestStatusController.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/TestStatusController.java new file mode 100644 index 00000000..860336f0 --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/TestStatusController.java @@ -0,0 +1,124 @@ +/* TestStatusController.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring; + +import static org.junit.Assert.*; + +import org.fishwife.jrugged.Status; +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + + +public class TestStatusController { + + private MonitoredServiceStub monitoredService; + private StatusController impl; + private MockHttpServletRequest req; + private MockHttpServletResponse resp; + + @Before + public void setUp() { + monitoredService = new MonitoredServiceStub(); + impl = new StatusController(monitoredService); + req = new MockHttpServletRequest(); + resp = new MockHttpServletResponse(); + } + + private void assertResponseCodeIs(Status status, int code) throws Exception { + monitoredService.setStatus(status); + impl.handleRequest(req, resp); + assertEquals(code, resp.getStatus()); + } + + private void assertBodyForStatusIs(Status status, String bodyString) + throws Exception { + monitoredService.setStatus(status); + impl.handleRequest(req, resp); + assertEquals(bodyString, resp.getContentAsString()); + assertEquals("text/plain;charset=utf-8", resp.getHeader("Content-Type")); + assertEquals(bodyString.getBytes().length + "", resp.getHeader("Content-Length")); + } + + @Test + public void handlesRequestInternally() throws Exception { + assertNull(impl.handleRequest(req, resp)); + } + + @Test + public void returns200IfStatusIsUp() throws Exception { + assertResponseCodeIs(Status.UP, 200); + } + + @Test + public void returns503IfStatusIsDown() throws Exception { + assertResponseCodeIs(Status.DOWN, 503); + } + + @Test + public void returns200IfStatusIsDegraded() throws Exception { + assertResponseCodeIs(Status.DEGRADED, 200); + } + + @Test + public void setsWarningHeaderIfDegraded() throws Exception { + monitoredService.setStatus(Status.DEGRADED); + impl.handleRequest(req, resp); + boolean found = false; + for(Object val : resp.getHeaders("Warning")) { + if ("199 jrugged \"Status degraded\"".equals(val)) { + found = true; + } + } + assertTrue(found); + } + + @Test + public void returns200IfStatusIsBypass() throws Exception { + assertResponseCodeIs(Status.BYPASS, 200); + } + + @Test + public void returns500IfStatusIsFailed() throws Exception { + assertResponseCodeIs(Status.FAILED, 500); + } + + @Test + public void returns503IfStatusIsInit() throws Exception { + assertResponseCodeIs(Status.INIT, 503); + } + + @Test + public void writesStatusOutInResponseBodyWhenUp() throws Exception { + assertBodyForStatusIs(Status.UP, "UP\n"); + } + + @Test + public void writesStatusOutInResponseBodyWhenDown() throws Exception { + assertBodyForStatusIs(Status.DOWN, "DOWN\n"); + } + + @Test + public void setsNonCacheableHeaders() throws Exception { + impl.handleRequest(req,resp); + assertNotNull(resp.getHeader("Expires")); + assertEquals(resp.getHeader("Date"), resp.getHeader("Expires")); + assertEquals("no-cache", resp.getHeader("Cache-Control")); + } + +} diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/aspects/RetryTemplateAspectTest.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/aspects/RetryTemplateAspectTest.java new file mode 100644 index 00000000..4a3feb69 --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/aspects/RetryTemplateAspectTest.java @@ -0,0 +1,175 @@ +package org.fishwife.jrugged.spring.aspects; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.Signature; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanNotOfRequiredTypeException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.retry.RecoveryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.policy.SimpleRetryPolicy; + +import java.util.HashMap; +import java.util.Map; + +@RunWith(MockitoJUnitRunner.class) +public class RetryTemplateAspectTest { + + private static final String TEST_RETRY_TEMPLATE = "TestRetryTemplate"; + private static final String TEST_RETRY_TEMPLATE_RECOVERY = "TestRetryTemplateRecovery"; + + private RetryTemplateAspect aspect; + + @Mock + private RetryTemplate mockAnnotation; + + @Mock + private Signature mockSignature; + + @Mock + private BeanFactory beanFactory; + + @Mock + private ProceedingJoinPoint mockPjp; + + @Mock + private RecoveryCallback recoveryCallback; + + @Before + public void setUp() { + aspect = new RetryTemplateAspect(); + aspect.setBeanFactory(beanFactory); + Mockito.doReturn("Signature").when(mockSignature).getName(); + Mockito.doReturn(TEST_RETRY_TEMPLATE).when(mockAnnotation).name(); + Mockito.doReturn(TEST_RETRY_TEMPLATE_RECOVERY).when(mockAnnotation).recoveryCallbackName(); + Mockito.doReturn("Target").when(mockPjp).getTarget(); + Mockito.doReturn(mockSignature).when(mockPjp).getSignature(); + } + + @Test(expected = NoSuchBeanDefinitionException.class) + public void testRetryWithMissingBean() throws Throwable { + Mockito.doThrow(new NoSuchBeanDefinitionException("")).when(beanFactory) + .getBean(TEST_RETRY_TEMPLATE, org.springframework.retry.support.RetryTemplate.class); + try { + aspect.retry(mockPjp, mockAnnotation); + } + finally { + Mockito.verify(mockPjp, Mockito.never()).proceed(); + } + } + + @Test(expected = BeanNotOfRequiredTypeException.class) + public void testRetryWithWrongBeanType() throws Throwable { + Mockito.doThrow(new BeanNotOfRequiredTypeException("", String.class, String.class)).when(beanFactory) + .getBean(TEST_RETRY_TEMPLATE, org.springframework.retry.support.RetryTemplate.class); + try { + aspect.retry(mockPjp, mockAnnotation); + } + finally { + Mockito.verify(mockPjp, Mockito.never()).proceed(); + } + } + + + @Test + public void testRetry() throws Throwable { + org.springframework.retry.support.RetryTemplate template = + new org.springframework.retry.support.RetryTemplate(); + Mockito.doReturn(template).when(beanFactory) + .getBean(TEST_RETRY_TEMPLATE, org.springframework.retry.support.RetryTemplate.class); + Mockito.doReturn("a").when(mockPjp).proceed(); + Assert.assertEquals("a", aspect.retry(mockPjp, mockAnnotation)); + Mockito.verify(mockPjp, Mockito.times(1)).proceed(); + } + + @Test + public void testRetryExceptionWithRecovery() throws Throwable { + Mockito.doReturn(TEST_RETRY_TEMPLATE_RECOVERY).when(mockAnnotation).recoveryCallbackName(); + + org.springframework.retry.support.RetryTemplate template = + new org.springframework.retry.support.RetryTemplate(); + Map, Boolean> exceptionMap = new HashMap, Boolean>(); + exceptionMap.put(RuntimeException.class, Boolean.TRUE); + template.setRetryPolicy(new SimpleRetryPolicy(1, exceptionMap)); + + Mockito.doReturn(template).when(beanFactory) + .getBean(TEST_RETRY_TEMPLATE, org.springframework.retry.support.RetryTemplate.class); + Mockito.doReturn(recoveryCallback).when(beanFactory) + .getBean(TEST_RETRY_TEMPLATE_RECOVERY, RecoveryCallback.class); + Mockito.doThrow(new RuntimeException()).when(mockPjp).proceed(); + Mockito.doReturn("a").when(recoveryCallback).recover(Mockito.any(RetryContext.class)); + Assert.assertEquals("a", aspect.retry(mockPjp, mockAnnotation)); + Mockito.verify(mockPjp, Mockito.times(1)).proceed(); + } + + @Test(expected=RuntimeException.class) + public void testRetryExceptionWithoutRecovery() throws Throwable { + org.springframework.retry.support.RetryTemplate template = + new org.springframework.retry.support.RetryTemplate(); + Map, Boolean> exceptionMap = new HashMap, Boolean>(); + exceptionMap.put(RuntimeException.class, Boolean.TRUE); + template.setRetryPolicy(new SimpleRetryPolicy(1, exceptionMap)); + + Mockito.doReturn(template).when(beanFactory) + .getBean(TEST_RETRY_TEMPLATE, org.springframework.retry.support.RetryTemplate.class); + Mockito.doReturn(null).when(beanFactory) + .getBean(TEST_RETRY_TEMPLATE_RECOVERY, RecoveryCallback.class); + Mockito.doThrow(new RuntimeException()).when(mockPjp).proceed(); + try { + aspect.retry(mockPjp, mockAnnotation); + } + finally { + Mockito.verify(mockPjp, Mockito.times(1)).proceed(); + } + } + + @Test(expected=OutOfMemoryError.class) + public void testRetryErrorWithoutRecovery() throws Throwable { + org.springframework.retry.support.RetryTemplate template = + new org.springframework.retry.support.RetryTemplate(); + Map, Boolean> exceptionMap = new HashMap, Boolean>(); + exceptionMap.put(RuntimeException.class, Boolean.TRUE); + template.setRetryPolicy(new SimpleRetryPolicy(1, exceptionMap)); + + Mockito.doReturn(template).when(beanFactory) + .getBean(TEST_RETRY_TEMPLATE, org.springframework.retry.support.RetryTemplate.class); + Mockito.doReturn(null).when(beanFactory) + .getBean(TEST_RETRY_TEMPLATE_RECOVERY, RecoveryCallback.class); + Mockito.doThrow(new OutOfMemoryError()).when(mockPjp).proceed(); + try { + aspect.retry(mockPjp, mockAnnotation); + } + finally { + Mockito.verify(mockPjp, Mockito.times(1)).proceed(); + } + } + + @Test(expected=RuntimeException.class) + public void testRetryThrowableWithoutRecovery() throws Throwable { + org.springframework.retry.support.RetryTemplate template = + new org.springframework.retry.support.RetryTemplate(); + Map, Boolean> exceptionMap = new HashMap, Boolean>(); + exceptionMap.put(RuntimeException.class, Boolean.TRUE); + template.setRetryPolicy(new SimpleRetryPolicy(1, exceptionMap)); + + Mockito.doReturn(template).when(beanFactory) + .getBean(TEST_RETRY_TEMPLATE, org.springframework.retry.support.RetryTemplate.class); + Mockito.doReturn(null).when(beanFactory) + .getBean(TEST_RETRY_TEMPLATE_RECOVERY, RecoveryCallback.class); + Mockito.doThrow(new Throwable("")).when(mockPjp).proceed(); + try { + aspect.retry(mockPjp, mockAnnotation); + } + finally { + Mockito.verify(mockPjp, Mockito.times(1)).proceed(); + } + } + +} diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/config/DummyService.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/config/DummyService.java new file mode 100644 index 00000000..d8e1e8f9 --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/config/DummyService.java @@ -0,0 +1,33 @@ +/* DummyService.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.config; + + +public class DummyService { + + public String foo() { + return "Hello World!"; + } + + public String bar() { + return "Wazzzzup!"; + } + + public String baz() { + return "Hey Everybody!"; + } +} diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/config/MonitorMethodInterceptorDefinitionDecoratorTest.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/config/MonitorMethodInterceptorDefinitionDecoratorTest.java new file mode 100644 index 00000000..7d41b99b --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/config/MonitorMethodInterceptorDefinitionDecoratorTest.java @@ -0,0 +1,75 @@ +/* MonitorMethodInterceptorDefinitionDecoratorTest.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.config; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class MonitorMethodInterceptorDefinitionDecoratorTest { + + private MonitorMethodInterceptorDefinitionDecorator decorator; + + @Before + public void setUp() { + decorator = new MonitorMethodInterceptorDefinitionDecorator(); + } + + @Test + public void testParseMethodListNoName() { + + List methods = decorator.parseMethodList(""); + + Assert.assertNotNull(methods); + Assert.assertTrue(methods.size() == 0); + } + + @Test + public void testParseMethodListOneName() { + + List methods = decorator.parseMethodList("foo"); + + Assert.assertNotNull(methods); + Assert.assertTrue(methods.size() == 1); + Assert.assertTrue(methods.contains("foo")); + } + + @Test + public void testParseMethodListTwoNames() { + + List methods = decorator.parseMethodList("foo, bar"); + + Assert.assertNotNull(methods); + Assert.assertTrue(methods.size() == 2); + Assert.assertTrue(methods.contains("foo")); + Assert.assertTrue(methods.contains("bar")); + } + + @Test + public void testParseMethodListThreeNames() { + + List methods = decorator.parseMethodList("foo, bar, baz"); + + Assert.assertNotNull(methods); + Assert.assertTrue(methods.size() == 3); + Assert.assertTrue(methods.contains("foo")); + Assert.assertTrue(methods.contains("bar")); + Assert.assertTrue(methods.contains("baz")); + } +} diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/config/SpringHandlerTest.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/config/SpringHandlerTest.java new file mode 100644 index 00000000..48db8a9a --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/config/SpringHandlerTest.java @@ -0,0 +1,74 @@ +/* SpringHandlerTest.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.config; + +import org.aopalliance.intercept.MethodInterceptor; +import org.fishwife.jrugged.PerformanceMonitor; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; + +public class SpringHandlerTest { + + ClassPathXmlApplicationContext context; + + @Before + public void setUp() { + String[] config = new String[] {"applicationContext.xml"}; + context = new ClassPathXmlApplicationContext(config); + } + + @After + public void tearDown() { + context.close(); + } + + @Test + public void testAttributesCreatePerfMon() { + + DummyService service = (DummyService)context.getBean("dummyService"); + assertNotNull(service); + + PerformanceMonitor monitor = + (PerformanceMonitor)context.getBean("dummyServicePerformanceMonitor"); + assertNotNull(monitor); + + service.foo(); + assertEquals(1, monitor.getRequestCount()); + + service.bar(); + assertEquals(2, monitor.getRequestCount()); + + service.baz(); + assertEquals(2, monitor.getRequestCount()); + + MethodInterceptor wrapper = + (MethodInterceptor)context.getBean("dummyServicePerformanceMonitorInterceptor"); + assertNotNull(wrapper); + } + + @Test + public void testPerfMonElementCreatedPerfMon() { + PerformanceMonitor monitor = + (PerformanceMonitor)context.getBean("performanceMonitor"); + assertNotNull(monitor); + } +} diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/jmx/TestMBeanOperationInvoker.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/jmx/TestMBeanOperationInvoker.java new file mode 100644 index 00000000..0a77863d --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/jmx/TestMBeanOperationInvoker.java @@ -0,0 +1,117 @@ +/* TestMBeanOperationInvoker.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.jmx; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import javax.management.JMException; +import javax.management.MBeanOperationInfo; +import javax.management.MBeanParameterInfo; +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import java.util.HashMap; +import java.util.Map; + +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.createMock; + +import static junit.framework.Assert.assertEquals; +import static org.easymock.EasyMock.eq; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +public class TestMBeanOperationInvoker { + + private MBeanServer mockMBeanServer; + private ObjectName mockObjectName; + private MBeanOperationInfo mockOperationInfo; + private MBeanValueConverter mockValueConverter; + + private MBeanOperationInvoker invoker; + + class MockMBeanOperationInvoker extends MBeanOperationInvoker { + MockMBeanOperationInvoker(MBeanServer mBeanServer, ObjectName objectName, MBeanOperationInfo operationInfo) { + super(mBeanServer, objectName, operationInfo); + } + + MBeanValueConverter createMBeanValueConverter(Map parameterMap) { + return mockValueConverter; + } + } + + @Before + public void setUp() { + mockMBeanServer = createMock(MBeanServer.class); + mockObjectName = createMock(ObjectName.class); + mockOperationInfo = createMock(MBeanOperationInfo.class); + mockValueConverter = createMock(MBeanValueConverter.class); + + invoker = new MockMBeanOperationInvoker(mockMBeanServer, mockObjectName, mockOperationInfo); + } + + @Test + public void testConstructor() { + assertEquals(mockMBeanServer, ReflectionTestUtils.getField(invoker, "mBeanServer")); + assertEquals(mockObjectName, ReflectionTestUtils.getField(invoker, "objectName")); + assertEquals(mockOperationInfo, ReflectionTestUtils.getField(invoker, "operationInfo")); + } + + @Test + public void testInvokeOperation() throws JMException { + MBeanParameterInfo mockParameterInfo1 = createMock(MBeanParameterInfo.class); + MBeanParameterInfo mockParameterInfo2 = createMock(MBeanParameterInfo.class); + MBeanParameterInfo[] parameterInfoArray = new MBeanParameterInfo[] { mockParameterInfo1, mockParameterInfo2 }; + + expect(mockOperationInfo.getSignature()).andReturn(parameterInfoArray); + String name1 = "name 1"; + String type1 = "type 1"; + expect(mockParameterInfo1.getType()).andReturn(type1); + expect(mockParameterInfo1.getName()).andReturn(name1); + String value1 = "value 1"; + expect(mockValueConverter.convertParameterValue(name1, type1)).andReturn(value1); + + String name2 = "name 2"; + String type2 = "type 2"; + expect(mockParameterInfo2.getType()).andReturn(type2); + expect(mockParameterInfo2.getName()).andReturn(name2); + String value2 = "value 2"; + expect(mockValueConverter.convertParameterValue(name2, type2)).andReturn(value2); + + String operationName = "some_operation_name"; + expect(mockOperationInfo.getName()).andReturn(operationName); + + Object value = new Object(); + expect(mockMBeanServer.invoke(eq(mockObjectName), eq(operationName), anyObject(String[].class), anyObject(String[].class))).andReturn(value); + + replay(mockMBeanServer, mockObjectName, mockOperationInfo, mockValueConverter, + mockParameterInfo1, mockParameterInfo2); + + Map parameterMap = new HashMap(); + parameterMap.put(name1, new String[] { value1 }); + parameterMap.put(name2, new String[] { value2 }); + + Object invokeResult = invoker.invokeOperation(parameterMap); + + assertEquals(value, invokeResult); + verify(mockMBeanServer, mockObjectName, mockOperationInfo, mockValueConverter, + mockParameterInfo1, mockParameterInfo2); + } +} diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/jmx/TestMBeanStringSanitizer.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/jmx/TestMBeanStringSanitizer.java new file mode 100644 index 00000000..ca357fc1 --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/jmx/TestMBeanStringSanitizer.java @@ -0,0 +1,58 @@ +/* TestMBeanStringSanitizer.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.jmx; + +import org.junit.Before; +import org.junit.Test; + +import java.io.UnsupportedEncodingException; + +import static junit.framework.Assert.assertEquals; + +public class TestMBeanStringSanitizer { + + private MBeanStringSanitizer sanitizer; + + @Before + public void setUp() { + sanitizer = new MBeanStringSanitizer(); + } + + @Test + public void testUrlDecodeDecodesSlashes() throws Exception { + String testString = "this%2Fhas%2Fslashes"; + String sanitizedString = sanitizer.urlDecode(testString, "UTF-8"); + assertEquals("this/has/slashes", sanitizedString); + } + + @Test(expected= UnsupportedEncodingException.class) + public void testUrlDecodeThrowsUnsupportedEncodingException() throws Exception { + sanitizer.urlDecode("some_string_with_encoding_%2F", "unsupported_encoding"); + } + + @Test + public void testEscapeValueEscapesValues() { + String testString = "thisevil&characters"; + String escapedString = sanitizer.escapeValue(testString); + assertEquals("this<contains>evil&characters", escapedString); + } + + @Test + public void testEscapeValueEscapesNulls() { + assertEquals("<null>", sanitizer.escapeValue(null)); + } +} diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/jmx/TestMBeanValueConverter.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/jmx/TestMBeanValueConverter.java new file mode 100644 index 00000000..f0605747 --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/jmx/TestMBeanValueConverter.java @@ -0,0 +1,88 @@ +/* TestMBeanValueConverter.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.jmx; + +import org.junit.Before; +import org.junit.Test; +import static junit.framework.Assert.assertEquals; + +import java.util.HashMap; +import java.util.Map; + +public class TestMBeanValueConverter { + + private MBeanValueConverter converter; + + @Before + public void setUp() { + + Map parameterMap = new HashMap(); + parameterMap.put("nullString", new String[] { "" }); + parameterMap.put("stringValue", new String[] { "some_string" }); + parameterMap.put("booleanValue", new String[] { "true" }); + parameterMap.put("intValue", new String[] { "123" }); + parameterMap.put("longValue", new String[] { "456" }); + parameterMap.put("floatValue", new String[] { "123.45" }); + parameterMap.put("doubleValue", new String[] { "456.78" }); + converter = new MBeanValueConverter(parameterMap); + } + + @Test + public void testNonProvidedValue() throws Exception { + assertEquals(null, converter.convertParameterValue("not_in_the_map", "")); + } + + @Test + public void testNullString() throws Exception { + assertEquals(null, converter.convertParameterValue("nullString", "any_type_will_do")); + } + + @Test + public void testStringValue() throws Exception { + assertEquals("some_string", converter.convertParameterValue("stringValue", "java.lang.String")); + } + + @Test + public void testBooleanValue() throws Exception { + assertEquals(true, converter.convertParameterValue("booleanValue", "boolean")); + } + + @Test + public void testIntValue() throws Exception { + assertEquals(123, converter.convertParameterValue("intValue", "int")); + } + + @Test + public void testLongValue() throws Exception { + assertEquals((long)456, converter.convertParameterValue("longValue", "long")); + } + + @Test + public void testFloatValue() throws Exception { + assertEquals((float)123.45, converter.convertParameterValue("floatValue", "float")); + } + + @Test + public void testDoubleValue() throws Exception { + assertEquals(456.78, converter.convertParameterValue("doubleValue", "double")); + } + + @Test(expected=UnhandledParameterTypeException.class) + public void testUnhandledType() throws Exception { + converter.convertParameterValue("stringValue", "unknown_type"); + } +} diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/jmx/TestWebMBeanAdapter.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/jmx/TestWebMBeanAdapter.java new file mode 100644 index 00000000..12b4a566 --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/jmx/TestWebMBeanAdapter.java @@ -0,0 +1,297 @@ +/* TestWebMBeanAdapter.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.jmx; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.JMException; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanInfo; +import javax.management.MBeanOperationInfo; +import javax.management.MBeanServer; +import javax.management.ObjectName; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static junit.framework.Assert.assertEquals; +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.eq; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.reset; +import static org.easymock.EasyMock.verify; + +public class TestWebMBeanAdapter { + + private MBeanServer mockMBeanServer; + private MBeanStringSanitizer mockSanitizer; + private WebMBeanAdapter webMBeanAdapter; + private ObjectName mockObjectName; + private MBeanInfo mockMBeanInfo; + private MBeanOperationInvoker mockMBeanOperationInvoker; + private static final String ENCODING = "UTF-8"; + + class MockWebMBeanAdapter extends WebMBeanAdapter { + public MockWebMBeanAdapter(MBeanServer mBeanServer, String mBeanName) + throws JMException, UnsupportedEncodingException { + super(mBeanServer, mBeanName, ENCODING); + } + @Override + MBeanStringSanitizer createMBeanStringSanitizer() { + return mockSanitizer; + } + @Override + ObjectName createObjectName(String name) { + return mockObjectName; + } + @Override + MBeanOperationInvoker createMBeanOperationInvoker( + MBeanServer mBeanServer, ObjectName objectName, MBeanOperationInfo operationInfo) { + return mockMBeanOperationInvoker; + } + } + + @Before + public void setUp() throws Exception { + mockMBeanServer = createMock(MBeanServer.class); + mockSanitizer = createMock(MBeanStringSanitizer.class); + mockMBeanInfo = createMock(MBeanInfo.class); + mockObjectName = createMock(ObjectName.class); + mockMBeanOperationInvoker = createMock(MBeanOperationInvoker.class); + + String beanName = "some_bean_name"; + + expect(mockSanitizer.urlDecode(beanName, ENCODING)).andReturn(beanName); + expect(mockMBeanServer.getMBeanInfo(mockObjectName)).andReturn(mockMBeanInfo); + replay(mockSanitizer, mockMBeanServer); + + webMBeanAdapter = new MockWebMBeanAdapter(mockMBeanServer, beanName); + + verify(mockMBeanServer, mockSanitizer); + reset(mockMBeanServer, mockSanitizer); + } + + @Test + public void testConstructor() throws Exception { + assertEquals(mockMBeanServer, ReflectionTestUtils.getField(webMBeanAdapter, "mBeanServer")); + assertEquals(mockObjectName, ReflectionTestUtils.getField(webMBeanAdapter, "objectName")); + assertEquals(mockMBeanInfo, ReflectionTestUtils.getField(webMBeanAdapter, "mBeanInfo")); + } + + @Test + public void testGetAttributeMetadata() throws Exception { + + String attributeName1 = "attribute_name_1"; + MBeanAttributeInfo mockAttribute1 = createMock(MBeanAttributeInfo.class); + + String attributeName2 = "attribute_name_2"; + MBeanAttributeInfo mockAttribute2 = createMock(MBeanAttributeInfo.class); + + MBeanAttributeInfo[] attributeList = new MBeanAttributeInfo[2]; + attributeList[0] = mockAttribute1; + attributeList[1] = mockAttribute2; + expect(mockMBeanInfo.getAttributes()).andReturn(attributeList); + expect(mockAttribute1.getName()).andReturn(attributeName1); + expect(mockAttribute2.getName()).andReturn(attributeName2); + + replay(mockMBeanServer, mockSanitizer, mockObjectName, mockMBeanInfo, + mockAttribute1, mockAttribute2); + + Map attributeMap = webMBeanAdapter.getAttributeMetadata(); + + assertEquals(2, attributeMap.size()); + assertEquals(mockAttribute1, attributeMap.get(attributeName1)); + assertEquals(mockAttribute2, attributeMap.get(attributeName2)); + + verify(mockMBeanServer, mockSanitizer, mockObjectName, mockMBeanInfo, + mockAttribute1, mockAttribute2); + } + + @Test + public void testGetOperationMetadata() throws Exception { + String operationName1 = "operation_name_1"; + MBeanOperationInfo mockOperation1 = createMock(MBeanOperationInfo.class); + + String operationName2 = "operation_name_2"; + MBeanOperationInfo mockOperation2 = createMock(MBeanOperationInfo.class); + + MBeanOperationInfo[] operationList = new MBeanOperationInfo[2]; + operationList[0] = mockOperation1; + operationList[1] = mockOperation2; + expect(mockMBeanInfo.getOperations()).andReturn(operationList); + expect(mockOperation1.getName()).andReturn(operationName1); + expect(mockOperation2.getName()).andReturn(operationName2); + + replay(mockMBeanServer, mockSanitizer, mockObjectName, mockMBeanInfo, + mockOperation1, mockOperation2); + + Map operationMap = webMBeanAdapter.getOperationMetadata(); + + assertEquals(2, operationMap.size()); + assertEquals(mockOperation1, operationMap.get(operationName1)); + assertEquals(mockOperation2, operationMap.get(operationName2)); + + verify(mockMBeanServer, mockSanitizer, mockObjectName, mockMBeanInfo, + mockOperation1, mockOperation2); + } + + @Test + public void testGetOperationInfoWhenItExists() throws Exception { + String operationName1 = "operation_name_1"; + MBeanOperationInfo mockOperation1 = createMock(MBeanOperationInfo.class); + + String operationName2 = "operation_name_2"; + MBeanOperationInfo mockOperation2 = createMock(MBeanOperationInfo.class); + + expect(mockSanitizer.urlDecode(operationName2, ENCODING)).andReturn(operationName2); + MBeanOperationInfo[] operationList = new MBeanOperationInfo[2]; + operationList[0] = mockOperation1; + operationList[1] = mockOperation2; + expect(mockMBeanInfo.getOperations()).andReturn(operationList); + expect(mockOperation1.getName()).andReturn(operationName1); + expect(mockOperation2.getName()).andReturn(operationName2); + + replay(mockMBeanServer, mockSanitizer, mockObjectName, mockMBeanInfo, + mockOperation1, mockOperation2); + + MBeanOperationInfo operationInfo = webMBeanAdapter.getOperationInfo(operationName2); + + assertEquals(mockOperation2, operationInfo); + + verify(mockMBeanServer, mockSanitizer, mockObjectName, mockMBeanInfo, + mockOperation1, mockOperation2); + } + + @Test(expected=OperationNotFoundException.class) + public void testGetOperationInfoThrowsOperationNotFoundException() throws Exception { + String operationName = "nonexistent"; + expect(mockObjectName.getCanonicalName()).andReturn("some_name"); + + MBeanOperationInfo[] operationList = new MBeanOperationInfo[0]; + expect(mockSanitizer.urlDecode(operationName, ENCODING)).andReturn(operationName); + expect(mockMBeanInfo.getOperations()).andReturn(operationList); + + replay(mockMBeanServer, mockSanitizer, mockObjectName, mockMBeanInfo); + + webMBeanAdapter.getOperationInfo(operationName); + } + + @Test + public void testGetAttributeValues() throws Exception { + MBeanAttributeInfo[] attributeInfoArray = new MBeanAttributeInfo[2]; + MBeanAttributeInfo mockAttributeInfo1 = createMock(MBeanAttributeInfo.class); + MBeanAttributeInfo mockAttributeInfo2 = createMock(MBeanAttributeInfo.class); + attributeInfoArray[0] = mockAttributeInfo1; + attributeInfoArray[1] = mockAttributeInfo2; + expect(mockMBeanInfo.getAttributes()).andReturn(attributeInfoArray); + + String attributeName1 = "attribute_name_1"; + String attributeName2 = "attribute_name_2"; + expect(mockAttributeInfo1.getName()).andReturn(attributeName1); + expect(mockAttributeInfo2.getName()).andReturn(attributeName2); + + AttributeList mockAttributeList = createMock(AttributeList.class); + expect(mockMBeanServer.getAttributes(eq(mockObjectName), anyObject(String[].class))).andReturn(mockAttributeList); + + List attributeList = new ArrayList(); + Attribute mockAttribute1 = createMock(Attribute.class); + Attribute mockAttribute2 = createMock(Attribute.class); + + attributeList.add(mockAttribute1); + attributeList.add(mockAttribute2); + + expect(mockAttributeList.asList()).andReturn(attributeList); + + String name1 = "name 1"; + String value1 = "value 1"; + expect(mockAttribute1.getName()).andReturn(name1); + expect(mockAttribute1.getValue()).andReturn(value1); + expect(mockSanitizer.escapeValue(value1)).andReturn(value1); + + String name2 = "name 2"; + String value2 = "value 2"; + expect(mockAttribute2.getName()).andReturn(name2); + expect(mockAttribute2.getValue()).andReturn(value2); + expect(mockSanitizer.escapeValue(value2)).andReturn(value2); + + replay(mockMBeanServer, mockSanitizer, mockObjectName, mockMBeanInfo, + mockAttributeInfo1, mockAttributeInfo2, mockAttributeList, + mockAttribute1, mockAttribute2); + + Map attributeValueMap = webMBeanAdapter.getAttributeValues(); + + assertEquals(2, attributeValueMap.size()); + assertEquals(value1, attributeValueMap.get(name1)); + assertEquals(value2, attributeValueMap.get(name2)); + + verify(mockMBeanServer, mockSanitizer, mockObjectName, mockMBeanInfo, + mockAttributeInfo1, mockAttributeInfo2, mockAttributeList, + mockAttribute1, mockAttribute2); + } + + @Test + public void testGetAttributeValue() throws Exception { + String attributeName = "attribute_name"; + expect(mockSanitizer.urlDecode(attributeName, ENCODING)).andReturn(attributeName); + + Object value = new Object(); + String valueString = "some_string"; + expect(mockMBeanServer.getAttribute(mockObjectName, attributeName)).andReturn(value); + expect(mockSanitizer.escapeValue(value)).andReturn(valueString); + + replay(mockMBeanServer, mockSanitizer, mockObjectName); + + String attributeValue = webMBeanAdapter.getAttributeValue(attributeName); + + assertEquals(valueString, attributeValue); + verify(mockMBeanServer, mockSanitizer, mockObjectName); + } + + @Test + public void testInvokeOperation() throws Exception { + String operationName = "operation_name"; + expect(mockSanitizer.urlDecode(operationName, ENCODING)).andReturn(operationName); + + MBeanOperationInfo mockOperation = createMock(MBeanOperationInfo.class); + MBeanOperationInfo[] operationList = new MBeanOperationInfo[1]; + operationList[0] = mockOperation; + expect(mockMBeanInfo.getOperations()).andReturn(operationList); + expect(mockOperation.getName()).andReturn(operationName); + + Map parameterMap = new HashMap(); + Object value = new Object(); + expect(mockMBeanOperationInvoker.invokeOperation(parameterMap)).andReturn(value); + + String valueString = "some_value"; + expect(mockSanitizer.escapeValue(value)).andReturn(valueString); + + replay(mockMBeanServer, mockSanitizer, mockObjectName, mockMBeanInfo, mockOperation, mockMBeanOperationInvoker); + + String invokeResult = webMBeanAdapter.invokeOperation(operationName, parameterMap); + + assertEquals(valueString, invokeResult); + verify(mockMBeanServer, mockSanitizer, mockObjectName, mockMBeanInfo, mockOperation, mockMBeanOperationInvoker); + } +} diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/jmx/TestWebMBeanServerAdapter.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/jmx/TestWebMBeanServerAdapter.java new file mode 100644 index 00000000..a01d7844 --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/jmx/TestWebMBeanServerAdapter.java @@ -0,0 +1,88 @@ +/* TestWebMBeanServerAdapter.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.jmx; + +import org.junit.Before; +import org.junit.Test; + +import javax.management.MBeanServer; +import javax.management.ObjectInstance; +import javax.management.ObjectName; + +import java.util.HashSet; +import java.util.Set; + +import static junit.framework.Assert.assertTrue; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; + +import static junit.framework.Assert.assertEquals; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +public class TestWebMBeanServerAdapter { + + private MBeanServer mockMbeanServer; + private WebMBeanServerAdapter webMBeanServerAdapter; + private MBeanStringSanitizer mockSanitizer; + + class MockWebMBeanServerAdapter extends WebMBeanServerAdapter { + public MockWebMBeanServerAdapter(MBeanServer mBeanServer) { + super(mBeanServer); + } + @Override + MBeanStringSanitizer createMBeanStringSanitizer() { + return mockSanitizer; + } + } + + @Before + public void setUp() { + mockMbeanServer = createMock(MBeanServer.class); + mockSanitizer = createMock(MBeanStringSanitizer.class); + webMBeanServerAdapter = new MockWebMBeanServerAdapter(mockMbeanServer); + } + + @Test + public void testGetMBeanNames() throws Exception { + String name1 = "com.test:type=objectName,value=1"; + String name2 = "com.test:type=objectName,value=2"; + ObjectName objectName1 = new ObjectName(name1); + ObjectInstance object1 = createMock(ObjectInstance.class); + expect(object1.getObjectName()).andReturn(objectName1); + + ObjectName objectName2 = new ObjectName(name2); + ObjectInstance object2 = createMock(ObjectInstance.class); + expect(object2.getObjectName()).andReturn(objectName2); + + Set objectInstanceList = new HashSet(); + objectInstanceList.add(object1); + objectInstanceList.add(object2); + expect(mockMbeanServer.queryMBeans(null, null)).andReturn(objectInstanceList); + expect(mockSanitizer.escapeValue(name1)).andReturn(name1); + expect(mockSanitizer.escapeValue(name2)).andReturn(name2); + + replay(mockMbeanServer, mockSanitizer, object1, object2); + + Set resultSet = webMBeanServerAdapter.getMBeanNames(); + + assertEquals(2, resultSet.size()); + assertTrue(resultSet.contains(name1)); + assertTrue(resultSet.contains(name2)); + verify(mockMbeanServer, mockSanitizer, object1, object2); + } +} diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/retry/ClassifierSimpleRetryPolicyTest.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/retry/ClassifierSimpleRetryPolicyTest.java new file mode 100644 index 00000000..a2fa23bb --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/retry/ClassifierSimpleRetryPolicyTest.java @@ -0,0 +1,59 @@ +/* Copyright 2009-2019 Comcast Interactive Media, LLC. + + 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.fishwife.jrugged.spring.retry; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.classify.Classifier; +import org.springframework.retry.RetryContext; + +public class ClassifierSimpleRetryPolicyTest { + @Test + public void test_classify() { + RetryContext context = Mockito.mock(RetryContext.class); + Classifier classifier = Mockito.mock(Classifier.class); + ClassifierSimpleRetryPolicy policy = new ClassifierSimpleRetryPolicy(classifier); + Mockito.when(context.getLastThrowable()).thenReturn(new RuntimeException()); + Mockito.when(classifier.classify(Mockito.any(Throwable.class))).thenReturn(true); + Assert.assertTrue(policy.canRetry(context)); + Assert.assertSame(classifier, policy.getClassifier()); + } + + @Test + public void test_classify_nullClassifier() { + RetryContext context = Mockito.mock(RetryContext.class); + ClassifierSimpleRetryPolicy policy = new ClassifierSimpleRetryPolicy(null); + Mockito.when(context.getLastThrowable()).thenReturn(new RuntimeException()); + Assert.assertFalse(policy.canRetry(context)); + } + + @Test + public void test_classify_nullClassifier2() { + RetryContext context = Mockito.mock(RetryContext.class); + ClassifierSimpleRetryPolicy policy = new ClassifierSimpleRetryPolicy(); + Mockito.when(context.getLastThrowable()).thenReturn(new RuntimeException()); + Assert.assertFalse(policy.canRetry(context)); + } + + @Test + public void test_classify_nullClassifier3() { + RetryContext context = Mockito.mock(RetryContext.class); + ClassifierSimpleRetryPolicy policy = new ClassifierSimpleRetryPolicy(4); + Mockito.when(context.getLastThrowable()).thenReturn(new RuntimeException()); + Assert.assertFalse(policy.canRetry(context)); + } +} diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/retry/ExtendedPredicatesTest.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/retry/ExtendedPredicatesTest.java new file mode 100644 index 00000000..9a73f29f --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/retry/ExtendedPredicatesTest.java @@ -0,0 +1,51 @@ +/* Copyright 2009-2019 Comcast Interactive Media, LLC. + + 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.fishwife.jrugged.spring.retry; + +import com.google.common.base.Predicate; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; + +public class ExtendedPredicatesTest { + + @Test + public void test_isInstanceOf() { + Predicate t = ExtendedPredicates.isInstanceOf(Throwable.class, RuntimeException.class); + Assert.assertTrue(t.apply(new RuntimeException())); + Assert.assertFalse(t.apply(new IOException())); + } + + @Test + public void test_throwableContainsMessage_sensitive() { + Predicate t = ExtendedPredicates.throwableContainsMessage("foo", true); + Assert.assertTrue(t.apply(new RuntimeException("foo"))); + Assert.assertFalse(t.apply(new RuntimeException("Foo"))); + Assert.assertFalse(t.apply(new RuntimeException("bar"))); + Assert.assertFalse(t.apply(new RuntimeException("Bar"))); + } + + @Test + public void test_throwableContainsMessage_insensitive() { + Predicate t = ExtendedPredicates.throwableContainsMessage("foo", false); + Assert.assertTrue(t.apply(new RuntimeException("Foo"))); + Assert.assertTrue(t.apply(new RuntimeException("foo"))); + Assert.assertFalse(t.apply(new RuntimeException("bar"))); + Assert.assertFalse(t.apply(new RuntimeException("Bar"))); + } + +} diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/retry/ExtendedRetryTemplateTest.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/retry/ExtendedRetryTemplateTest.java new file mode 100644 index 00000000..02d6aac4 --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/retry/ExtendedRetryTemplateTest.java @@ -0,0 +1,97 @@ +/* Copyright 2009-2019 Comcast Interactive Media, LLC. + + 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.fishwife.jrugged.spring.retry; + +import java.util.concurrent.Callable; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.retry.RecoveryCallback; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryState; + +public class ExtendedRetryTemplateTest { + @Test + public void test_asCallable_callable() throws Exception { + Callable callable = Mockito.mock(Callable.class); + ExtendedRetryTemplate template = new ExtendedRetryTemplate(); + + Mockito.when(callable.call()).thenReturn(10L); + + Callable wrapped = template.asCallable(callable); + Assert.assertEquals(10L, wrapped.call().longValue()); + + Mockito.verify(callable, Mockito.times(1)).call(); + } + + @Test + public void test_asCallable_callback() throws Exception { + RetryCallback callback = Mockito.mock(RetryCallback.class); + ExtendedRetryTemplate template = new ExtendedRetryTemplate(); + + Mockito.when(callback.doWithRetry(Mockito.any(RetryContext.class))).thenReturn(10L); + + Callable wrapped = template.asCallable(callback); + Assert.assertEquals(10L, wrapped.call().longValue()); + + Mockito.verify(callback, Mockito.times(1)).doWithRetry(Mockito.any(RetryContext.class)); + } + + @Test + public void test_execute_callable() throws Exception { + Callable callable = Mockito.mock(Callable.class); + ExtendedRetryTemplate template = new ExtendedRetryTemplate(); + + Mockito.when(callable.call()).thenReturn(10L); + + Assert.assertEquals(10L, template.execute(callable).longValue()); + + Mockito.verify(callable, Mockito.times(1)).call(); + + } + + @Test + public void test_execute_callableWithState() throws Exception { + Callable callable = Mockito.mock(Callable.class); + RetryState retryState = Mockito.mock(RetryState.class); + ExtendedRetryTemplate template = new ExtendedRetryTemplate(); + + Mockito.when(callable.call()).thenReturn(10L); + + Assert.assertEquals(10L, template.execute(callable, retryState).longValue()); + + Mockito.verify(callable, Mockito.times(1)).call(); + + } + + @Test + public void test_execute_callableWithRecoveryAndState() throws Exception { + Callable callable = Mockito.mock(Callable.class); + RetryState retryState = Mockito.mock(RetryState.class); + RecoveryCallback recoveryCallback = Mockito.mock(RecoveryCallback.class); + + ExtendedRetryTemplate template = new ExtendedRetryTemplate(); + + Mockito.when(callable.call()).thenReturn(10L); + + Assert.assertEquals(10L, template.execute(callable, recoveryCallback, retryState).longValue()); + + Mockito.verify(callable, Mockito.times(1)).call(); + } +} + diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/retry/PredicateBinaryExceptionClassifierTest.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/retry/PredicateBinaryExceptionClassifierTest.java new file mode 100644 index 00000000..ef62c544 --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/retry/PredicateBinaryExceptionClassifierTest.java @@ -0,0 +1,35 @@ +/* Copyright 2009-2019 Comcast Interactive Media, LLC. + + 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.fishwife.jrugged.spring.retry; + +import com.google.common.base.Predicate; +import junit.framework.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +public class PredicateBinaryExceptionClassifierTest { + + @Test + public void testClassify() { + Predicate predicate = Mockito.mock(Predicate.class); + PredicateBinaryExceptionClassifier classifier = new PredicateBinaryExceptionClassifier(predicate); + classifier.setPredicate(predicate); + Mockito.when(predicate.apply(Mockito.any(Throwable.class))).thenReturn(false); + Assert.assertSame(predicate, classifier.getPredicate()); + Assert.assertFalse(classifier.classify(new RuntimeException())); + Mockito.verify(predicate, Mockito.times(1)).apply(Mockito.any(Throwable.class)); + } +} diff --git a/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/testonly/TestClass.java b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/testonly/TestClass.java new file mode 100644 index 00000000..76818274 --- /dev/null +++ b/jrugged-spring-V5/src/test/java/org/fishwife/jrugged/spring/testonly/TestClass.java @@ -0,0 +1,56 @@ +/* TestClass.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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.fishwife.jrugged.spring.testonly; + +import org.fishwife.jrugged.aspects.CircuitBreaker; +import org.fishwife.jrugged.aspects.PerformanceMonitor; +import org.junit.Ignore; + +// Test class under test package to test package scan +@Ignore +public class TestClass { + + @PerformanceMonitor("monitorA") + public void monitoredMethod1() { + + } + + @PerformanceMonitor("monitorB") + public void monitoredMethod2() { + + } + + @PerformanceMonitor("monitorA") + public void monitoredMethod3() { + + } + + @CircuitBreaker(name = "breakerA") + public void circuitBreakerMethod1() { + + } + + @CircuitBreaker(name = "breakerB") + public void circuitBreakerMethod2() { + + } + + public void unmonitoredMethod() { + + } + +} diff --git a/jrugged-spring-V5/src/test/resources/applicationContext.xml b/jrugged-spring-V5/src/test/resources/applicationContext.xml new file mode 100644 index 00000000..dda3c8fc --- /dev/null +++ b/jrugged-spring-V5/src/test/resources/applicationContext.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jrugged-spring-V5/src/test/resources/circuitTestContext.xml b/jrugged-spring-V5/src/test/resources/circuitTestContext.xml new file mode 100644 index 00000000..84a33af7 --- /dev/null +++ b/jrugged-spring-V5/src/test/resources/circuitTestContext.xml @@ -0,0 +1,105 @@ + + + + + + 4 + 5000 + 5000 + + + + + + + + + + + org.fishwife.jrugged.spring.TestCircuitBreakerException + + + + + + + + + + + + + + + + + + + + + + + + + + testCircuitBreakerInterceptor + testMonitorInterceptor + + + + + + + + + + + + + + + + + + diff --git a/jrugged-spring-V5/src/test/resources/log4j.xml b/jrugged-spring-V5/src/test/resources/log4j.xml new file mode 100644 index 00000000..c72737b5 --- /dev/null +++ b/jrugged-spring-V5/src/test/resources/log4j.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 69c13df2..4c8999f5 100644 --- a/pom.xml +++ b/pom.xml @@ -193,8 +193,9 @@ jrugged-core jrugged-aspects jrugged-spring + jrugged-spring-V5 jrugged-httpclient - jrugged-examples + jrugged-examples