View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.web.util;
20  
21  import javax.servlet.http.HttpServletRequest;
22  import javax.servlet.http.HttpServletResponse;
23  import java.io.IOException;
24  import java.io.UnsupportedEncodingException;
25  import java.net.URLEncoder;
26  import java.util.Map;
27  
28  /**
29   * View that redirects to an absolute, context relative, or current request
30   * relative URL, exposing all model attributes as HTTP query parameters.
31   * <p/>
32   * A URL for this view is supposed to be a HTTP redirect URL, i.e.
33   * suitable for HttpServletResponse's <code>sendRedirect</code> method, which
34   * is what actually does the redirect if the HTTP 1.0 flag is on, or via sending
35   * back an HTTP 303 code - if the HTTP 1.0 compatibility flag is off.
36   * <p/>
37   * Note that while the default value for the "contextRelative" flag is off,
38   * you will probably want to almost always set it to true. With the flag off,
39   * URLs starting with "/" are considered relative to the web server root, while
40   * with the flag on, they are considered relative to the web application root.
41   * Since most web apps will never know or care what their context path actually
42   * is, they are much better off setting this flag to true, and submitting paths
43   * which are to be considered relative to the web application root.
44   * <p/>
45   * Note that in a Servlet 2.2 environment, i.e. a servlet container which
46   * is only compliant to the limits of this spec, this class will probably fail
47   * when feeding in URLs which are not fully absolute, or relative to the current
48   * request (no leading "/"), as these are the only two types of URL that
49   * <code>sendRedirect</code> supports in a Servlet 2.2 environment.
50   * <p/>
51   * <em>This class was borrowed from a nearly identical version found in
52   * the <a href="http://www.springframework.org/">Spring Framework</a>, with minor modifications to
53   * avoid a dependency on Spring itself for a very small amount of code - we couldn't have done it better, and
54   * don't want to repeat all of their great effort ;).
55   * The original author names and copyright (Apache 2.0) has been left in place.  A special
56   * thanks to Rod Johnson, Juergen Hoeller, and Colin Sampaleanu for making this available.</em>
57   *
58   * @see #setContextRelative
59   * @see #setHttp10Compatible
60   * @see javax.servlet.http.HttpServletResponse#sendRedirect
61   * @since 0.2
62   */
63  public class RedirectView {
64  
65      //TODO - complete JavaDoc
66  
67      /**
68       * The default encoding scheme: UTF-8
69       */
70      public static final String DEFAULT_ENCODING_SCHEME = "UTF-8";
71  
72      private String url;
73  
74      private boolean contextRelative = false;
75  
76      private boolean http10Compatible = true;
77  
78      private String encodingScheme = DEFAULT_ENCODING_SCHEME;
79  
80      /**
81       * Constructor for use as a bean.
82       */
83      @SuppressWarnings({"UnusedDeclaration"})
84      public RedirectView() {
85      }
86  
87      /**
88       * Create a new RedirectView with the given URL.
89       * <p>The given URL will be considered as relative to the web server,
90       * not as relative to the current ServletContext.
91       *
92       * @param url the URL to redirect to
93       * @see #RedirectView(String, boolean)
94       */
95      public RedirectView(String url) {
96          setUrl(url);
97      }
98  
99      /**
100      * Create a new RedirectView with the given URL.
101      *
102      * @param url             the URL to redirect to
103      * @param contextRelative whether to interpret the given URL as
104      *                        relative to the current ServletContext
105      */
106     public RedirectView(String url, boolean contextRelative) {
107         this(url);
108         this.contextRelative = contextRelative;
109     }
110 
111     /**
112      * Create a new RedirectView with the given URL.
113      *
114      * @param url              the URL to redirect to
115      * @param contextRelative  whether to interpret the given URL as
116      *                         relative to the current ServletContext
117      * @param http10Compatible whether to stay compatible with HTTP 1.0 clients
118      */
119     public RedirectView(String url, boolean contextRelative, boolean http10Compatible) {
120         this(url);
121         this.contextRelative = contextRelative;
122         this.http10Compatible = http10Compatible;
123     }
124 
125 
126     public String getUrl() {
127         return url;
128     }
129 
130     public void setUrl(String url) {
131         this.url = url;
132     }
133 
134     /**
135      * Set whether to interpret a given URL that starts with a slash ("/")
136      * as relative to the current ServletContext, i.e. as relative to the
137      * web application root.
138      * <p/>
139      * Default is "false": A URL that starts with a slash will be interpreted
140      * as absolute, i.e. taken as-is. If true, the context path will be
141      * prepended to the URL in such a case.
142      *
143      * @param contextRelative whether to interpret a given URL that starts with a slash ("/")
144      *                        as relative to the current ServletContext, i.e. as relative to the
145      *                        web application root.
146      * @see javax.servlet.http.HttpServletRequest#getContextPath
147      */
148     public void setContextRelative(boolean contextRelative) {
149         this.contextRelative = contextRelative;
150     }
151 
152     /**
153      * Set whether to stay compatible with HTTP 1.0 clients.
154      * <p>In the default implementation, this will enforce HTTP status code 302
155      * in any case, i.e. delegate to <code>HttpServletResponse.sendRedirect</code>.
156      * Turning this off will send HTTP status code 303, which is the correct
157      * code for HTTP 1.1 clients, but not understood by HTTP 1.0 clients.
158      * <p>Many HTTP 1.1 clients treat 302 just like 303, not making any
159      * difference. However, some clients depend on 303 when redirecting
160      * after a POST request; turn this flag off in such a scenario.
161      *
162      * @param http10Compatible whether to stay compatible with HTTP 1.0 clients.
163      * @see javax.servlet.http.HttpServletResponse#sendRedirect
164      */
165     public void setHttp10Compatible(boolean http10Compatible) {
166         this.http10Compatible = http10Compatible;
167     }
168 
169     /**
170      * Set the encoding scheme for this view. Default is UTF-8.
171      *
172      * @param encodingScheme the encoding scheme for this view. Default is UTF-8.
173      */
174     @SuppressWarnings({"UnusedDeclaration"})
175     public void setEncodingScheme(String encodingScheme) {
176         this.encodingScheme = encodingScheme;
177     }
178 
179 
180     /**
181      * Convert model to request parameters and redirect to the given URL.
182      *
183      * @param model    the model to convert
184      * @param request  the incoming HttpServletRequest
185      * @param response the outgoing HttpServletResponse
186      * @throws java.io.IOException if there is a problem issuing the redirect
187      * @see #appendQueryProperties
188      * @see #sendRedirect
189      */
190     public final void renderMergedOutputModel(
191             Map model, HttpServletRequest request, HttpServletResponse response) throws IOException {
192 
193         // Prepare name URL.
194         StringBuilder targetUrl = new StringBuilder();
195         if (this.contextRelative && getUrl().startsWith("/")) {
196             // Do not apply context path to relative URLs.
197             targetUrl.append(request.getContextPath());
198         }
199         targetUrl.append(getUrl());
200         //change the following method to accept a StringBuilder instead of a StringBuilder for Shiro 2.x:
201         appendQueryProperties(targetUrl, model, this.encodingScheme);
202 
203         sendRedirect(request, response, targetUrl.toString(), this.http10Compatible);
204     }
205 
206     /**
207      * Append query properties to the redirect URL.
208      * Stringifies, URL-encodes and formats model attributes as query properties.
209      *
210      * @param targetUrl      the StringBuffer to append the properties to
211      * @param model          Map that contains model attributes
212      * @param encodingScheme the encoding scheme to use
213      * @throws java.io.UnsupportedEncodingException if string encoding failed
214      * @see #urlEncode
215      * @see #queryProperties
216      * @see #urlEncode(String, String)
217      */
218     protected void appendQueryProperties(StringBuilder targetUrl, Map model, String encodingScheme)
219             throws UnsupportedEncodingException {
220 
221         // Extract anchor fragment, if any.
222         // The following code does not use JDK 1.4's StringBuffer.indexOf(String)
223         // method to retain JDK 1.3 compatibility.
224         String fragment = null;
225         int anchorIndex = targetUrl.toString().indexOf('#');
226         if (anchorIndex > -1) {
227             fragment = targetUrl.substring(anchorIndex);
228             targetUrl.delete(anchorIndex, targetUrl.length());
229         }
230 
231         // If there aren't already some parameters, we need a "?".
232         boolean first = (getUrl().indexOf('?') < 0);
233         Map queryProps = queryProperties(model);
234 
235         if (queryProps != null) {
236             for (Object o : queryProps.entrySet()) {
237                 if (first) {
238                     targetUrl.append('?');
239                     first = false;
240                 } else {
241                     targetUrl.append('&');
242                 }
243                 Map.Entry entry = (Map.Entry) o;
244                 String encodedKey = urlEncode(entry.getKey().toString(), encodingScheme);
245                 String encodedValue =
246                         (entry.getValue() != null ? urlEncode(entry.getValue().toString(), encodingScheme) : "");
247                 targetUrl.append(encodedKey).append('=').append(encodedValue);
248             }
249         }
250 
251         // Append anchor fragment, if any, to end of URL.
252         if (fragment != null) {
253             targetUrl.append(fragment);
254         }
255     }
256 
257     /**
258      * URL-encode the given input String with the given encoding scheme, using
259      * {@link URLEncoder#encode(String, String) URLEncoder.encode(input, enc)}.
260      *
261      * @param input          the unencoded input String
262      * @param encodingScheme the encoding scheme
263      * @return the encoded output String
264      * @throws UnsupportedEncodingException if thrown by the JDK URLEncoder
265      * @see java.net.URLEncoder#encode(String, String)
266      * @see java.net.URLEncoder#encode(String)
267      */
268     protected String urlEncode(String input, String encodingScheme) throws UnsupportedEncodingException {
269         return URLEncoder.encode(input, encodingScheme);
270     }
271 
272     /**
273      * Determine name-value pairs for query strings, which will be stringified,
274      * URL-encoded and formatted by appendQueryProperties.
275      * <p/>
276      * This implementation returns all model elements as-is.
277      *
278      * @param model the model elements for which to determine name-value pairs.
279      * @return the name-value pairs for query strings.
280      * @see #appendQueryProperties
281      */
282     protected Map queryProperties(Map model) {
283         return model;
284     }
285 
286     /**
287      * Send a redirect back to the HTTP client
288      *
289      * @param request          current HTTP request (allows for reacting to request method)
290      * @param response         current HTTP response (for sending response headers)
291      * @param targetUrl        the name URL to redirect to
292      * @param http10Compatible whether to stay compatible with HTTP 1.0 clients
293      * @throws IOException if thrown by response methods
294      */
295     @SuppressWarnings({"UnusedDeclaration"})
296     protected void sendRedirect(HttpServletRequest request, HttpServletResponse response,
297                                 String targetUrl, boolean http10Compatible) throws IOException {
298         if (http10Compatible) {
299             // Always send status code 302.
300             response.sendRedirect(response.encodeRedirectURL(targetUrl));
301         } else {
302             // Correct HTTP status code is 303, in particular for POST requests.
303             response.setStatus(303);
304             response.setHeader("Location", response.encodeRedirectURL(targetUrl));
305         }
306     }
307 
308 }