001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    package org.apache.shiro.web.util;
020    
021    import javax.servlet.http.HttpServletRequest;
022    import javax.servlet.http.HttpServletResponse;
023    import java.io.IOException;
024    import java.io.UnsupportedEncodingException;
025    import java.net.URLEncoder;
026    import java.util.Map;
027    
028    /**
029     * View that redirects to an absolute, context relative, or current request
030     * relative URL, exposing all model attributes as HTTP query parameters.
031     * <p/>
032     * A URL for this view is supposed to be a HTTP redirect URL, i.e.
033     * suitable for HttpServletResponse's <code>sendRedirect</code> method, which
034     * is what actually does the redirect if the HTTP 1.0 flag is on, or via sending
035     * back an HTTP 303 code - if the HTTP 1.0 compatibility flag is off.
036     * <p/>
037     * Note that while the default value for the "contextRelative" flag is off,
038     * you will probably want to almost always set it to true. With the flag off,
039     * URLs starting with "/" are considered relative to the web server root, while
040     * with the flag on, they are considered relative to the web application root.
041     * Since most web apps will never know or care what their context path actually
042     * is, they are much better off setting this flag to true, and submitting paths
043     * which are to be considered relative to the web application root.
044     * <p/>
045     * Note that in a Servlet 2.2 environment, i.e. a servlet container which
046     * is only compliant to the limits of this spec, this class will probably fail
047     * when feeding in URLs which are not fully absolute, or relative to the current
048     * request (no leading "/"), as these are the only two types of URL that
049     * <code>sendRedirect</code> supports in a Servlet 2.2 environment.
050     * <p/>
051     * <em>This class was borrowed from a nearly identical version found in
052     * the <a href="http://www.springframework.org/">Spring Framework</a>, with minor modifications to
053     * avoid a dependency on Spring itself for a very small amount of code - we couldn't have done it better, and
054     * don't want to repeat all of their great effort ;).
055     * The original author names and copyright (Apache 2.0) has been left in place.  A special
056     * thanks to Rod Johnson, Juergen Hoeller, and Colin Sampaleanu for making this available.</em>
057     *
058     * @see #setContextRelative
059     * @see #setHttp10Compatible
060     * @see javax.servlet.http.HttpServletResponse#sendRedirect
061     * @since 0.2
062     */
063    public class RedirectView {
064    
065        //TODO - complete JavaDoc
066    
067        /**
068         * The default encoding scheme: UTF-8
069         */
070        public static final String DEFAULT_ENCODING_SCHEME = "UTF-8";
071    
072        private String url;
073    
074        private boolean contextRelative = false;
075    
076        private boolean http10Compatible = true;
077    
078        private String encodingScheme = DEFAULT_ENCODING_SCHEME;
079    
080        /**
081         * Constructor for use as a bean.
082         */
083        @SuppressWarnings({"UnusedDeclaration"})
084        public RedirectView() {
085        }
086    
087        /**
088         * Create a new RedirectView with the given URL.
089         * <p>The given URL will be considered as relative to the web server,
090         * not as relative to the current ServletContext.
091         *
092         * @param url the URL to redirect to
093         * @see #RedirectView(String, boolean)
094         */
095        public RedirectView(String url) {
096            setUrl(url);
097        }
098    
099        /**
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    }