001    package hirondelle.web4j.util;
002    
003    import static hirondelle.web4j.util.Consts.NOT_FOUND;
004    
005    import java.util.regex.*;
006    import javax.mail.internet.AddressException;
007    import javax.mail.internet.InternetAddress;
008    import javax.servlet.http.HttpServletRequest;
009    import javax.servlet.http.HttpServletResponse;
010    import javax.servlet.http.HttpSession;
011    import javax.servlet.ServletContext;
012    import javax.servlet.ServletConfig;
013    
014    /**
015     Static convenience methods for common web-related tasks, which eliminate code duplication.
016    
017    <P> Similar to {@link hirondelle.web4j.util.Util}, but for methods particular to the web.
018    */
019    public final class WebUtil {
020      
021      /** Called only upon startup, by the framework.  */
022      public static void init(ServletConfig aConfig){
023        fContext = aConfig.getServletContext();
024      }
025      
026      /**
027       Validate the form of an email address.
028      
029       <P>Return <tt>true</tt> only if 
030      <ul> 
031       <li> <tt>aEmailAddress</tt> can successfully construct an 
032       {@link javax.mail.internet.InternetAddress} 
033       <li> when parsed with "@" as delimiter, <tt>aEmailAddress</tt> contains 
034       two tokens which satisfy {@link hirondelle.web4j.util.Util#textHasContent}.
035      </ul>
036      
037      <P> The second condition arises since local email addresses, simply of the form
038       "<tt>albert</tt>", for example, are valid for {@link javax.mail.internet.InternetAddress}, 
039       but almost always undesired.
040      */
041      public static boolean isValidEmailAddress(String aEmailAddress){
042        if (aEmailAddress == null) return false;
043        boolean result = true;
044        try {
045          InternetAddress emailAddr = new InternetAddress(aEmailAddress);
046          if ( ! hasNameAndDomain(aEmailAddress) ) {
047            result = false;
048          }
049        }
050        catch (AddressException ex){
051          result = false;
052        }
053        return result;
054      }
055    
056      /**
057       Ensure a particular name-value pair is present in a URL.
058        
059       <P>If the parameter does not currently exist in the URL, then the name-value
060       pair is appended to the URL; if the parameter is already present in the URL,
061       however, then its value is changed.
062      
063       <P>Any number of query parameters can be added to a URL, one after the other.
064       Any special characters in <tt>aParamName</tt> and <tt>aParamValue</tt> will be 
065       escaped by this method using {@link EscapeChars#forURL}.
066      
067       <P>This method is intended for cases in which an <tt>Action</tt> requires
068       a redirect after processing, and the redirect in turn requires <em>dynamic</em> 
069       query parameters. (With a redirect, this is the only way to 
070       pass data to the destination page. Items placed in request scope for the 
071       original request will no longer be available to the second request caused 
072       by the redirect.)
073      
074       <P>Example 1, where a new parameter is added :<P>
075       <tt>setQueryParam("blah.do", "artist", "Tom Thomson")</tt>
076      <br>
077       returns the value :<br> <tt>blah.do?artist=Tom+Thomson</tt>
078      
079       <P>Example 2, where an existing parameter is updated :<P>
080       <tt>setQueryParam("blah.do?artist=Tom+Thomson", "artist", "A Y Jackson")</tt>
081      <br>
082       returns the value :<br> <tt>blah.do?artist=A+Y+Jackson</tt>
083       
084       <P>Example 3, with a parameter name of slightly different form :<P>
085       <tt>setQueryParam("blah.do?Favourite+Artist=Tom+Thomson", "Favourite Artist", "A Y Jackson")</tt>
086      <br>
087       returns the value :<br> <tt>blah.do?Favourite+Artist=A+Y+Jackson</tt>
088       
089       @param aURL a base URL, with <em>escaped</em> parameter names and values
090       @param aParamName <em>unescaped</em> parameter name
091       @param aParamValue <em>unescaped</em> parameter value
092      */
093      public static String setQueryParam(String aURL, String aParamName, String aParamValue){
094        String result = null;
095        if ( aURL.indexOf(EscapeChars.forURL(aParamName) + "=") == -1) {
096          result = appendParam(aURL, aParamName, aParamValue);
097        }
098        else {
099          result = replaceParam(aURL, aParamName, aParamValue);
100        }
101        return result;
102      }
103      
104      /**
105       Return {@link HttpServletRequest#getRequestURL}, optionally concatenated with 
106       <tt>?</tt> and {@link HttpServletRequest#getQueryString}.
107       
108       <P>Query parameters are added only if they are present.
109       
110       <P>If the underlying method is a <tt>GET</tt> which does NOT edit the database, 
111       then presenting the return value of this method in a link is usually acceptable. 
112       
113       <P>If the underlying method is a <tt>POST</tt>, or if it is a <tt>GET</tt> which 
114       (erroneously) edits the database, it is recommended that the return value of 
115       this method NOT be placed in a link. 
116       
117       <P><em>Warning</em> : if this method is called in JSP or custom tag, then it 
118       is likely that the original query string has been overwritten by the server, 
119       as result of an internal <tt>forward</tt> operation.
120      */
121      public static String getURLWithQueryString(HttpServletRequest aRequest){
122        StringBuilder result = new StringBuilder();
123        result.append(aRequest.getRequestURL());
124        String queryString = aRequest.getQueryString();
125        if ( Util.textHasContent(queryString) ) {
126          result.append("?");
127          result.append(queryString);
128        }
129        return result.toString();
130      }
131      
132      /**
133       Return the original, complete URL submitted by the browser.
134       
135       <P>Session id is included in the return value.
136       
137       <P>Somewhat frustratingly, the original client request is not directly available from 
138       the Servlet API. 
139       <P>This implementation is based on an example in the 
140       <a href="http://www.exampledepot.com/egs/javax.servlet/GetReqUrl.html">Java Almanac</a>.
141      */
142      public static String getOriginalRequestURL(HttpServletRequest aRequest, HttpServletResponse aResponse){
143        String result = null;
144        //http://hostname.com:80/mywebapp/servlet/MyServlet/a/b;c=123?d=789
145        String scheme = aRequest.getScheme();             // http
146        String serverName = aRequest.getServerName();     // hostname.com
147        int serverPort = aRequest.getServerPort();        // 80
148        String contextPath = aRequest.getContextPath();   // /mywebapp
149        String servletPath = aRequest.getServletPath();   // /servlet/MyServlet
150        String pathInfo = aRequest.getPathInfo();         // /a/b;c=123
151        String queryString = aRequest.getQueryString();   // d=789
152    
153        // Reconstruct original requesting URL
154        result = scheme + "://" + serverName + ":" + serverPort + contextPath + servletPath;
155        if (Util.textHasContent(pathInfo)) {
156            result = result + pathInfo;
157        }
158        if (Util.textHasContent(queryString)) {
159            result = result + "?" + queryString;
160        }
161        return aResponse.encodeURL(result);    
162      }
163      
164      /**
165       Find an attribute by searching request scope, session scope (if it exists), and application scope (in that order).
166       
167       <P>If there is no session, then this method will not create one. 
168       
169       <P>If no <tt>Object</tt> corresponding to <tt>aKey</tt> is found, then <tt>null</tt> is returned. 
170      */
171      public static Object findAttribute(String aKey, HttpServletRequest aRequest){
172        //This method is similar to {@link javax.servlet.jsp.JspContext#findAttribute(java.lang.String)}
173        Object result = null;
174        result = aRequest.getAttribute(aKey);
175        if( result == null ) {
176          HttpSession session = aRequest.getSession(DO_NOT_CREATE);
177          if( session != null ) {
178            result = session.getAttribute(aKey);
179          }
180        }
181        if( result == null ) {
182          result = fContext.getAttribute(aKey);
183        }
184        return result;
185      }
186      
187      /**
188       Returns the 'file extension' for a given URL. 
189       
190       <P>Some example return values for this method :
191       <table  border='1' cellpadding='3' cellspacing='0'>
192        <tr><th>URL</th><th>'File Extension'</th></tr>
193        <tr>
194          <td>.../VacationAction.do</td>
195          <td>do</td>
196        </tr>
197        <tr>
198          <td>.../VacationAction.fetchForChange?Id=103</td>
199          <td>fetchForChange</td>
200        </tr>
201        <tr>
202          <td>.../VacationAction.list?Start=Now&End=Never</td>
203          <td>list</td>
204        </tr>
205        <tr>
206          <td>.../SomethingAction.show;jsessionid=32131?SomeId=123456</td>
207          <td>show</td>
208        </tr>
209       </table>
210       
211       @param aURL has content, and contains a '.' character (which defines the start of the 'file extension'.)
212      */
213      public static String getFileExtension(String aURL) {
214        String result = null;
215        int lastPeriod = aURL.lastIndexOf(".");
216        if( lastPeriod == NOT_FOUND ) {
217          throw new RuntimeException("Cannot find '.' character in URL: " + Util.quote(aURL));
218        }
219        int jsessionId = aURL.indexOf(";jsessionid");
220        int firstQuestionMark = aURL.indexOf("?");
221        if( jsessionId != NOT_FOUND){
222          result = aURL.substring(lastPeriod + 1, jsessionId);
223        }
224        else if( firstQuestionMark != NOT_FOUND){
225          result = aURL.substring(lastPeriod + 1, firstQuestionMark);
226        }
227        else {
228          result = aURL.substring(lastPeriod + 1);
229        }
230        return result;
231      }
232      
233      // PRIVATE 
234      
235      private WebUtil(){
236        //empty - prevent construction
237      }
238      
239      /** Needed for searching application scope for attributes. */
240      private static ServletContext fContext;
241      
242      private static final boolean DO_NOT_CREATE = false;
243      
244      private static String appendParam(String aURL, String aParamName, String aParamValue){
245        StringBuilder result = new StringBuilder(aURL);
246        if (aURL.indexOf("?") == -1) {
247          result.append("?");
248        }
249        else {
250          result.append("&");
251        }
252        result.append( EscapeChars.forURL(aParamName) );
253        result.append("=");
254        result.append( EscapeChars.forURL(aParamValue) );
255        return result.toString();
256      }
257    
258      private static String replaceParam(String aURL, String aParamName, String aParamValue){
259        String regex = "(\\?|\\&)(" + EscapeChars.forRegex(EscapeChars.forURL(aParamName)) + "=)([^\\&]*)";
260        StringBuffer result = new StringBuffer();
261        Pattern pattern = Pattern.compile( regex );
262        Matcher matcher = pattern.matcher(aURL);
263        while ( matcher.find() ) {
264          matcher.appendReplacement(result, getReplacement(matcher, aParamValue));
265        }
266        matcher.appendTail(result);
267        return result.toString();
268      }
269      
270      private static String getReplacement(Matcher aMatcher, String aParamValue){
271        return aMatcher.group(1) + aMatcher.group(2) + EscapeChars.forURL(aParamValue);
272      }
273      
274      private static boolean hasNameAndDomain(String aEmailAddress){
275        String[] tokens = aEmailAddress.split("@");
276        return 
277         tokens.length == 2 &&
278         Util.textHasContent( tokens[0] ) && 
279         Util.textHasContent( tokens[1] ) ;
280      }
281    }