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    }