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 }