001 package hirondelle.fish.test.doubles;
002
003 import java.io.BufferedReader;
004 import java.io.IOException;
005 import java.io.UnsupportedEncodingException;
006 import java.security.Principal;
007 import java.util.*;
008 import java.text.*;
009 import javax.servlet.RequestDispatcher;
010 import javax.servlet.ServletInputStream;
011 import javax.servlet.http.Cookie;
012 import javax.servlet.http.HttpServletRequest;
013 import javax.servlet.http.HttpSession;
014 import hirondelle.web4j.model.ModelUtil;
015 import hirondelle.web4j.util.Args;
016 import hirondelle.web4j.util.Util;
017 import static hirondelle.web4j.util.Consts.EMPTY_STRING;
018
019 /**
020 Fake implementation of {@link HttpServletRequest}, used
021 only for testing outside of the regular runtime environment.
022
023 <P>Various methods have been added to make testing more convenient.
024
025 <P>The {@link #logInAuthenticatedUser(String, List)} method allows you
026 to mimic an authenticated user.
027 */
028 public final class FakeRequest implements HttpServletRequest {
029
030 /** Used by factory methods to distinguish between <tt>GET</tt> and <tt>POST</tt> requests.*/
031 public enum HttpMethod {GET, POST}
032
033 /** Factory method for <tt>GET</tt> requests . */
034 public static FakeRequest forGET(String aServletMatchingPath, String aQueryString){
035 return new FakeRequest (aServletMatchingPath, EMPTY_STRING, aQueryString, HttpMethod.GET);
036 }
037
038 /** Factory method for <tt>POST</tt> requests . */
039 public static FakeRequest forPOST(String aServletMatchingPath, String aQueryString){
040 return new FakeRequest (aServletMatchingPath, EMPTY_STRING, aQueryString, HttpMethod.POST);
041 }
042
043 /** Full constructor. */
044 public FakeRequest(
045 String aScheme, String aServerName, Integer aServerPort, String aContextPath,
046 String aServletMatchingPath, String aExtraPath, String aQueryString, HttpMethod aMethod
047 ){
048 fScheme = aScheme;
049 fServerName = aServerName;
050 fServerPort = aServerPort;
051 fContextPath = ensureNonNullStartsWithSlash(aContextPath);
052 fServletMatchingPath = ensureNonNullStartsWithSlash(aServletMatchingPath);
053 fExtraPath = ensureNonNullStartsWithSlash(aExtraPath);
054 extractParamsFrom(aQueryString);
055 fMethod = aMethod;
056 }
057
058 /*
059 Methods added for testing.
060 */
061
062 /** Method added for testing. */
063 public void setContentType(String aContentType){ fContentType = aContentType; }
064 /** Method added for testing. */
065 public void setContentLength(int aContentLength){ fContentLength = aContentLength; }
066 /** Method added for testing. */
067 public void setProtocol(String aProtocol){ fProtocol = aProtocol; }
068 /** Method added for testing. */
069 public void setScheme(String aScheme){ fScheme = aScheme; }
070 /** Method added for testing. */
071 public void setServerName(String aServerName){ fServerName = aServerName; }
072 /** Method added for testing. */
073 public void setServerPort(int aServerPort){ fServerPort = aServerPort; }
074 /** Method added for testing. */
075 public void setRemoteAddr(String aRemoteAddr){ fRemoteAddr = aRemoteAddr; }
076 /** Method added for testing. */
077 public void setRemoteHost(String aRemoteHost){ fRemoteHost = aRemoteHost; }
078 /** Method added for testing. */
079 public void setIsSecure(boolean aIsSecure){ fIsSecure = aIsSecure; }
080 /** Method added for testing. */
081 public void setRemotePort(int aRemotePort){ fRemotePort = aRemotePort; }
082 /** Method added for testing. */
083 public void setLocale(Locale aLocale){ fLocale = aLocale; }
084
085 /** Method added for testing. */
086 public void addCookie(String aName, String aValue){
087 Args.checkForContent(aName);
088 fCookies.put(aName, aValue);
089 }
090
091 /** Method added for testing. */
092 public void addParameter(String aName, String aValue){
093 Args.checkForContent(aName);
094 List<String> existingValues = fParams.get(aName);
095 if ( existingValues != null ) {
096 existingValues.add(aValue);
097 }
098 else {
099 List<String> newValues = new ArrayList<String>();
100 newValues.add(aValue);
101 fParams.put(aName, newValues);
102 }
103 }
104
105 /**
106 Date/time headers must use RFC 1123 format.
107 Method added for testing.
108 */
109 public void addHeader(String aName, String aValue){
110 Args.checkForContent(aName);
111 if( fHeaders.containsKey(aName) ){
112 List<String> values = fHeaders.get(aName);
113 values.add(aValue);
114 }
115 else {
116 List<String> values = new ArrayList<String>();
117 values.add(aValue);
118 fHeaders.put(aName, values);
119 }
120 }
121
122 /** Method added for testing. */
123 public void logInAuthenticatedUser(String aUserName, List<String> aRoles){
124 Args.checkForContent(aUserName);
125 fUserName = aUserName;
126 fUserRoles.addAll(aRoles);
127 }
128
129 /** Method added for testing. */
130 public void setRequestedSessionId(String aSessionId){ fRequestedSessionId = aSessionId; }
131
132 /*
133 ServletRequest methods.
134 */
135
136 public Object getAttribute(String aName) {
137 return fAttrs.get(aName);
138 }
139
140 public Enumeration getAttributeNames() {
141 return Collections.enumeration(fAttrs.keySet());
142 }
143
144 public void setAttribute(String aName, Object aObject) {
145 fAttrs.put(aName, aObject);
146 }
147
148 public void removeAttribute(String aName) {
149 fAttrs.remove(aName);
150 }
151
152 /** Default value 'UTF-8'. */
153 public String getCharacterEncoding() {
154 return fCharEncoding;
155 }
156
157 public void setCharacterEncoding(String aEncoding) throws UnsupportedEncodingException {
158 fCharEncoding = aEncoding;
159 }
160
161 /** Default value 0. */
162 public int getContentLength() {
163 return fContentLength;
164 }
165
166 /** Default value 'text/html'. */
167 public String getContentType() {
168 return fContentType;
169 }
170
171 public String getParameter(String aName) {
172 Args.checkForContent(aName);
173 String result = null;
174 List<String> existingValues = fParams.get(aName);
175 if ( existingValues != null ) {
176 result = existingValues.get(FIRST);
177 }
178 return result;
179 }
180
181 public Enumeration getParameterNames() {
182 return Collections.enumeration(fParams.keySet());
183 }
184
185 public String[] getParameterValues(String aName) {
186 Args.checkForContent(aName);
187 String[] result = null;
188 List<String> existingValues = fParams.get(aName);
189 if ( existingValues != null ) {
190 result = existingValues.toArray(new String[0]);
191 }
192 return result;
193 }
194
195 public Map getParameterMap() {
196 Map<String, String[]> result = new LinkedHashMap<String, String[]>();
197 for(String name: fParams.keySet()) {
198 String[] values = fParams.get(name).toArray(new String[0]);
199 result.put(name, values);
200 }
201 return result;
202 }
203
204 public String getProtocol() {
205 return fProtocol;
206 }
207
208 /** Default value 'http'. */
209 public String getScheme() {
210 return fScheme;
211 }
212
213 /** Default value 'Test Double'. */
214 public String getServerName() { return fServerName; }
215 /** Default value 8080. */
216 public int getServerPort() { return fServerPort; }
217
218 /** Default value '127.0.0.1'. */
219 public String getLocalAddr() { return fLocalAddr; }
220 /** Default value '127.0.0.1'. */
221 public String getLocalName() { return fLocalName; }
222 /** Default value 8080. */
223 public int getLocalPort() { return fLocalPort; }
224
225 /** Default value '127.0.0.1'. */
226 public String getRemoteAddr() { return fRemoteAddr; }
227 /** Default value '127.0.0.1'. */
228 public String getRemoteHost() { return fRemoteHost; }
229 /** Default value '80'. */
230 public int getRemotePort() { return fRemotePort; }
231
232
233 /** Default value <tt>Locale.ENGLISH</tt>. */
234 public Locale getLocale() {
235 return fLocale;
236 }
237 /** Returns only one Locale. */
238 public Enumeration getLocales() {
239 Collection<Locale> locales = new ArrayList<Locale>();
240 locales.add(fLocale);
241 return Collections.enumeration(locales);
242 }
243
244 public boolean isSecure() { return fIsSecure; }
245
246 /** Returns <tt>null</tt> - not implemented. */
247 public RequestDispatcher getRequestDispatcher(String aPath) { return null; }
248 /** Returns <tt>null</tt> - deprecated. */
249 public String getRealPath(String aPath) { return null; }
250 /** Returns <tt>null</tt> - not implemented. */
251 public ServletInputStream getInputStream() throws IOException { return null; }
252 /** Returns <tt>null</tt> - not implemented. */
253 public BufferedReader getReader() throws IOException { return null; }
254
255 /*
256 HttpServletRequest methods.
257 */
258
259 /** Returns <tt>null</tt> - not authenticated. */
260 public String getAuthType() { return null; }
261
262 public Cookie[] getCookies() {
263 List<Cookie> cookies = new ArrayList<Cookie>();
264 if ( ! fCookies.isEmpty() ) {
265 for(String name: fCookies.keySet()){
266 String value = fCookies.get(name);
267 Cookie cookie = new Cookie(name, value);
268 cookies.add(cookie);
269 }
270 }
271 return cookies.isEmpty() ? null : cookies.toArray(new Cookie[0]);
272 }
273
274 public long getDateHeader(String aName) {
275 Args.checkForContent(aName);
276 long result = -1;
277 String value = getHeader(aName);
278 if(value != null) {
279 SimpleDateFormat format = new SimpleDateFormat(PATTERN_RFC1123);
280 try {
281 Date date = format.parse(value);
282 result = date.getTime();
283 }
284 catch (ParseException ex){
285 throw new IllegalArgumentException("Cannot parse date/time header value using RFC 1123: " + Util.quote(value));
286 }
287 }
288 return result;
289 }
290
291 public String getHeader(String aName) {
292 Args.checkForContent(aName);
293 String result = null;
294 if( fHeaders.containsKey(aName) ) {
295 result = fHeaders.get(aName).get(FIRST);
296 }
297 return result;
298 }
299
300 public Enumeration getHeaders(String aName) {
301 Args.checkForContent(aName);
302 Collection<String> result = new ArrayList<String>();
303 List<String> values = fHeaders.get(aName);
304 if( values != null ) {
305 result.addAll(values);
306 }
307 return Collections.enumeration(result);
308 }
309
310 public Enumeration getHeaderNames() {
311 Collection<String> result = new ArrayList<String>();
312 for(String name : fHeaders.keySet() ){
313 result.add(name);
314 }
315 return Collections.enumeration(result);
316 }
317
318 public int getIntHeader(String aName) {
319 int result = -1;
320 String value = getHeader(aName);
321 if(value != null) {
322 result = Integer.valueOf(value);
323 }
324 return result;
325 }
326
327 public String getMethod() {
328 return fMethod.toString();
329 }
330
331 public String getPathInfo() {
332 return fExtraPath;
333 }
334
335 /** Returns <tt>null</tt> - not implemented. */
336 public String getPathTranslated() { return null; }
337
338 public String getContextPath() {
339 return fContextPath;
340 }
341
342 /**
343 Created from the given request parameters.
344 Includes the initial '?'. Returns <tt>null</tt> if no parameters present.
345 <i>This implementation is artificial but convenient, since it makes no distinction
346 between GET and POST</i>.
347 */
348 public String getQueryString() {
349 StringBuilder result = new StringBuilder("");
350 boolean hasAddedFirstParam = false;
351 for (String name : fParams.keySet()){
352 if ( ! hasAddedFirstParam ) {
353 result.append("?");
354 hasAddedFirstParam = true;
355 }
356 else {
357 result.append("&");
358 }
359 result.append(name + "=" + fParams.get(name));
360 }
361 return hasAddedFirstParam ? result.toString() : null;
362 }
363
364 public String getRemoteUser() {
365 return fUserName;
366 }
367
368 public boolean isUserInRole(String aRole) {
369 return fUserRoles.contains(aRole);
370 }
371
372 public Principal getUserPrincipal() {
373 return new FakePrincipal(fUserName);
374 }
375
376 public String getRequestURI() {
377 return fContextPath + fServletMatchingPath + fExtraPath;
378 }
379
380 public StringBuffer getRequestURL() {
381 String result = fScheme + "://" + fServerName;
382 if ( fServerPort != null ) {
383 result = result + ":" + fServerPort.toString();
384 }
385 result = result + fContextPath + fServletMatchingPath + fExtraPath + getQueryString();
386 return new StringBuffer(result);
387 }
388
389 public String getServletPath() {
390 return fServletMatchingPath;
391 }
392
393 public String getRequestedSessionId() {
394 return fRequestedSessionId;
395 }
396
397 public boolean isRequestedSessionIdValid() {
398 return Util.textHasContent(fRequestedSessionId) && fRequestedSessionId.equals(getSession(false).getId());
399 }
400
401 /** Hard-code to <tt>true</tt>. */
402 public boolean isRequestedSessionIdFromCookie() {
403 return true;
404 }
405
406 /** Hard-code to <tt>false</tt>. */
407 public boolean isRequestedSessionIdFromURL() {
408 return false;
409 }
410
411 /** Hard-code to <tt>false</tt>. */
412 public boolean isRequestedSessionIdFromUrl() {
413 return false;
414 }
415
416 public HttpSession getSession(boolean aCreateNew) {
417 if( fSession == null ) {
418 fSession = FakeSession.joinOrCreate(fRequestedSessionId, aCreateNew);
419 }
420 return fSession;
421 }
422
423 public HttpSession getSession() {
424 if( fSession == null ) {
425 fSession = FakeSession.joinOrCreate(fRequestedSessionId, true);
426 }
427 return fSession;
428 }
429
430 // PRIVATE //
431 private Map<String, List<String>> fParams = new LinkedHashMap<String, List<String>>();
432 private static final int FIRST = 0;
433 private Map<String, Object> fAttrs = new LinkedHashMap<String, Object>();
434
435 //Items for ServletRequest
436 private String fCharEncoding = "UTF-8";
437 private String fContentType = "text/html";
438 private int fContentLength;
439 private String fProtocol = "HTTP/1.1";
440 private String fScheme = "http";
441
442 private String fServerName = "Test Double";
443 private Integer fServerPort = 8080;
444
445 private String fLocalName = "Test Double";
446 private int fLocalPort = 8080;
447 private String fLocalAddr = "127.0.0.1";
448
449 private String fRemoteAddr = "127.0.0.1";
450 private String fRemoteHost = "127.0.0.1";
451 private int fRemotePort = 80;
452
453 private Locale fLocale = Locale.ENGLISH;
454
455 private boolean fIsSecure = false;
456
457 //Items for HttpServletRequest
458 private String fRequestedSessionId = EMPTY_STRING;
459 private Map<String, String> fCookies = new LinkedHashMap<String, String>();
460 private Map<String, List<String>> fHeaders = new LinkedHashMap<String, List<String>>();
461 private static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
462 private static final String SLASH = "/";
463 private HttpMethod fMethod;
464 private String fContextPath = EMPTY_STRING;
465 private String fServletMatchingPath = EMPTY_STRING;
466 private String fExtraPath = EMPTY_STRING;
467 private String fUserName = EMPTY_STRING;
468 private List<String> fUserRoles = new ArrayList<String>();
469 private HttpSession fSession;
470
471 private FakeRequest(String aServletMatchingPath, String aExtraPath, String aQueryString, HttpMethod aMethod){
472 fServletMatchingPath = ensureNonNullStartsWithSlash(aServletMatchingPath);
473 fExtraPath = ensureNonNullStartsWithSlash(aExtraPath);
474 extractParamsFrom(aQueryString);
475 fMethod = aMethod;
476 }
477
478 private String ensureNonNullStartsWithSlash(String aText){
479 String result = aText;
480 if ( Util.textHasContent(aText) ) {
481 if (! aText.startsWith(SLASH)){
482 throw new IllegalArgumentException("Does not start with a '/' character: " + Util.quote(aText));
483 }
484 }
485 else {
486 result = EMPTY_STRING;
487 }
488 return result;
489 }
490
491 /**
492 @param aQueryString 'blah=yes', 'blah=', 'blah=yes&Id=123', 'blah=yes&Id='.
493 */
494 private void extractParamsFrom(String aQueryString){
495 if( Util.textHasContent(aQueryString)) {
496 StringTokenizer firstParse = new StringTokenizer(aQueryString, "&");
497 while ( firstParse.hasMoreElements() ) {
498 String eachNameValuePair = firstParse.nextToken();
499 StringTokenizer secondParse = new StringTokenizer(eachNameValuePair, "=");
500 //sometimes the value is missing. in that case, coerce the value into an empty string
501 List<String> items = new ArrayList<String>();
502 while ( secondParse.hasMoreTokens() ) {
503 items.add(secondParse.nextToken());
504 }
505 String name = items.get(0); //assume name always present
506 String value = EMPTY_STRING; //value may not be present
507 if( items.size() > 1 ) {
508 value = items.get(1); //value is present
509 }
510 addParameter(name, value);
511 }
512 }
513 }
514
515 private static class FakePrincipal implements Principal {
516 FakePrincipal(String aName){
517 fName = aName;
518 }
519 public String getName() {
520 return fName;
521 }
522 @Override public String toString(){
523 return fName;
524 }
525 @Override public boolean equals(Object aThat){
526 Boolean result = ModelUtil.quickEquals(this, aThat);
527 if ( result == null ){
528 FakePrincipal that = (FakePrincipal) aThat;
529 result = ModelUtil.equalsFor(this.getSignificantFields(), that.getSignificantFields());
530 }
531 return result;
532 }
533 @Override public int hashCode() {
534 return ModelUtil.hashCodeFor(getSignificantFields());
535 }
536 private String fName;
537 private Object[] getSignificantFields() { return new Object[] {fName}; }
538 }
539 }