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 }