001 package com.mockrunner.connector;
002
003 import java.util.HashMap;
004 import java.util.Iterator;
005 import java.util.Map;
006
007 import javax.resource.ResourceException;
008 import javax.resource.cci.InteractionSpec;
009 import javax.resource.cci.MappedRecord;
010 import javax.resource.cci.Record;
011
012 import com.mockrunner.base.NestedApplicationException;
013 import com.mockrunner.mock.connector.cci.MockMappedRecord;
014
015 /**
016 * This interaction implementor works with mapped records. It takes a <code>Map</code> for
017 * the request and a <code>Map</code> or a <code>Record</code> instance for the response.
018 * If the request <code>Map</code> is <code>null</code>, which is the default,
019 * the implementor accepts any request and returns the specified result. If a request
020 * <code>Map</code> is specified, this implementor accepts only requests that contain the same
021 * data as the specified expected request <code>Map</code>. The underlying maps are compared
022 * as described in the <code>Map.equals</code> method.
023 * If a request is accepted, this implementor replies with the specified
024 * response. You can use the various constructors and <code>set</code> methods
025 * to configure the expected request and the response.<br>
026 * Please check out the documentation of the various methods for details.
027 */
028 public class MappedRecordInteraction implements InteractionImplementor
029 {
030 private boolean enabled;
031 private Map expectedRequest;
032 private Map responseData;
033 private Class responseClass;
034 private Record responseRecord;
035
036 /**
037 * Sets the expected request and the response to <code>null</code>,
038 * i.e. an empty response is returned for every request.
039 */
040 public MappedRecordInteraction()
041 {
042 this(null, null, MockMappedRecord.class);
043 }
044
045 /**
046 * Sets the expected request to <code>null</code> and prepares
047 * the specified response <code>Map</code>. The response class for the
048 * {@link #execute(InteractionSpec,Record)} method is set
049 * to the default {@link com.mockrunner.mock.connector.cci.MockMappedRecord}.
050 * It is allowed to pass <code>null</code> for the response <code>Map</code>
051 * which is equivalent to an empty response.
052 * The specified response is returned for every request.
053 * @param responseMap the response <code>Map</code>
054 */
055 public MappedRecordInteraction(Map responseMap)
056 {
057 this(null, responseMap, MockMappedRecord.class);
058 }
059
060 /**
061 * Sets the specified expected request <code>Map</code> and prepares
062 * the specified response <code>Map</code>. The response class for the
063 * {@link #execute(InteractionSpec,Record)} method is set
064 * to the default {@link com.mockrunner.mock.connector.cci.MockMappedRecord}.
065 * It is allowed to pass <code>null</code> for the request and response <code>Map</code>
066 * which is equivalent to an empty expected request (i.e. every request is accepted)
067 * or to an empty response respectively.
068 * The specified response is returned, if the actual request matches the specified expected
069 * request.
070 * @param expectedRequest the expected request <code>Map</code>
071 * @param responseMap the response <code>Map</code>
072 */
073 public MappedRecordInteraction(Map expectedRequest, Map responseMap)
074 {
075 this(expectedRequest, responseMap, MockMappedRecord.class);
076 }
077
078 /**
079 * Sets the expected request to <code>null</code> and prepares
080 * the specified response <code>Map</code>. The response class for the
081 * {@link #execute(InteractionSpec,Record)} method is set
082 * to the specified <code>responseClass</code>. The specified
083 * <code>responseClass</code> must implement <code>MappedRecord</code>,
084 * otherwise an <code>IllegalArgumentException</code> will be thrown.
085 * It is allowed to pass <code>null</code> for the response <code>Map</code>
086 * which is equivalent to an empty response.
087 * The specified response is returned for every request.
088 * @param responseMap the response <code>Map</code
089 * @param responseClass the response <code>Record</code> class
090 * @throws IllegalArgumentException if the <code>responseClass</code>
091 * is not valid
092 */
093 public MappedRecordInteraction(Map responseMap, Class responseClass)
094 {
095 this(null, responseMap, responseClass);
096 }
097
098 /**
099 * Sets the specified expected request <code>Map</code> and prepares
100 * the specified response <code>Map</code>. The response class for the
101 * {@link #execute(InteractionSpec,Record)} method is set
102 * to the specified <code>responseClass</code>. The specified
103 * <code>responseClass</code> must implement <code>MappedRecord</code>,
104 * otherwise an <code>IllegalArgumentException</code> will be thrown.
105 * It is allowed to pass <code>null</code> for the request and response <code>Map</code>
106 * which is equivalent to an empty expected request (i.e. every request is accepted)
107 * or to an empty response respectively.
108 * The specified response is returned, if the actual request matches the specified expected
109 * request.
110 * @param expectedRequest the expected request <code>Map</code>
111 * @param responseMap the response <code>Map</code
112 * @param responseClass the response <code>Record</code> class
113 * @throws IllegalArgumentException if the <code>responseClass</code>
114 * is not valid
115 */
116 public MappedRecordInteraction(Map expectedRequest, Map responseMap, Class responseClass)
117 {
118 setExpectedRequest(expectedRequest);
119 setResponse(responseMap, responseClass);
120 this.enabled = true;
121 }
122
123 /**
124 * Sets the specified expected request <code>Map</code> and the response
125 * <code>Record</code> for the {@link #execute(InteractionSpec, Record)}
126 * method. The response <code>Record</code> is ignored for
127 * {@link #execute(InteractionSpec,Record,Record)} but takes precedence
128 * over the specified response <code>Map</code> for {@link #execute(InteractionSpec, Record)}.
129 * It is allowed to pass <code>null</code> for the request and response <code>Record</code>
130 * which is equivalent to an empty expected request (i.e. every request is accepted)
131 * or to no specified response <code>Record</code>, i.e. the specified response
132 * <code>Map</code> is taken.
133 * The specified response is returned, if the actual request matches the specified expected
134 * request.
135 * @param expectedRequest the expected request <code>Map</code>
136 * @param responseRecord the response <code>Record</code>
137 */
138 public MappedRecordInteraction(Map expectedRequest, Record responseRecord)
139 {
140 setExpectedRequest(expectedRequest);
141 setResponse(responseRecord);
142 this.enabled = true;
143 }
144
145 /**
146 * Sets the expected request to <code>null</code> and prepares the response
147 * <code>Record</code> for the {@link #execute(InteractionSpec, Record)}
148 * method. The response <code>Record</code> is ignored for
149 * {@link #execute(InteractionSpec,Record,Record)} but takes precedence
150 * over the specified response <code>Map</code> for {@link #execute(InteractionSpec, Record)}.
151 * It is allowed to pass <code>null</code> for the response <code>Record</code>
152 * which is equivalent to no specified response <code>Record</code>, i.e. the specified response
153 * <code>Map</code> is taken.
154 * The specified response is returned for every request.
155 * @param responseRecord the response <code>Record</code>
156 */
157 public MappedRecordInteraction(Record responseRecord)
158 {
159 this(null, responseRecord);
160 }
161
162 /**
163 * Enables this implementor.
164 */
165 public void enable()
166 {
167 this.enabled = true;
168 }
169
170 /**
171 * Disables this implementor. {@link #canHandle(InteractionSpec, Record, Record)}
172 * always returns <code>false</code>, if this implementor is disabled.
173 */
174 public void disable()
175 {
176 this.enabled = false;
177 }
178
179 /**
180 * Sets the specified expected request <code>Map</code>. The response is returned,
181 * if the actual request matches the specified expected request <code>Map</code>
182 * according to <code>Map.equals</code>.
183 * It is allowed to pass <code>null</code> for the request <code>Map</code>
184 * which is equivalent to an empty expected request (i.e. every request
185 * is accepted).
186 * @param expectedRequest the expected request <code>Map</code>
187 */
188 public void setExpectedRequest(Map expectedRequest)
189 {
190 if(null == expectedRequest)
191 {
192 this.expectedRequest = null;
193 }
194 else
195 {
196 this.expectedRequest = new HashMap(expectedRequest);
197 }
198 }
199
200 /**
201 * Prepares the specified response <code>Map</code>. The response class for the
202 * {@link #execute(InteractionSpec,Record)} method is set
203 * to the default {@link com.mockrunner.mock.connector.cci.MockMappedRecord}.
204 * It is allowed to pass <code>null</code> for the response <code>Map</code>
205 * which is equivalent to an empty response.
206 * @param responseMap the response <code>Map</code
207 */
208 public void setResponse(Map responseMap)
209 {
210 setResponse(responseMap, MockMappedRecord.class);
211 }
212
213 /**
214 * Prepares the specified response <code>Map</code>. The response class for the
215 * {@link #execute(InteractionSpec,Record)} method is set
216 * to the specified <code>responseClass</code>. The specified
217 * <code>responseClass</code> must implement <code>MappedRecord</code>,
218 * otherwise an <code>IllegalArgumentException</code> will be thrown.
219 * It is allowed to pass <code>null</code> for the response <code>Map</code>
220 * which is equivalent to an empty response.
221 * @param responseMap the response <code>Map</code>
222 * @param responseClass the response <code>Record</code> class
223 * @throws IllegalArgumentException if the <code>responseClass</code>
224 * is not valid
225 */
226 public void setResponse(Map responseMap, Class responseClass)
227 {
228 if(!isResponseClassAcceptable(responseClass))
229 {
230 throw new IllegalArgumentException("responseClass must implement " + MappedRecord.class.getName());
231 }
232 if(null == responseMap)
233 {
234 this.responseData = null;
235 }
236 else
237 {
238 this.responseData = new HashMap(responseMap);
239 }
240 this.responseClass = responseClass;
241 }
242
243 /**
244 * Prepares the response <code>Record</code> for the
245 * {@link #execute(InteractionSpec, Record)} method. The response
246 * <code>Record</code> is ignored for {@link #execute(InteractionSpec,Record,Record)}
247 * but takes precedence over the specified response <code>Map</code> for
248 * {@link #execute(InteractionSpec, Record)}.
249 * It is allowed to pass <code>null</code> for the response <code>Record</code>
250 * which is equivalent to no specified response <code>Record</code>, i.e. the specified response
251 * <code>Map</code> is taken.
252 * @param responseRecord the response <code>Record</code>
253 */
254 public void setResponse(Record responseRecord)
255 {
256 this.responseRecord = responseRecord;
257 }
258
259 /**
260 * Returns <code>true</code> if this implementor is enabled and will handle the request.
261 * This method returns <code>true</code> if the following prerequisites are fulfilled:<br><br>
262 * It is enabled.<br><br>
263 * The response <code>Record</code> must implement <code>MappedRecord</code>
264 * or it must be <code>null</code> (which is the case, if the actual request
265 * targets the {@link #execute(InteractionSpec,Record)} method instead of
266 * {@link #execute(InteractionSpec,Record,Record)}).<br><br>
267 * The expected request must be <code>null</code> (use the various
268 * <code>setExpectedRequest</code> methods) or the actual request <code>Record</code>
269 * must implement <code>MappedRecord</code> and must contain the same data as
270 * the specified expected request <code>Map</code> according to <code>Map.equals</code>.<br><br>
271 * Otherwise, <code>false</code> is returned.
272 * @param interactionSpec the <code>InteractionSpec</code> for the actual call
273 * @param actualRequest the request for the actual call
274 * @param actualResponse the response for the actual call, may be <code>null</code>
275 * @return <code>true</code> if this implementor will handle the request and
276 * will return the specified response, <code>false</code> otherwise
277 */
278 public boolean canHandle(InteractionSpec interactionSpec, Record actualRequest, Record actualResponse)
279 {
280 if(!enabled) return false;
281 if(!isResponseAcceptable(actualResponse)) return false;
282 return doesRequestMatch(actualRequest);
283 }
284
285 private boolean doesRequestMatch(Record request)
286 {
287 if(null == expectedRequest) return true;
288 if(null == request) return false;
289 if(request instanceof MappedRecord)
290 {
291 try
292 {
293 MappedRecord mappedRequest = (MappedRecord)request;
294 if(mappedRequest.size() != expectedRequest.size()) return false;
295 Iterator keys = mappedRequest.keySet().iterator();
296 while(keys.hasNext())
297 {
298 Object nextKey = keys.next();
299 Object actualValue = mappedRequest.get(nextKey);
300 Object expectedValue = expectedRequest.get(nextKey);
301 if(!areObjectsEquals(actualValue, expectedValue))
302 {
303 return false;
304 }
305 }
306 return true;
307 }
308 catch(Exception exc)
309 {
310 throw new NestedApplicationException(exc);
311 }
312 }
313 return false;
314 }
315
316 private boolean areObjectsEquals(Object object1, Object object2)
317 {
318 if(null == object1 && null == object2) return true;
319 if(null == object1) return false;
320 return object1.equals(object2);
321 }
322
323 private boolean isResponseAcceptable(Record response)
324 {
325 return (null == response) || (response instanceof MappedRecord);
326 }
327
328 private boolean isResponseClassAcceptable(Class responseClass)
329 {
330 return (null == responseClass) || (MappedRecord.class.isAssignableFrom(responseClass));
331 }
332
333 /**
334 * First version of the <code>execute</code> methods.<br><br>
335 * This method returns <code>null</code>, if the request does not match
336 * according to the contract of {@link #canHandle}. This never happens under
337 * normal conditions since the {@link InteractionHandler} does not call
338 * <code>execute</code>, if {@link #canHandle} returns <code>false</code>.
339 * <br><br>
340 * Otherwise, this method returns the specified response. If a response
341 * <code>Record</code> object is specified (use {@link #setResponse(Record)}),
342 * it always takes precedence, i.e. the response <code>Map</code> will be ignored.
343 * If no <code>Record</code> object is specified, a <code>Record</code> object
344 * is created and filled with the specified response <code>Map</code> data. Use the
345 * <code>setResponse</code> methods that take a <code>Map</code>
346 * to prepare the response <code>Map</code>. The created <code>Record</code> is of the the
347 * specified type (the <code>setResponse</code> method that takes a second
348 * <code>Class</code> parameter allows for specifying a type). If no type
349 * is specified, a {@link com.mockrunner.mock.connector.cci.MockMappedRecord}
350 * is created. If no response <code>Map</code> is specified at all, an empty
351 * {@link com.mockrunner.mock.connector.cci.MockMappedRecord}
352 * will be returned.
353 * @param interactionSpec the interaction spec
354 * @param actualRequest the actual request
355 * @return the response according to the current request
356 */
357 public Record execute(InteractionSpec interactionSpec, Record actualRequest) throws ResourceException
358 {
359 if(!canHandle(interactionSpec, actualRequest, null)) return null;
360 if(null != responseRecord) return responseRecord;
361 MappedRecord response = null;
362 try
363 {
364 if(null == responseClass)
365 {
366 response = new MockMappedRecord();
367 }
368 else
369 {
370 response = (MappedRecord)responseClass.newInstance();
371 }
372 if(null != responseData)
373 {
374 response.putAll(responseData);
375 }
376 }
377 catch(Exception exc)
378 {
379 ResourceException resExc = new ResourceException("execute() failed");
380 resExc.setLinkedException(exc);
381 throw resExc;
382 }
383 return (Record)response;
384 }
385
386 /**
387 * Second version of the <code>execute</code> methods.<br><br>
388 * This method returns <code>false</code>, if the request does not match
389 * according to the contract of {@link #canHandle}. This never happens under
390 * normal conditions since the {@link InteractionHandler} does not call
391 * <code>execute</code>, if {@link #canHandle} returns <code>false</code>.
392 * <br><br>
393 * Otherwise, this method fills the response <code>Record</code> with the
394 * specified response <code>Map</code> data. Use the <code>setResponse</code> methods that
395 * take a <code>Map</code> to prepare the response <code>Map</code>.
396 * The response <code>Record</code> must implement <code>MappedRecord</code>
397 * (it does, otherwise the request would have been rejected by
398 * {@link #canHandle}). If no response <code>Map</code> is specified at all,
399 * the response <code>Record</code> is not touched but <code>true</code>
400 * is returned anyway
401 * @param interactionSpec the interaction spec
402 * @param actualRequest the actual request
403 * @param actualResponse the actual response
404 * @return <code>true</code> under normal conditions
405 */
406 public boolean execute(InteractionSpec interactionSpec, Record actualRequest, Record actualResponse) throws ResourceException
407 {
408 if(!canHandle(interactionSpec, actualRequest, actualResponse)) return false;
409 try
410 {
411 if(null != responseData && null != actualResponse)
412 {
413 ((MappedRecord)actualResponse).clear();
414 ((MappedRecord)actualResponse).putAll(responseData);
415 }
416 }
417 catch(Exception exc)
418 {
419 ResourceException resExc = new ResourceException("execute() failed");
420 resExc.setLinkedException(exc);
421 throw resExc;
422 }
423 return true;
424 }
425 }