/* * Copyright 2014, The Sporting Exchange Limited * Copyright 2015, Simon Matić Langford * * 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 com.betfair.cougar.transport.impl.protocol.http.jsonrpc; import com.betfair.cougar.api.ExecutionContext; import com.betfair.cougar.api.DehydratedExecutionContext; import com.betfair.cougar.api.RequestUUID; import com.betfair.cougar.api.ResponseCode; import com.betfair.cougar.api.export.Protocol; import com.betfair.cougar.api.security.*; import com.betfair.cougar.core.api.OperationBindingDescriptor; import com.betfair.cougar.core.api.RequestTimer; import com.betfair.cougar.core.api.ServiceBindingDescriptor; import com.betfair.cougar.core.api.ServiceVersion; import com.betfair.cougar.core.api.ev.Executable; import com.betfair.cougar.core.api.ev.ExecutionTimingRecorder; import com.betfair.cougar.core.api.ev.NullExecutionTimingRecorder; import com.betfair.cougar.core.api.ev.ExecutionObserver; import com.betfair.cougar.core.api.ev.ExecutionPostProcessor; import com.betfair.cougar.core.api.ev.ExecutionPreProcessor; import com.betfair.cougar.core.api.ev.ExecutionResult; import com.betfair.cougar.core.api.ev.ExecutionVenue; import com.betfair.cougar.core.api.ev.OperationDefinition; import com.betfair.cougar.core.api.ev.OperationKey; import com.betfair.cougar.core.api.ev.TimeConstraints; import com.betfair.cougar.core.api.exception.CougarException; import com.betfair.cougar.core.api.exception.CougarServiceException; import com.betfair.cougar.core.api.exception.CougarValidationException; import com.betfair.cougar.core.api.exception.ServerFaultCode; import com.betfair.cougar.core.api.tracing.Tracer; import com.betfair.cougar.core.api.transcription.Parameter; import com.betfair.cougar.core.api.transcription.ParameterType; import com.betfair.cougar.core.impl.CougarInternalOperations; import com.betfair.cougar.core.impl.ev.BaseExecutionVenue; import com.betfair.cougar.logging.CougarLoggingUtils; import com.betfair.cougar.transport.api.DehydratedExecutionContextResolution; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.betfair.cougar.logging.EventLogDefinition; import com.betfair.cougar.logging.EventLoggingRegistry; import com.betfair.cougar.marshalling.impl.databinding.json.JSONBindingFactory; import com.betfair.cougar.marshalling.impl.databinding.json.JSONDateFormat; import com.betfair.cougar.transport.api.CommandResolver; import com.betfair.cougar.transport.api.CommandValidator; import com.betfair.cougar.transport.api.ExecutionCommand; import com.betfair.cougar.transport.api.RequestLogger; import com.betfair.cougar.transport.api.RequestTimeResolver; import com.betfair.cougar.transport.api.TransportCommand; import com.betfair.cougar.transport.api.protocol.http.HttpCommand; import com.betfair.cougar.transport.api.protocol.http.jsonrpc.JsonRpcOperationBindingDescriptor; import com.betfair.cougar.transport.impl.AbstractCommandProcessor; import com.betfair.cougar.transport.impl.CommandValidatorRegistry; import com.betfair.cougar.transport.impl.protocol.http.ContentTypeNormaliser; import com.betfair.cougar.util.RequestUUIDImpl; import com.betfair.cougar.util.UUIDGeneratorImpl; import com.betfair.cougar.util.geolocation.GeoIPLocator; import com.betfair.cougar.util.geolocation.RemoteAddressUtils; import com.fasterxml.jackson.databind.type.TypeFactory; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mockito; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.ServletOutputStream; import javax.servlet.WriteListener; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.core.MediaType; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.text.ParseException; import java.util.*; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.Assert.*; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.*; /** * Unit test for @See JsonRpcTransportCommandProcessor */ public class JsonRpcTransportCommandProcessorTest { public static final String AZ = "Azerbaijan"; public static final String SERVICE_NAME = "TestingService"; public static final ServiceVersion SERVICE_VERSION = new ServiceVersion("v1.0"); public static final String OP_NAME = "characterCount"; public static final OperationKey TEST_OP_KEY = new OperationKey(SERVICE_VERSION, SERVICE_NAME, OP_NAME); public static final String OP_NAME2 = "dateEcho"; public static final OperationKey TEST_OP2_KEY = new OperationKey(SERVICE_VERSION, SERVICE_NAME, OP_NAME2); private LocalJsonRpcCommandProcessor commandProcessor; private ObjectMapper objectMapper; private JSONDateFormat jdf = new JSONDateFormat(); protected RequestLogger logger; GeoIPLocator geoIPLocator; protected CommandValidatorRegistry<HttpCommand> validatorRegistry = new CommandValidatorRegistry<HttpCommand>(); private ExecutionVenue ev; //private RequestTimeResolver requestTimeResolver; private Tracer tracer; private DehydratedExecutionContextResolution contextResolution; private DehydratedExecutionContext context; protected void verifyTracerCalls(final OperationKey... calls) { verifyTracerCalls(false, calls); } protected void verifyTracerCalls(boolean allowMoreStarts, final OperationKey... calls) { final ArgumentCaptor<RequestUUID> startCaptor = ArgumentCaptor.forClass(RequestUUID.class); final ArgumentCaptor<RequestUUID> endCaptor = ArgumentCaptor.forClass(RequestUUID.class); final ArgumentCaptor<OperationKey> opKeyCaptor = ArgumentCaptor.forClass(OperationKey.class); InOrder inOrder = inOrder(tracer); inOrder.verify(tracer, allowMoreStarts ? atLeast(calls.length+1) : times(calls.length+1)).start(startCaptor.capture(), opKeyCaptor.capture()); inOrder.verify(tracer, times(calls.length+1)).end(endCaptor.capture()); List<RequestUUID> starts = new ArrayList<>(startCaptor.getAllValues()); List<RequestUUID> ends = new ArrayList<>(endCaptor.getAllValues()); if (allowMoreStarts) { for (; starts.size()>ends.size();) { starts.remove(starts.size()-1); } } Collections.reverse(ends); assertEquals(starts,ends); for (int i=0; i<calls.length; i++) { assertEquals(calls[i], opKeyCaptor.getAllValues().get(i+1)); } } @BeforeClass public static void setupStatic() { RequestUUIDImpl.setGenerator(new UUIDGeneratorImpl()); CougarLoggingUtils.suppressAllRootLoggerOutput(); } @Before public void init() { geoIPLocator = mock(GeoIPLocator.class); ContentTypeNormaliser ctn = mock(ContentTypeNormaliser.class); EventLoggingRegistry mockEventLoggingRegistry = mock(EventLoggingRegistry.class); EventLogDefinition logDef = mock(EventLogDefinition.class); logger = mock(RequestLogger.class); tracer = mock(Tracer.class); contextResolution = mock(DehydratedExecutionContextResolution.class); context = mock(DehydratedExecutionContext.class); when(contextResolution.resolveExecutionContext(eq(Protocol.JSON_RPC),any(HttpCommand.class),isNull())).thenReturn(context); RequestUUID uuid = new RequestUUIDImpl(); when(context.getRequestUUID()).thenReturn(uuid); when(ctn.getNormalisedRequestMediaType(any(HttpServletRequest.class))).thenReturn(MediaType.APPLICATION_JSON_TYPE); when(ctn.getNormalisedResponseMediaType(any(HttpServletRequest.class))).thenReturn(MediaType.APPLICATION_JSON_TYPE); when(mockEventLoggingRegistry.getInvokableLogger(anyString())).thenReturn(logDef); when(logDef.getLogName()).thenReturn(""); //requestTimeResolver = mock(RequestTimeResolver.class); commandProcessor = new LocalJsonRpcCommandProcessor(contextResolution, new JSONBindingFactory()); commandProcessor.setContentTypeNormaliser(ctn); commandProcessor.setRequestLogger(logger); commandProcessor.setValidatorRegistry(validatorRegistry); commandProcessor.setExecutor(new Executor() { @Override public void execute(Runnable runnable) { runnable.run(); } }); commandProcessor.setExecutionVenue(ev = mock(ExecutionVenue.class)); commandProcessor.setTracer(tracer); objectMapper = new ObjectMapper(); } private void bindOperations() { bindOperations(ev, true); } private void bindOperations(ExecutionVenue ev, boolean mocked) { // register the ops in the ev - well, sort of OperationDefinition def1 = new OperationDefinition() { @Override public OperationKey getOperationKey() { return TEST_OP_KEY; } @Override public Parameter[] getParameters() { return new Parameter[] { new Parameter("message", ParameterType.create(String.class, null), true), new Parameter("count", ParameterType.create(Integer.class, null), true)}; } @Override public ParameterType getReturnType() { return null; } }; OperationDefinition def2 = new OperationDefinition() { @Override public OperationKey getOperationKey() { return TEST_OP2_KEY; } @Override public Parameter[] getParameters() { return new Parameter[] { new Parameter("date", ParameterType.create(Date.class, null), true) }; } @Override public ParameterType getReturnType() { return ParameterType.create(Date.class, null); } }; if (mocked) { when(ev.getOperationDefinition(TEST_OP_KEY)).thenReturn(def1); when(ev.getOperationDefinition(TEST_OP2_KEY)).thenReturn(def2); } else { Executable noop = new Executable() { @Override public void execute(ExecutionContext ctx, OperationKey key, Object[] args, ExecutionObserver observer, ExecutionVenue executionVenue, TimeConstraints expirtyTime) { observer.onResult(new ExecutionResult(null)); } }; ExecutionTimingRecorder nullMgr = new NullExecutionTimingRecorder(); ev.registerOperation(null, def1, noop, nullMgr, 0); ev.registerOperation(null, def2, noop, nullMgr, 0); } commandProcessor.bind(new ServiceBindingDescriptor() { @Override public OperationBindingDescriptor[] getOperationBindings() { return new OperationBindingDescriptor[]{ new JsonRpcOperationBindingDescriptor(TEST_OP_KEY), new JsonRpcOperationBindingDescriptor(TEST_OP2_KEY) }; } @Override public ServiceVersion getServiceVersion() { return SERVICE_VERSION; } @Override public String getServiceName() { return SERVICE_NAME; } @Override public Protocol getServiceProtocol() { return Protocol.JSON_RPC; } }); commandProcessor.onCougarStart(); } @Test public void ensureIdentityResolverBound() { bindOperations(); verify(ev, times(1)).registerOperation(JsonRpcTransportCommandProcessor.IDENTITY_RESOLUTION_NAMESPACE, JsonRpcTransportCommandProcessor.IDENTITY_RESOLUTION_OPDEF, JsonRpcTransportCommandProcessor.IDENTITY_RESOLUTION_EXEC, JsonRpcTransportCommandProcessor.IDENTITY_RESOLUTION_TIMING_RECORDER, 0); // verify(ev, times(1)).execute(any(DehydratedExecutionContext.class), eq(JsonRpcTransportCommandProcessor.IDENTITY_RESOLUTION_OPDEF.getOperationKey()), eq(new Object[0]), any(ExecutionObserver.class)); } @Test public void ensureNoIdentityResolverBoundWhenNoOperations() { commandProcessor.onCougarStart(); verify(ev, times(0)).registerOperation(JsonRpcTransportCommandProcessor.IDENTITY_RESOLUTION_NAMESPACE, JsonRpcTransportCommandProcessor.IDENTITY_RESOLUTION_OPDEF, JsonRpcTransportCommandProcessor.IDENTITY_RESOLUTION_EXEC, JsonRpcTransportCommandProcessor.IDENTITY_RESOLUTION_TIMING_RECORDER, 0); // verify(ev, times(0)).execute(any(DehydratedExecutionContext.class), eq(JsonRpcTransportCommandProcessor.IDENTITY_RESOLUTION_OPDEF.getOperationKey()), eq(new Object[0]), any(ExecutionObserver.class)); } @Test public void ensureSeperateCallMadeToResolveIdentity() throws IOException { TestBaseExecutionVenue realEv = new TestBaseExecutionVenue(); commandProcessor.setExecutionVenue(realEv); bindOperations(realEv, false); HttpCommand command = mock(HttpCommand.class); HttpServletRequest request = mock(HttpServletRequest.class); when(request.getScheme()).thenReturn("http"); HttpServletResponse response = mock(HttpServletResponse.class); RequestTimer mockTimer = mock(RequestTimer.class); when(command.getRequest()).thenReturn(request); when(command.getResponse()).thenReturn(response); when(command.getTimer()).thenReturn(mockTimer); //Also note that we're mixing the case of the method call up - JSON rpc implementation //matches operations in a case insensitive fashion String body="{ \"method\": \"" + SERVICE_NAME + "/v1.0/" + OP_NAME + "\", \"params\": [\"Hello\", 333], \"id\": 1}"; TestInputStream tis = new TestInputStream(new ByteArrayInputStream(body.getBytes("UTF-8"))); when(request.getInputStream()).thenReturn(tis); TestOutputStream tos = new TestOutputStream(); when(response.getOutputStream()).thenReturn(tos); commandProcessor.process(command); assertEquals(2, realEv.requests.size()); ExecutionRequest req1 = realEv.requests.get(0); assertTrue(DehydratedExecutionContext.class.isAssignableFrom(req1.ctx.getClass())); assertEquals(JsonRpcTransportCommandProcessor.IDENTITY_RESOLUTION_OPDEF.getOperationKey(), req1.key); assertEquals(0, req1.args.length); ExecutionRequest req2 = realEv.requests.get(1); assertFalse(DehydratedExecutionContext.class.isAssignableFrom(req2.ctx.getClass())); assertEquals(TEST_OP_KEY, req2.key); verifyTracerCalls(TEST_OP_KEY); } @Test public void testCreateCommandResolverSingleOp() throws IOException { bindOperations(); HttpServletRequest request = mock(HttpServletRequest.class); when(request.getScheme()).thenReturn("http"); HttpServletResponse response = mock(HttpServletResponse.class); IdentityTokenResolver tokenResolver = mock(IdentityTokenResolver.class); RequestTimer mockTimer = mock(RequestTimer.class); HttpCommand mockedCommand = mock(HttpCommand.class); when(mockedCommand.getRequest()).thenReturn(request); when(mockedCommand.getResponse()).thenReturn(response); when(mockedCommand.getIdentityTokenResolver()).thenReturn(tokenResolver); when(mockedCommand.getStatus()).thenReturn(TransportCommand.CommandStatus.InProgress); when(mockedCommand.getTimer()).thenReturn(mockTimer); String body="{ \"method\": \"" + SERVICE_NAME + "/v1.0/" + OP_NAME + "\", \"params\": [\"Hello\", 333], \"id\": 1}"; TestInputStream tis = new TestInputStream(new ByteArrayInputStream(body.getBytes("UTF-8"))); when(request.getInputStream()).thenReturn(tis); TestOutputStream tos = new TestOutputStream(); when(response.getOutputStream()).thenReturn(tos); CommandResolver<HttpCommand> resolver = commandProcessor.createCommandResolver(mockedCommand, tracer); assertNotNull(resolver); Iterable<ExecutionCommand> cmds = resolver.resolveExecutionCommands(); assertNotNull(cmds); assertTrue(cmds.iterator().hasNext()); ExecutionCommand executionCommand = cmds.iterator().next(); assertNotNull(executionCommand); Object[] args = executionCommand.getArgs(); assertTrue(args.length == 2); assertEquals(args[0], "Hello"); assertEquals(args[1], 333); OperationKey actual = executionCommand.getOperationKey(); assertEquals(TEST_OP_KEY, actual); ExecutionContext ctx = resolver.resolveExecutionContext(); assertNotNull(ctx); //Now we need to ensure that it attempts to write to the response when result is called executionCommand.onResult(new ExecutionResult()); verify(response).getOutputStream(); verify(logger).logAccess(eq(mockedCommand), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); verifyTracerCalls(); } @Test public void testCreateCommandResolverBatched() throws IOException, ParseException { bindOperations(); HttpServletRequest request = mock(HttpServletRequest.class); when(request.getScheme()).thenReturn("http"); HttpServletResponse response = mock(HttpServletResponse.class); IdentityTokenResolver tokenResolver = mock(IdentityTokenResolver.class); HttpCommand mockedCommand = mock(HttpCommand.class); RequestTimer mockTimer = mock(RequestTimer.class); when(mockedCommand.getRequest()).thenReturn(request); when(mockedCommand.getResponse()).thenReturn(response); when(mockedCommand.getIdentityTokenResolver()).thenReturn(tokenResolver); when(mockedCommand.getStatus()).thenReturn(TransportCommand.CommandStatus.InProgress); when(mockedCommand.getTimer()).thenReturn(mockTimer); String body="[{ \"method\": \"" + SERVICE_NAME + "/v1.0/" + OP_NAME + "\", \"params\": [\"Hello\", 333], \"id\": \"1\"}," + "{ \"method\": \"" + SERVICE_NAME + "/v1.0/" + OP_NAME2 + "\", \"params\": [\"2009-01-01T00:00:00.333Z\"], \"id\": \"yyyy\"}]"; TestInputStream tis = new TestInputStream(new ByteArrayInputStream(body.getBytes("UTF-8"))); when(request.getInputStream()).thenReturn(tis); TestOutputStream tos = new TestOutputStream(); when(response.getOutputStream()).thenReturn(tos); CommandResolver<HttpCommand> resolver = commandProcessor.createCommandResolver(mockedCommand, tracer); assertNotNull(resolver); Iterable<ExecutionCommand> cmds = resolver.resolveExecutionCommands(); assertNotNull(cmds); assertTrue(cmds.iterator().hasNext()); Iterator<ExecutionCommand> iter = cmds.iterator(); //first command should be the OP_NAME ExecutionCommand executionCommand = iter.next(); assertNotNull(executionCommand); assertEquals(executionCommand.getOperationKey(), TEST_OP_KEY); executionCommand.onResult(new ExecutionResult()); assertTrue(iter.hasNext()); //Second should be OP_NAME2 - dateEcho executionCommand = iter.next(); assertNotNull(executionCommand); assertEquals(executionCommand.getOperationKey(), TEST_OP2_KEY); Object[] args = executionCommand.getArgs(); assertTrue(args.length == 1); //Very quick marshalling test for dates Calendar gc = new GregorianCalendar(2009, 0, 1, 0, 0, 0); gc.set(Calendar.MILLISECOND, 333); Date d = gc.getTime(); assertEquals(args[0], d); //Now we need to ensure that it attempts to write to the response when result is called executionCommand.onResult(new ExecutionResult(d)); String written = tos.getCapturedOutputStream(); int messageSep = written.indexOf("},{"); assertTrue(messageSep > -1); String firstResponse = written.substring(1, messageSep + 1); Map m = parseAndValidateResult(firstResponse, "1"); Object o = m.get("result"); assertNull(o); String secondResponse = written.substring(messageSep + 2, written.length() - 1); m = parseAndValidateResult(secondResponse, "yyyy"); Object result = m.get("result"); assertNotNull(result); Date resultDate = jdf.parse(result.toString()); assertEquals(resultDate, d); verify(logger).logAccess(eq(mockedCommand), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); verifyTracerCalls(); } @Test public void testExceptionalResultUnBatched() throws IOException { bindOperations(); HttpServletRequest request = mock(HttpServletRequest.class); when(request.getScheme()).thenReturn("http"); HttpServletResponse response = mock(HttpServletResponse.class); IdentityTokenResolver tokenResolver = mock(IdentityTokenResolver.class); HttpCommand mockedCommand = mock(HttpCommand.class); RequestTimer mockTimer = mock(RequestTimer.class); when(mockedCommand.getRequest()).thenReturn(request); when(mockedCommand.getResponse()).thenReturn(response); when(mockedCommand.getIdentityTokenResolver()).thenReturn(tokenResolver); when(mockedCommand.getStatus()).thenReturn(TransportCommand.CommandStatus.InProgress); when(mockedCommand.getTimer()).thenReturn(mockTimer); String body="{ \"method\": \"" + SERVICE_NAME + "/v1.0/" + OP_NAME + "\", \"params\": [\"Hello\", 333], \"id\": \"fail\"}"; TestInputStream tis = new TestInputStream(new ByteArrayInputStream(body.getBytes("UTF-8"))); when(request.getInputStream()).thenReturn(tis); TestOutputStream tos = new TestOutputStream(); when(response.getOutputStream()).thenReturn(tos); CommandResolver<HttpCommand> resolver = commandProcessor.createCommandResolver(mockedCommand, tracer); assertNotNull(resolver); Iterable<ExecutionCommand> cmds = resolver.resolveExecutionCommands(); assertNotNull(cmds); assertTrue(cmds.iterator().hasNext()); Iterator<ExecutionCommand> iter = cmds.iterator(); ExecutionCommand executionCommand = iter.next(); assertNotNull(executionCommand); assertEquals(executionCommand.getOperationKey(), TEST_OP_KEY); executionCommand.onResult(new ExecutionResult(new CougarServiceException(ServerFaultCode.MandatoryNotDefined, "Field x is not defined"))); String written = tos.getCapturedOutputStream(); Map m = parseAndValidateResult(written, "fail"); assertTrue(m.containsKey("error")); Map errorMap = (Map)m.get("error"); assertTrue(errorMap.containsKey("code")); assertTrue(((Integer)errorMap.get("code")) <= -32099); assertTrue(errorMap.containsKey("message")); assertTrue(((String)errorMap.get("message")).startsWith("DSC-")); verify(logger).logAccess(eq(mockedCommand), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); verifyTracerCalls(); } @Test public void testBatchMixedSuccessAndFailure() throws IOException { bindOperations(); //Try one call that works, one to a method that doesn't exist and another with a mandatory param missing HttpServletRequest request = mock(HttpServletRequest.class); when(request.getScheme()).thenReturn("http"); HttpServletResponse response = mock(HttpServletResponse.class); IdentityTokenResolver tokenResolver = mock(IdentityTokenResolver.class); HttpCommand mockedCommand = mock(HttpCommand.class); RequestTimer mockTimer = mock(RequestTimer.class); when(mockedCommand.getRequest()).thenReturn(request); when(mockedCommand.getResponse()).thenReturn(response); when(mockedCommand.getIdentityTokenResolver()).thenReturn(tokenResolver); when(mockedCommand.getStatus()).thenReturn(TransportCommand.CommandStatus.InProgress); when(mockedCommand.getTimer()).thenReturn(mockTimer); //Also note that we're mixing the case of the method call up - JSON rpc implementation //matches operations in a case insensitive fashion String body="[{ \"method\": \"thisMethodDoesntExist/v1.0/nonExistent\", \"params\": [], \"id\": \"1\"}," + "{ \"method\": \"" + SERVICE_NAME + "/v1.0/" + OP_NAME + "\", \"params\": [], \"id\": \"2\"}," + "{ \"method\": \"" + SERVICE_NAME.toLowerCase() + "/v1.0/" + OP_NAME.toUpperCase() + "\", \"params\": [\"Hello\", 333], \"id\": \"3\"}]"; TestInputStream tis = new TestInputStream(new ByteArrayInputStream(body.getBytes("UTF-8"))); when(request.getInputStream()).thenReturn(tis); TestOutputStream tos = new TestOutputStream(); when(response.getOutputStream()).thenReturn(tos); CommandResolver<HttpCommand> resolver = commandProcessor.createCommandResolver(mockedCommand, tracer); assertNotNull(resolver); Iterable<ExecutionCommand> cmds = resolver.resolveExecutionCommands(); assertNotNull(cmds); assertTrue(cmds.iterator().hasNext()); Iterator<ExecutionCommand> iter = cmds.iterator(); //This should lead to two executionCommands - the method that doesn't exist shouldn't //result in an ExecutionCommand ExecutionCommand cmd2 = iter.next(); ExecutionCommand cmd3 = iter.next(); assertNotNull(cmd2); assertNotNull(cmd3); assertFalse(iter.hasNext()); //Throw a mandatory param exception cmd2.onResult(new ExecutionResult(new CougarValidationException(ServerFaultCode.MandatoryNotDefined, "Mandatory params not defined"))); //Successful void result cmd3.onResult(new ExecutionResult()); String written = tos.getCapturedOutputStream(); List<Map> batchedResult = (List<Map>)objectMapper.readValue(written, List.class); Map result1 = findBatchedResultById(batchedResult, "1"); assertNotNull(result1); assertTrue(result1.containsKey("jsonrpc")); assertTrue(result1.get("jsonrpc").equals("2.0")); assertFalse(result1.containsKey("result")); assertTrue(result1.containsKey("error")); Map error = (Map)result1.get("error"); assertNotNull(error); assertTrue(error.containsKey("code")); //http://groups.google.com/group/json-rpc/web/json-rpc-1-2-proposal //lists errors codes etc. assertEquals(-32601, error.get("code")); //Result 2 - missing mandatory params Map result2 = findBatchedResultById(batchedResult, "2"); assertNotNull(result2); assertTrue(result2.containsKey("jsonrpc")); assertTrue(result2.get("jsonrpc").equals("2.0")); assertFalse(result2.containsKey("result")); assertTrue(result2.containsKey("error")); error = (Map)result2.get("error"); assertNotNull(error); assertTrue(error.containsKey("code")); assertEquals(-32602, error.get("code")); //Result 3 - everything fine. Map result3 = findBatchedResultById(batchedResult, "3"); assertNotNull(result3); assertTrue(result3.containsKey("jsonrpc")); assertTrue(result3.get("jsonrpc").equals("2.0")); assertTrue(result3.containsKey("result")); assertFalse(result3.containsKey("error")); verify(logger).logAccess(eq(mockedCommand), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); verifyTracerCalls(); } @Test public void testBatchBadRequest() throws IOException { bindOperations(); HttpServletRequest request = mock(HttpServletRequest.class); when(request.getScheme()).thenReturn("http"); HttpServletResponse response = mock(HttpServletResponse.class); IdentityTokenResolver tokenResolver = mock(IdentityTokenResolver.class); HttpCommand mockedCommand = mock(HttpCommand.class); RequestTimer mockTimer = mock(RequestTimer.class); when(mockedCommand.getRequest()).thenReturn(request); when(mockedCommand.getResponse()).thenReturn(response); when(mockedCommand.getIdentityTokenResolver()).thenReturn(tokenResolver); when(mockedCommand.getStatus()).thenReturn(TransportCommand.CommandStatus.InProgress); when(mockedCommand.getTimer()).thenReturn(mockTimer); String body="[{ \"this is never gonna parse\"}]"; TestInputStream tis = new TestInputStream(new ByteArrayInputStream(body.getBytes("UTF-8"))); when(request.getInputStream()).thenReturn(tis); TestOutputStream tos = new TestOutputStream(); when(response.getOutputStream()).thenReturn(tos); commandProcessor.createCommandResolver(mockedCommand, tracer); //What should happen is a total parse failure and something should be written with parse error -32700 String written = tos.getCapturedOutputStream(); Map result = objectMapper.readValue(written, Map.class); Map error = (Map)result.get("error"); assertTrue(error.containsKey("code")); assertEquals(-32700, error.get("code")); verify(logger).logAccess(eq(mockedCommand), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); verifyTracerCalls(); } @Test public void testCallsValidators() throws Exception { bindOperations(); HttpCommand command = mock(HttpCommand.class); HttpServletRequest request = mock(HttpServletRequest.class); when(request.getScheme()).thenReturn("http"); HttpServletResponse response = mock(HttpServletResponse.class); RequestTimer mockTimer = mock(RequestTimer.class); when(command.getRequest()).thenReturn(request); when(command.getResponse()).thenReturn(response); when(command.getTimer()).thenReturn(mockTimer); CommandValidator<HttpCommand> validator = mock(CommandValidator.class); validatorRegistry.addValidator(validator); //Also note that we're mixing the case of the method call up - JSON rpc implementation //matches operations in a case insensitive fashion String body="{ \"method\": \"" + SERVICE_NAME + "/v1.0/" + OP_NAME + "\", \"params\": [\"Hello\", 333], \"id\": 1}"; TestInputStream tis = new TestInputStream(new ByteArrayInputStream(body.getBytes("UTF-8"))); when(request.getInputStream()).thenReturn(tis); TestOutputStream tos = new TestOutputStream(); when(response.getOutputStream()).thenReturn(tos); commandProcessor.process(command); assertFalse(commandProcessor.errorCalled); verify(validator).validate(any(HttpCommand.class)); //verifyTracerCalls(); // doesn't call a real command, so doesn't callback to observer for tracer hook } @Test public void testStopsOnValidatorFail() throws Exception { bindOperations(); HttpCommand command = mock(HttpCommand.class); HttpServletRequest request = mock(HttpServletRequest.class); when(request.getScheme()).thenReturn("http"); HttpServletResponse response = mock(HttpServletResponse.class); RequestTimer mockTimer = mock(RequestTimer.class); when(command.getRequest()).thenReturn(request); when(command.getResponse()).thenReturn(response); when(command.getTimer()).thenReturn(mockTimer); TestOutputStream tos = new TestOutputStream(); when(response.getOutputStream()).thenReturn(tos); CommandValidator<HttpCommand> validator = new CommandValidator<HttpCommand>() { @Override public void validate(HttpCommand command) throws CougarException { throw new CougarServiceException(ServerFaultCode.SecurityException, "wibble"); } }; validatorRegistry.addValidator(validator); commandProcessor.process(command); assertTrue(commandProcessor.errorCalled); verify(logger).logAccess(eq(command), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); } @Test public void testIOExceptionDuringMapping() throws Exception { bindOperations(); HttpCommand command = mock(HttpCommand.class); HttpServletRequest request = mock(HttpServletRequest.class); when(request.getScheme()).thenReturn("http"); ServletInputStream is = mock(ServletInputStream.class); when(request.getInputStream()).thenReturn(is); when(is.read()).thenThrow(new IOException("i/o error")); when(is.read((byte[])any())).thenThrow(new IOException("i/o error")); when(is.read((byte[])any(),anyInt(),anyInt())).thenThrow(new IOException("i/o error")); HttpServletResponse response = mock(HttpServletResponse.class); RequestTimer mockTimer = mock(RequestTimer.class); when(command.getRequest()).thenReturn(request); when(command.getResponse()).thenReturn(response); when(command.getTimer()).thenReturn(mockTimer); TestOutputStream tos = new TestOutputStream(); when(response.getOutputStream()).thenReturn(tos); commandProcessor.process(command); assertTrue(commandProcessor.errorCalled); verify(logger).logAccess(eq(command), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); verifyTracerCalls(); } @Test public void testIOExceptionDueToTooMuchData() throws Exception { bindOperations(); HttpCommand command = mock(HttpCommand.class); HttpServletRequest request = mock(HttpServletRequest.class); when(request.getScheme()).thenReturn("http"); String body="{ \"method\": \"" + SERVICE_NAME + "/v1.0/" + OP_NAME + "\", \"params\": [\"Hello\", 333], \"id\": 1}"; TestInputStream tis = new TestInputStream(new ByteArrayInputStream(body.getBytes("UTF-8"))); when(request.getInputStream()).thenReturn(tis); commandProcessor.setMaxPostBodyLength(10); HttpServletResponse response = mock(HttpServletResponse.class); RequestTimer mockTimer = mock(RequestTimer.class); when(command.getRequest()).thenReturn(request); when(command.getResponse()).thenReturn(response); when(command.getTimer()).thenReturn(mockTimer); TestOutputStream tos = new TestOutputStream(); when(response.getOutputStream()).thenReturn(tos); commandProcessor.process(command); assertTrue(commandProcessor.errorCalled); verify(logger).logAccess(eq(command), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); verifyTracerCalls(); } @Test public void testRandomExecutionFailure() throws IOException { bindOperations(); HttpServletRequest request = mock(HttpServletRequest.class); when(request.getScheme()).thenReturn("http"); HttpServletResponse response = mock(HttpServletResponse.class); IdentityTokenResolver tokenResolver = mock(IdentityTokenResolver.class); HttpCommand mockedCommand = mock(HttpCommand.class); RequestTimer mockTimer = mock(RequestTimer.class); commandProcessor.setExecutionVenue(new RandomFailureEV(1)); when(mockedCommand.getRequest()).thenReturn(request); when(mockedCommand.getResponse()).thenReturn(response); when(mockedCommand.getIdentityTokenResolver()).thenReturn(tokenResolver); when(mockedCommand.getStatus()).thenReturn(TransportCommand.CommandStatus.InProgress); when(mockedCommand.getTimer()).thenReturn(mockTimer); Executor executor = new Executor() { @Override public void execute(Runnable command) { //Inline executor ... command.run(); } }; commandProcessor.setExecutor(executor); String body="{ \"method\": \"" + SERVICE_NAME + "/v1.0/" + OP_NAME + "\", \"params\": [\"hello\", 321], \"id\": \"1\"}"; TestInputStream tis = new TestInputStream(new ByteArrayInputStream(body.getBytes("UTF-8"))); when(request.getInputStream()).thenReturn(tis); TestOutputStream tos = new TestOutputStream(); when(response.getOutputStream()).thenReturn(tos); commandProcessor.process(mockedCommand); String written = tos.getCapturedOutputStream(); Map result = objectMapper.readValue(written, Map.class); Map error = (Map)result.get("error"); assertTrue(error.containsKey("code")); assertEquals(-32603, error.get("code")); verify(logger).logAccess(eq(mockedCommand), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); verifyTracerCalls(true); } /** * Tests that out of range numbers aren't shoe-horned into ints for non body parameters. */ @Test(expected=IllegalArgumentException.class) public void testInRangeIntegerForNonBodyParam() throws IOException { bindOperations(); ObjectMapper mapper = new JSONBindingFactory().createBaseObjectMapper(); String request = "{\"params\": [21474836470]}"; // If successfully narrowed to int, would be -10 JsonNode root = mapper.readTree(new ByteArrayInputStream(request.getBytes())); JsonRpcRequest rpc = mapper.convertValue(root, TypeFactory.defaultInstance().uncheckedSimpleType(JsonRpcRequest.class)); JsonNode paramValue = rpc.getParams().get(0); mapper.convertValue(paramValue, TypeFactory.defaultInstance().uncheckedSimpleType(Integer.class)); } /** * Tests that out of range numbers aren't shoe-horned into ints for body parameters. */ @Test(expected=IllegalArgumentException.class) public void testInRangeIntegerForBodyParam() throws IOException { bindOperations(); ObjectMapper mapper = new JSONBindingFactory().createBaseObjectMapper(); String request = "{\"params\": [{\"integer\":21474836470}]}"; JsonNode root = mapper.readTree(new ByteArrayInputStream(request.getBytes())); JsonRpcRequest rpc = mapper.convertValue(root, TypeFactory.defaultInstance().uncheckedSimpleType(JsonRpcRequest.class)); JsonNode paramValue = rpc.getParams().get(0); BodyType result = (BodyType) mapper.convertValue(paramValue, TypeFactory.defaultInstance().constructType(BodyType.class)); } /** * Tests that out of range numbers aren't shoe-horned into ints for non body parameters. */ @Test(expected=IllegalArgumentException.class) public void testInRangeLongForNonBodyParam() throws IOException { bindOperations(); ObjectMapper mapper = new JSONBindingFactory().createBaseObjectMapper(); String request = "{\"params\": [92233720368547758080]}"; // If successfully narrowed to long, would be 0 JsonNode root = mapper.readTree(new ByteArrayInputStream(request.getBytes())); JsonRpcRequest rpc = mapper.convertValue(root, TypeFactory.defaultInstance().uncheckedSimpleType(JsonRpcRequest.class)); JsonNode paramValue = rpc.getParams().get(0); System.out.println(mapper.convertValue(paramValue, TypeFactory.defaultInstance().uncheckedSimpleType(Long.class))); } /** * Tests that out of range numbers aren't shoe-horned into ints for body parameters. */ @Test(expected=IllegalArgumentException.class) public void testInRangeLongForBodyParam() throws IOException { bindOperations(); ObjectMapper mapper = new JSONBindingFactory().createBaseObjectMapper(); String request = "{\"params\": [{\"looong\":92233720368547758080}]}"; JsonNode root = mapper.readTree(new ByteArrayInputStream(request.getBytes())); JsonRpcRequest rpc = mapper.convertValue(root, TypeFactory.defaultInstance().uncheckedSimpleType(JsonRpcRequest.class)); JsonNode paramValue = rpc.getParams().get(0); BodyType result = (BodyType) mapper.convertValue(paramValue, TypeFactory.defaultInstance().constructType(BodyType.class)); } @Test public void createCommandResolver_NoTimeout() throws IOException { bindOperations(); HttpServletRequest request = mock(HttpServletRequest.class); when(request.getScheme()).thenReturn("http"); HttpServletResponse response = mock(HttpServletResponse.class); IdentityTokenResolver tokenResolver = mock(IdentityTokenResolver.class); HttpCommand mockedCommand = mock(HttpCommand.class); RequestTimer mockTimer = mock(RequestTimer.class); when(mockedCommand.getRequest()).thenReturn(request); when(mockedCommand.getResponse()).thenReturn(response); when(mockedCommand.getIdentityTokenResolver()).thenReturn(tokenResolver); when(mockedCommand.getStatus()).thenReturn(TransportCommand.CommandStatus.InProgress); when(mockedCommand.getTimer()).thenReturn(mockTimer); String body="{ \"method\": \"" + SERVICE_NAME + "/v1.0/" + OP_NAME + "\", \"params\": [\"Hello\", 333], \"id\": 1}"; TestInputStream tis = new TestInputStream(new ByteArrayInputStream(body.getBytes("UTF-8"))); when(request.getInputStream()).thenReturn(tis); TestOutputStream tos = new TestOutputStream(); when(response.getOutputStream()).thenReturn(tos); // resolve the command CommandResolver<HttpCommand> cr = commandProcessor.createCommandResolver(mockedCommand, tracer); Iterable<ExecutionCommand> executionCommands = cr.resolveExecutionCommands(); // check the output ExecutionCommand executionCommand = executionCommands.iterator().next(); TimeConstraints constraints = executionCommand.getTimeConstraints(); assertNull(constraints.getExpiryTime()); } @Test public void createCommandResolver_WithTimeout() throws IOException { bindOperations(); HttpServletRequest request = mock(HttpServletRequest.class); when(request.getScheme()).thenReturn("http"); HttpServletResponse response = mock(HttpServletResponse.class); IdentityTokenResolver tokenResolver = mock(IdentityTokenResolver.class); HttpCommand mockedCommand = mock(HttpCommand.class); RequestTimer mockTimer = mock(RequestTimer.class); when(mockedCommand.getRequest()).thenReturn(request); when(mockedCommand.getResponse()).thenReturn(response); when(mockedCommand.getIdentityTokenResolver()).thenReturn(tokenResolver); when(mockedCommand.getStatus()).thenReturn(TransportCommand.CommandStatus.InProgress); when(mockedCommand.getTimer()).thenReturn(mockTimer); String body="{ \"method\": \"" + SERVICE_NAME + "/v1.0/" + OP_NAME + "\", \"params\": [\"Hello\", 333], \"id\": 1}"; TestInputStream tis = new TestInputStream(new ByteArrayInputStream(body.getBytes("UTF-8"))); when(request.getInputStream()).thenReturn(tis); TestOutputStream tos = new TestOutputStream(); when(response.getOutputStream()).thenReturn(tos); // resolve the command when(request.getHeader("X-RequestTimeout")).thenReturn("10000"); when(context.getRequestTime()).thenReturn(new Date()); CommandResolver<HttpCommand> cr = commandProcessor.createCommandResolver(mockedCommand, tracer); Iterable<ExecutionCommand> executionCommands = cr.resolveExecutionCommands(); // check the output ExecutionCommand executionCommand = executionCommands.iterator().next(); TimeConstraints constraints = executionCommand.getTimeConstraints(); assertNotNull(constraints.getExpiryTime()); } @Test public void createCommandResolver_WithTimeoutAndOldRequestTime() throws IOException { bindOperations(); HttpServletRequest request = mock(HttpServletRequest.class); when(request.getScheme()).thenReturn("http"); HttpServletResponse response = mock(HttpServletResponse.class); IdentityTokenResolver tokenResolver = mock(IdentityTokenResolver.class); HttpCommand mockedCommand = mock(HttpCommand.class); RequestTimer mockTimer = mock(RequestTimer.class); when(mockedCommand.getRequest()).thenReturn(request); when(mockedCommand.getResponse()).thenReturn(response); when(mockedCommand.getIdentityTokenResolver()).thenReturn(tokenResolver); when(mockedCommand.getStatus()).thenReturn(TransportCommand.CommandStatus.InProgress); when(mockedCommand.getTimer()).thenReturn(mockTimer); String body="{ \"method\": \"" + SERVICE_NAME + "/v1.0/" + OP_NAME + "\", \"params\": [\"Hello\", 333], \"id\": 1}"; TestInputStream tis = new TestInputStream(new ByteArrayInputStream(body.getBytes("UTF-8"))); when(request.getInputStream()).thenReturn(tis); TestOutputStream tos = new TestOutputStream(); when(response.getOutputStream()).thenReturn(tos); // resolve the command when(request.getHeader("X-RequestTimeout")).thenReturn("10000"); when(context.getRequestTime()).thenReturn(new Date(System.currentTimeMillis() - 10001)); CommandResolver<HttpCommand> cr = commandProcessor.createCommandResolver(mockedCommand, tracer); Iterable<ExecutionCommand> executionCommands = cr.resolveExecutionCommands(); // check the output ExecutionCommand executionCommand = executionCommands.iterator().next(); TimeConstraints constraints = executionCommand.getTimeConstraints(); assertTrue(constraints.getExpiryTime() < System.currentTimeMillis()); } public static class BodyType { private Integer integer; private Long looong; public Integer getInteger() { return integer; } public void setInteger(Integer integer) { this.integer = integer; } public Long getLooong() { return looong; } public void setLooong(Long looong) { this.looong = looong; } } private Map findBatchedResultById(List<Map> batchedResults, String idToSearchFor) { for (Map batchedResult : batchedResults) { if (batchedResult.containsKey("id") && batchedResult.get("id").equals(idToSearchFor)) { return batchedResult; } } return null; } private Map parseAndValidateResult(String jsonResponse, String id) throws IOException { Map map = (Map)objectMapper.readValue(jsonResponse, Map.class); //A success result should always include jsonrpc 2.0 assertTrue(map.containsKey("jsonrpc")); assertEquals("2.0", map.get("jsonrpc")); //Should also always include the original ID assertTrue(map.containsKey("id")); assertEquals(id, map.get("id")); return map; } public void testBatchedWithFailures() { // todo: empty test? ContentTypeNormaliser ctn = mock(ContentTypeNormaliser.class); } private static class TestOutputStream extends ServletOutputStream { private ByteArrayOutputStream delegate; public TestOutputStream () { this.delegate = new ByteArrayOutputStream(); } @Override public void write(int b) throws IOException { delegate.write(b); } public String getCapturedOutputStream() { return new String(delegate.toByteArray()); } @Override public boolean isReady() { return false; //To change body of implemented methods use File | Settings | File Templates. } @Override public void setWriteListener(WriteListener writeListener) { //To change body of implemented methods use File | Settings | File Templates. } } private static class TestInputStream extends ServletInputStream { private InputStream delegate; public TestInputStream(InputStream delegate) { this.delegate = delegate; } @Override public int read() throws IOException { return delegate.read(); } @Override public boolean isFinished() { return false; //To change body of implemented methods use File | Settings | File Templates. } @Override public boolean isReady() { return false; //To change body of implemented methods use File | Settings | File Templates. } @Override public void setReadListener(ReadListener readListener) { //To change body of implemented methods use File | Settings | File Templates. } } private static class NonsenseTCP extends AbstractCommandProcessor { @Override protected CommandResolver createCommandResolver(TransportCommand command, Tracer tracer) { return null; } @Override protected void writeErrorResponse(TransportCommand command, DehydratedExecutionContext context, CougarException e, boolean traceStarted) { } @Override public void bind(ServiceBindingDescriptor serviceBindingDescriptor) { } @Override protected List<CommandValidator<HttpCommand>> getCommandValidators() { return Collections.EMPTY_LIST; } } private static class RandomFailureEV implements ExecutionVenue { private AtomicInteger remainingOk; private RandomFailureEV(int okCalls) { remainingOk = new AtomicInteger(okCalls); } @Override public void registerOperation(String namespace, OperationDefinition def, Executable executable, ExecutionTimingRecorder recorder, long maxExecutionTime) { } @Override public OperationDefinition getOperationDefinition(OperationKey key) { return null; } @Override public Set<OperationKey> getOperationKeys() { return null; } @Override public void execute(ExecutionContext ctx, OperationKey key, Object[] args, ExecutionObserver observer, TimeConstraints clientExpiryTime) { if (remainingOk.getAndDecrement()>0) { observer.onResult(new ExecutionResult(null)); } throw new NullPointerException("BANG"); } @Override public void execute(ExecutionContext ctx, OperationKey key, Object[] args, ExecutionObserver observer, Executor executor, TimeConstraints clientExpiryTime) { if (remainingOk.getAndDecrement()>0) { observer.onResult(new ExecutionResult(null)); } throw new NullPointerException("BANG"); } @Override public void setPreProcessors(List<ExecutionPreProcessor> preProcessorList) { } @Override public void setPostProcessors(List<ExecutionPostProcessor> preProcessorList) { } } private class LocalJsonRpcCommandProcessor extends JsonRpcTransportCommandProcessor { private boolean errorCalled; private LocalJsonRpcCommandProcessor(DehydratedExecutionContextResolution contextResolution, JSONBindingFactory jsonBindingFactory) { super(contextResolution, "X-RequestTimeout", jsonBindingFactory.createBaseObjectMapper()); } /* public LocalJsonRpcCommandProcessor(RequestTimeResolver requestTimeResolver, JSONBindingFactory jsonBindingFactory) { super(geoIPLocator, new DefaultGeoLocationDeserializer(), "X-UUID", "X-UUID-Parents", "X-RequestTimeout", requestTimeResolver, new InferredCountryResolver<HttpServletRequest>() { public String inferCountry(HttpServletRequest input) { return AZ;} }, jsonBindingFactory); } public LocalJsonRpcCommandProcessor(GeoIPLocator geoIPLocator, GeoLocationDeserializer deserializer, String uuidHeader, String uuidParentsHeader, RequestTimeResolver requestTimeResolver, JSONBindingFactory jsonBindingFactory) { super(geoIPLocator, deserializer, uuidHeader, uuidParentsHeader, "X-RequestTimeout", requestTimeResolver, jsonBindingFactory); } */ @Override public void writeErrorResponse(HttpCommand command, DehydratedExecutionContext context, CougarException e, boolean traceStarted) { errorCalled = true; super.writeErrorResponse(command, context, e, traceStarted); } @Override // only to make it public public DehydratedExecutionContext resolveExecutionContext(HttpCommand http, Void cc) { return super.resolveExecutionContext(http, null); } } private class TestBaseExecutionVenue extends BaseExecutionVenue { private List<ExecutionRequest> requests = new ArrayList<ExecutionRequest>(); @Override public void execute(ExecutionContext ctx, OperationKey key, Object[] args, ExecutionObserver observer, TimeConstraints expirtyTime) { requests.add(new ExecutionRequest(ctx, key, args, observer)); super.execute(ctx, key, args, observer, expirtyTime); } } private class ExecutionRequest { ExecutionContext ctx; OperationKey key; Object[] args; ExecutionObserver observer; private ExecutionRequest(ExecutionContext ctx, OperationKey key, Object[] args, ExecutionObserver observer) { this.ctx = ctx; this.key = key; this.args = args; this.observer = observer; } } }