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    }