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 firstPeriod = aURL.indexOf(".");
216 if( firstPeriod == 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(firstPeriod + 1, jsessionId);
223 }
224 else if( firstQuestionMark != NOT_FOUND){
225 result = aURL.substring(firstPeriod + 1, firstQuestionMark);
226 }
227 else {
228 result = aURL.substring(firstPeriod + 1);
229 }
230 /*
231 if( firstQuestionMark == NOT_FOUND ){
232 result = aURL.substring(firstPeriod + 1);
233 }
234 else {
235 result = aURL.substring(firstPeriod +1, firstQuestionMark);
236 }
237 */
238 return result;
239 }
240
241 // PRIVATE //
242
243 private WebUtil(){
244 //empty - prevent construction
245 }
246
247 /** Needed for searching application scope for attributes. */
248 private static ServletContext fContext;
249
250 private static final boolean DO_NOT_CREATE = false;
251
252 private static String appendParam(String aURL, String aParamName, String aParamValue){
253 StringBuilder result = new StringBuilder(aURL);
254 if (aURL.indexOf("?") == -1) {
255 result.append("?");
256 }
257 else {
258 result.append("&");
259 }
260 result.append( EscapeChars.forURL(aParamName) );
261 result.append("=");
262 result.append( EscapeChars.forURL(aParamValue) );
263 return result.toString();
264 }
265
266 private static String replaceParam(String aURL, String aParamName, String aParamValue){
267 String regex = "(\\?|\\&)(" + EscapeChars.forRegex(EscapeChars.forURL(aParamName)) + "=)([^\\&]*)";
268 StringBuffer result = new StringBuffer();
269 Pattern pattern = Pattern.compile( regex );
270 Matcher matcher = pattern.matcher(aURL);
271 while ( matcher.find() ) {
272 matcher.appendReplacement(result, getReplacement(matcher, aParamValue));
273 }
274 matcher.appendTail(result);
275 return result.toString();
276 }
277
278 private static String getReplacement(Matcher aMatcher, String aParamValue){
279 return aMatcher.group(1) + aMatcher.group(2) + EscapeChars.forURL(aParamValue);
280 }
281
282 private static boolean hasNameAndDomain(String aEmailAddress){
283 String[] tokens = aEmailAddress.split("@");
284 return
285 tokens.length == 2 &&
286 Util.textHasContent( tokens[0] ) &&
287 Util.textHasContent( tokens[1] ) ;
288 }
289 }