1/*
2 * Copyright 2001-2009 OFFIS, Tammo Freese
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package org.easymock.internal;
17
18import java.io.Serializable;
19import java.lang.reflect.Method;
20import java.util.HashMap;
21import java.util.List;
22import java.util.Map;
23
24import org.easymock.IAnswer;
25import org.easymock.IArgumentMatcher;
26
27public class RecordState implements IMocksControlState, Serializable {
28
29    private static final long serialVersionUID = -5418279681566430252L;
30
31    private ExpectedInvocation lastInvocation = null;
32
33    private boolean lastInvocationUsed = true;
34
35    private Result lastResult;
36
37    private final IMocksBehavior behavior;
38
39    private static Map<Class<?>, Object> emptyReturnValues = new HashMap<Class<?>, Object>();
40
41    static {
42        emptyReturnValues.put(Void.TYPE, null);
43        emptyReturnValues.put(Boolean.TYPE, Boolean.FALSE);
44        emptyReturnValues.put(Byte.TYPE, Byte.valueOf((byte) 0));
45        emptyReturnValues.put(Short.TYPE, Short.valueOf((short) 0));
46        emptyReturnValues.put(Character.TYPE, Character.valueOf((char)0));
47        emptyReturnValues.put(Integer.TYPE, Integer.valueOf(0));
48        emptyReturnValues.put(Long.TYPE, Long.valueOf(0));
49        emptyReturnValues.put(Float.TYPE, Float.valueOf(0));
50        emptyReturnValues.put(Double.TYPE, Double.valueOf(0));
51    }
52
53    private static Map<Class<?>, Class<?>> primitiveToWrapperType = new HashMap<Class<?>, Class<?>>();
54
55    static {
56        primitiveToWrapperType.put(Boolean.TYPE, Boolean.class);
57        primitiveToWrapperType.put(Byte.TYPE, Byte.class);
58        primitiveToWrapperType.put(Short.TYPE, Short.class);
59        primitiveToWrapperType.put(Character.TYPE, Character.class);
60        primitiveToWrapperType.put(Integer.TYPE, Integer.class);
61        primitiveToWrapperType.put(Long.TYPE, Long.class);
62        primitiveToWrapperType.put(Float.TYPE, Float.class);
63        primitiveToWrapperType.put(Double.TYPE, Double.class);
64    }
65
66    public RecordState(IMocksBehavior behavior) {
67        this.behavior = behavior;
68    }
69
70    public void assertRecordState() {
71    }
72
73    public java.lang.Object invoke(Invocation invocation) {
74        closeMethod();
75        List<IArgumentMatcher> lastMatchers = LastControl.pullMatchers();
76        lastInvocation = new ExpectedInvocation(invocation, lastMatchers);
77        lastInvocationUsed = false;
78        return emptyReturnValueFor(invocation.getMethod().getReturnType());
79    }
80
81    public void replay() {
82        closeMethod();
83        if (LastControl.pullMatchers() != null) {
84            throw new IllegalStateException("matcher calls were used outside expectations");
85        }
86    }
87
88    public void verify() {
89        throw new RuntimeExceptionWrapper(new IllegalStateException(
90                "calling verify is not allowed in record state"));
91    }
92
93    public void andReturn(Object value) {
94        requireMethodCall("return value");
95        value = convertNumberClassIfNeccessary(value);
96        requireAssignable(value);
97        if (lastResult != null) {
98            times(MocksControl.ONCE);
99        }
100        lastResult = Result.createReturnResult(value);
101    }
102
103    public void andThrow(Throwable throwable) {
104        requireMethodCall("Throwable");
105        requireValidThrowable(throwable);
106        if (lastResult != null) {
107            times(MocksControl.ONCE);
108        }
109        lastResult = Result.createThrowResult(throwable);
110    }
111
112    public void andAnswer(IAnswer<?> answer) {
113        requireMethodCall("answer");
114        requireValidAnswer(answer);
115        if (lastResult != null) {
116            times(MocksControl.ONCE);
117        }
118        lastResult = Result.createAnswerResult(answer);
119    }
120
121    public void andDelegateTo(Object delegateTo) {
122        requireMethodCall("delegate");
123        requireValidDelegation(delegateTo);
124        if (lastResult != null) {
125            times(MocksControl.ONCE);
126        }
127        lastResult = Result.createDelegatingResult(delegateTo);
128    }
129
130    public void andStubReturn(Object value) {
131        requireMethodCall("stub return value");
132        value = convertNumberClassIfNeccessary(value);
133        requireAssignable(value);
134        if (lastResult != null) {
135            times(MocksControl.ONCE);
136        }
137        behavior.addStub(lastInvocation, Result.createReturnResult(value));
138        lastInvocationUsed = true;
139    }
140
141    @SuppressWarnings("deprecation")
142    public void setDefaultReturnValue(Object value) {
143        requireMethodCall("default return value");
144        value = convertNumberClassIfNeccessary(value);
145        requireAssignable(value);
146        if (lastResult != null) {
147            times(MocksControl.ONCE);
148        }
149        behavior.addStub(
150                lastInvocation.withMatcher(org.easymock.MockControl.ALWAYS_MATCHER), Result
151                        .createReturnResult(value));
152        lastInvocationUsed = true;
153    }
154
155    public void asStub() {
156        requireMethodCall("stub behavior");
157        requireVoidMethod();
158        behavior.addStub(lastInvocation, Result.createReturnResult(null));
159        lastInvocationUsed = true;
160    }
161
162    @SuppressWarnings("deprecation")
163    public void setDefaultVoidCallable() {
164        requireMethodCall("default void callable");
165        requireVoidMethod();
166        behavior.addStub(
167                lastInvocation.withMatcher(org.easymock.MockControl.ALWAYS_MATCHER), Result
168                        .createReturnResult(null));
169        lastInvocationUsed = true;
170    }
171
172    public void andStubThrow(Throwable throwable) {
173        requireMethodCall("stub Throwable");
174        requireValidThrowable(throwable);
175        if (lastResult != null) {
176            times(MocksControl.ONCE);
177        }
178        behavior.addStub(lastInvocation, Result.createThrowResult(throwable));
179        lastInvocationUsed = true;
180    }
181
182    @SuppressWarnings("deprecation")
183    public void setDefaultThrowable(Throwable throwable) {
184        requireMethodCall("default Throwable");
185        requireValidThrowable(throwable);
186        if (lastResult != null) {
187            times(MocksControl.ONCE);
188        }
189        behavior.addStub(
190                lastInvocation.withMatcher(org.easymock.MockControl.ALWAYS_MATCHER), Result
191                        .createThrowResult(throwable));
192        lastInvocationUsed = true;
193    }
194
195    public void andStubAnswer(IAnswer<?> answer) {
196        requireMethodCall("stub answer");
197        requireValidAnswer(answer);
198        if (lastResult != null) {
199            times(MocksControl.ONCE);
200        }
201        behavior.addStub(lastInvocation, Result.createAnswerResult(answer));
202        lastInvocationUsed = true;
203    }
204
205    public void andStubDelegateTo(Object delegateTo) {
206        requireMethodCall("stub delegate");
207        requireValidDelegation(delegateTo);
208        if (lastResult != null) {
209            times(MocksControl.ONCE);
210        }
211        behavior.addStub(lastInvocation, Result
212                .createDelegatingResult(delegateTo));
213        lastInvocationUsed = true;
214    }
215
216    public void times(Range range) {
217        requireMethodCall("times");
218        requireLastResultOrVoidMethod();
219
220        behavior.addExpected(lastInvocation, lastResult != null ? lastResult
221                : Result.createReturnResult(null), range);
222        lastInvocationUsed = true;
223        lastResult = null;
224    }
225
226    private Object createNumberObject(Object value, Class<?> returnType) {
227        if (!(value instanceof Number)) {
228            return value;
229        }
230        Number number = (Number) value;
231        if (returnType.equals(Byte.TYPE)) {
232            return number.byteValue();
233        } else if (returnType.equals(Short.TYPE)) {
234            return number.shortValue();
235        } else if (returnType.equals(Character.TYPE)) {
236            return (char) number.intValue();
237        } else if (returnType.equals(Integer.TYPE)) {
238            return number.intValue();
239        } else if (returnType.equals(Long.TYPE)) {
240            return number.longValue();
241        } else if (returnType.equals(Float.TYPE)) {
242            return number.floatValue();
243        } else if (returnType.equals(Double.TYPE)) {
244            return number.doubleValue();
245        } else {
246            return number;
247        }
248    }
249
250    private Object convertNumberClassIfNeccessary(Object o) {
251        Class<?> returnType = lastInvocation.getMethod().getReturnType();
252        return createNumberObject(o, returnType);
253    }
254
255    @SuppressWarnings("deprecation")
256    private void closeMethod() {
257        if (lastInvocationUsed && lastResult == null) {
258            return;
259        }
260        if (!isLastResultOrVoidMethod()) {
261            throw new RuntimeExceptionWrapper(new IllegalStateException(
262                    "missing behavior definition for the preceding method call "
263                            + lastInvocation.toString()));
264        }
265        this.times(org.easymock.MockControl.ONE);
266    }
267
268    public static Object emptyReturnValueFor(Class<?> type) {
269        return type.isPrimitive() ? emptyReturnValues.get(type) : null;
270    }
271
272    private void requireMethodCall(String failMessage) {
273        if (lastInvocation == null) {
274            throw new RuntimeExceptionWrapper(new IllegalStateException(
275                    "method call on the mock needed before setting "
276                            + failMessage));
277        }
278    }
279
280    private void requireAssignable(Object returnValue) {
281        if (lastMethodIsVoidMethod()) {
282            throw new RuntimeExceptionWrapper(new IllegalStateException(
283                    "void method cannot return a value"));
284        }
285        if (returnValue == null) {
286            return;
287        }
288        Class<?> returnedType = lastInvocation.getMethod().getReturnType();
289        if (returnedType.isPrimitive()) {
290            returnedType = primitiveToWrapperType.get(returnedType);
291
292        }
293        if (!returnedType.isAssignableFrom(returnValue.getClass())) {
294            throw new RuntimeExceptionWrapper(new IllegalStateException(
295                    "incompatible return value type"));
296        }
297    }
298
299    private void requireValidThrowable(Throwable throwable) {
300        if (throwable == null)
301            throw new RuntimeExceptionWrapper(new NullPointerException(
302                    "null cannot be thrown"));
303        if (isValidThrowable(throwable))
304            return;
305
306        throw new RuntimeExceptionWrapper(new IllegalArgumentException(
307                "last method called on mock cannot throw "
308                        + throwable.getClass().getName()));
309    }
310
311    private void requireValidAnswer(IAnswer<?> answer) {
312        if (answer == null)
313            throw new RuntimeExceptionWrapper(new NullPointerException(
314                    "answer object must not be null"));
315    }
316
317    private void requireValidDelegation(Object delegateTo) {
318        if (delegateTo == null)
319            throw new RuntimeExceptionWrapper(new NullPointerException(
320                    "delegated to object must not be null"));
321        // Would be nice to validate delegateTo is implementing the mock
322        // interface (not possible right now)
323    }
324
325    private void requireLastResultOrVoidMethod() {
326        if (isLastResultOrVoidMethod()) {
327            return;
328        }
329        throw new RuntimeExceptionWrapper(new IllegalStateException(
330                "last method called on mock is not a void method"));
331    }
332
333    private void requireVoidMethod() {
334        if (lastMethodIsVoidMethod()) {
335            return;
336        }
337        throw new RuntimeExceptionWrapper(new IllegalStateException(
338                "last method called on mock is not a void method"));
339    }
340
341    private boolean isLastResultOrVoidMethod() {
342        return lastResult != null || lastMethodIsVoidMethod();
343    }
344
345    private boolean lastMethodIsVoidMethod() {
346        Class<?> returnType = lastInvocation.getMethod().getReturnType();
347        return returnType.equals(Void.TYPE);
348    }
349
350    private boolean isValidThrowable(Throwable throwable) {
351        if (throwable instanceof RuntimeException) {
352            return true;
353        }
354        if (throwable instanceof Error) {
355            return true;
356        }
357        Class<?>[] exceptions = lastInvocation.getMethod().getExceptionTypes();
358        Class<?> throwableClass = throwable.getClass();
359        for (Class<?> exception : exceptions) {
360            if (exception.isAssignableFrom(throwableClass))
361                return true;
362        }
363        return false;
364    }
365
366    public void checkOrder(boolean value) {
367        closeMethod();
368        behavior.checkOrder(value);
369    }
370
371    public void makeThreadSafe(boolean threadSafe) {
372        behavior.makeThreadSafe(threadSafe);
373    }
374
375    public void checkIsUsedInOneThread(boolean shouldBeUsedInOneThread) {
376        behavior.shouldBeUsedInOneThread(shouldBeUsedInOneThread);
377    }
378
379    @SuppressWarnings("deprecation")
380    public void setDefaultMatcher(org.easymock.ArgumentsMatcher matcher) {
381        behavior.setDefaultMatcher(matcher);
382    }
383
384    @SuppressWarnings("deprecation")
385    public void setMatcher(Method method, org.easymock.ArgumentsMatcher matcher) {
386        requireMethodCall("matcher");
387        behavior.setMatcher(lastInvocation.getMethod(), matcher);
388    }
389}