/* Copyright 2004-2005 Graeme Rocher
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.codehaus.groovy.grails.web.util;
import com.opensymphony.module.sitemesh.util.FastByteArrayOutputStream;
import grails.util.GrailsUtil;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.groovy.grails.commons.ConfigurationHolder;
import org.codehaus.groovy.grails.commons.GrailsApplication;
import org.codehaus.groovy.grails.web.mapping.UrlMappingInfo;
import org.codehaus.groovy.grails.web.mapping.UrlMappingsHolder;
import org.codehaus.groovy.grails.web.pages.FastStringWriter;
import org.codehaus.groovy.grails.web.servlet.GrailsApplicationAttributes;
import org.codehaus.groovy.grails.web.servlet.GrailsUrlPathHelper;
import org.codehaus.groovy.grails.web.servlet.WrappedResponseHolder;
import org.codehaus.groovy.grails.web.servlet.mvc.GrailsParameterMap;
import org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequest;
import org.codehaus.groovy.grails.web.servlet.mvc.exceptions.ControllerExecutionException;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.WebRequestInterceptor;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.handler.WebRequestHandlerInterceptorAdapter;
import org.springframework.web.util.UrlPathHelper;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
/**
*
* Utility methods to access commons objects and perform common web related functions for the internal framework
*
* @author Graeme Rocher
* @since 1.0
*
* Created: Oct 10, 2007
*/
public class WebUtils extends org.springframework.web.util.WebUtils {
public static final char SLASH = '/';
private static final Log LOG = LogFactory.getLog(WebUtils.class);
public static final String ENABLE_FILE_EXTENSIONS = "grails.mime.file.extensions";
public static final String DISPATCH_ACTION_PARAMETER = "_action_";
private static final String DISPATCH_URI_SUFFIX = ".dispatch";
private static final String GRAILS_DISPATCH_SERVLET_NAME = "/grails";
public static ViewResolver lookupViewResolver(ServletContext servletContext) {
WebApplicationContext wac =
WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
String[] beanNames = wac.getBeanNamesForType(ViewResolver.class);
if(beanNames.length > 0) {
String beanName = beanNames[0];
return (ViewResolver)wac.getBean(beanName);
}
return null;
}
/**
* Looks up all of the HandlerInterceptor instances registered for the application
*
* @param servletContext The ServletContext instance
* @return An array of HandlerInterceptor instances
*/
public static HandlerInterceptor[] lookupHandlerInterceptors(ServletContext servletContext) {
WebApplicationContext wac =
WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
final Collection allHandlerInterceptors = new ArrayList();
WebRequestInterceptor[] webRequestInterceptors = lookupWebRequestInterceptors(servletContext);
for (WebRequestInterceptor webRequestInterceptor : webRequestInterceptors) {
allHandlerInterceptors.add(new WebRequestHandlerInterceptorAdapter(webRequestInterceptor));
}
final Collection handlerInterceptors = wac.getBeansOfType(HandlerInterceptor.class).values();
allHandlerInterceptors.addAll(handlerInterceptors);
return allHandlerInterceptors.toArray(new HandlerInterceptor[allHandlerInterceptors.size()]);
}
/**
* Looks up all of the WebRequestInterceptor instances registered with the application
*
* @param servletContext The ServletContext instance
* @return An array of WebRequestInterceptor instances
*/
public static WebRequestInterceptor[] lookupWebRequestInterceptors(ServletContext servletContext) {
WebApplicationContext wac =
WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
final Collection webRequestInterceptors = wac.getBeansOfType(WebRequestInterceptor.class).values();
return (WebRequestInterceptor[]) webRequestInterceptors.toArray(new WebRequestInterceptor[webRequestInterceptors.size()]);
}
/**
* Looks up the UrlMappingsHolder instance
*
* @return The UrlMappingsHolder
* @param servletContext The ServletContext object
*/
public static UrlMappingsHolder lookupUrlMappings(ServletContext servletContext) {
WebApplicationContext wac =
WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
return (UrlMappingsHolder)wac.getBean(UrlMappingsHolder.BEAN_ID);
}
/**
* The Grails dispatch servlet maps URIs like /app/grails/example/index.dispatch. This method infers the
* controller URI for the dispatch URI so that /app/grails/example/index.dispatch becomes /app/example/index
*
* @param request The request
*/
public static String getRequestURIForGrailsDispatchURI(HttpServletRequest request) {
UrlPathHelper pathHelper = new UrlPathHelper();
if(request.getRequestURI().endsWith(DISPATCH_URI_SUFFIX)) {
String path = pathHelper.getPathWithinApplication(request);
if(path.startsWith(GRAILS_DISPATCH_SERVLET_NAME)) {
path = path.substring(GRAILS_DISPATCH_SERVLET_NAME.length(),path.length());
}
return path.substring(0, path.length()-DISPATCH_URI_SUFFIX.length());
}
else {
return pathHelper.getPathWithinApplication(request);
}
}
/**
* Looks up the GrailsApplication instance
*
* @return The GrailsApplication instance
*/
public static GrailsApplication lookupApplication(ServletContext servletContext) {
WebApplicationContext wac =
WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
return (GrailsApplication)wac.getBean(GrailsApplication.APPLICATION_ID);
}
/**
* Resolves a view for the given view and UrlMappingInfo instance
*
* @param request The request
* @param info The info
* @param viewName The view name
* @param viewResolver The view resolver
* @return The view or null
* @throws Exception
*/
public static View resolveView(HttpServletRequest request, UrlMappingInfo info, String viewName, ViewResolver viewResolver) throws Exception {
String controllerName = info.getControllerName();
return resolveView(request, viewName, controllerName, viewResolver);
}
/**
* Resolves a view for the given view name and controller name
* @param request The request
* @param viewName The view name
* @param controllerName The controller name
* @param viewResolver The resolver
* @return A View or null
* @throws Exception Thrown if an error occurs
*/
public static View resolveView(HttpServletRequest request, String viewName, String controllerName, ViewResolver viewResolver) throws Exception {
GrailsWebRequest webRequest = (GrailsWebRequest)request.getAttribute(GrailsApplicationAttributes.WEB_REQUEST);
View v;
if(viewName.startsWith(String.valueOf(SLASH))) {
v = viewResolver.resolveViewName(viewName, webRequest.getLocale());
}
else {
StringBuilder buf = new StringBuilder();
buf.append(SLASH);
if(controllerName != null) {
buf.append(controllerName).append(SLASH);
}
buf.append(viewName);
v = viewResolver.resolveViewName(buf.toString(), webRequest.getLocale());
}
return v;
}
/**
* Constructs the URI to forward to using the given request and UrlMappingInfo instance
*
* @param info The UrlMappingInfo
* @return The URI to forward to
*/
public static String buildDispatchUrlForMapping(UrlMappingInfo info) {
return buildDispatchUrlForMapping(info, false);
}
private static String buildDispatchUrlForMapping(UrlMappingInfo info, boolean includeParams) {
final StringBuilder forwardUrl = new StringBuilder();
if (info.getViewName() != null) {
String viewName = info.getViewName();
forwardUrl.append(SLASH).append(viewName);
}
else {
forwardUrl.append(GrailsUrlPathHelper.GRAILS_SERVLET_PATH);
forwardUrl.append(SLASH)
.append(info.getControllerName());
if(!StringUtils.isBlank(info.getActionName())) {
forwardUrl.append(SLASH)
.append(info.getActionName());
}
forwardUrl.append(GrailsUrlPathHelper.GRAILS_DISPATCH_EXTENSION);
}
if(info.getParameters()!=null && includeParams) {
try {
forwardUrl.append(toQueryString(info.getParameters()));
}
catch (UnsupportedEncodingException e) {
throw new ControllerExecutionException("Unable to include ");
}
}
return forwardUrl.toString();
}
/**
* @see org.codehaus.groovy.grails.web.util.WebUtils#forwardRequestForUrlMappingInfo(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, org.codehaus.groovy.grails.web.mapping.UrlMappingInfo, java.util.Map)
*/
public static String forwardRequestForUrlMappingInfo(HttpServletRequest request, HttpServletResponse response, UrlMappingInfo info) throws ServletException, IOException {
return forwardRequestForUrlMappingInfo(request, response, info, Collections.EMPTY_MAP);
}
/**
* @see #forwardRequestForUrlMappingInfo(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, org.codehaus.groovy.grails.web.mapping.UrlMappingInfo, java.util.Map, boolean)
*/
public static String forwardRequestForUrlMappingInfo(HttpServletRequest request, HttpServletResponse response, UrlMappingInfo info, Map model) throws ServletException, IOException {
return forwardRequestForUrlMappingInfo(request, response, info, model, false);
}
/**
* Forwards a request for the given UrlMappingInfo object and model
*
* @param request The request
* @param response The response
* @param info The UrlMappingInfo object
* @param model The Model
* @param includeParams Whether to include any request parameters
* @return The URI forwarded too
*
* @throws ServletException Thrown when an error occurs executing the forward
* @throws IOException Thrown when an error occurs executing the forward
*/
public static String forwardRequestForUrlMappingInfo(HttpServletRequest request, HttpServletResponse response, UrlMappingInfo info, Map model, boolean includeParams) throws ServletException, IOException {
String forwardUrl = buildDispatchUrlForMapping(info, includeParams);
//populateParamsForMapping(info);
RequestDispatcher dispatcher = request.getRequestDispatcher(forwardUrl);
exposeForwardRequestAttributes(request);
exposeRequestAttributes(request, model);
dispatcher.forward(request, response);
return forwardUrl;
}
/**
* Include whatever the given UrlMappingInfo maps to within the current response
*
* @param request The request
* @param response The response
* @param info The UrlMappingInfo
* @param model The model
*
* @return The included content
*/
public static IncludedContent includeForUrlMappingInfo(HttpServletRequest request, HttpServletResponse response, UrlMappingInfo info, Map model) {
String includeUrl = buildDispatchUrlForMapping(info, true);
return includeForUrl(includeUrl, request, response, model);
}
/**
* Includes the given URL returning the resulting content as a String
*
* @param includeUrl The URL to include
* @param request The request
* @param response The response
* @param model The model
* @return The content
*/
public static IncludedContent includeForUrl(String includeUrl, HttpServletRequest request, HttpServletResponse response, Map model) {
RequestDispatcher dispatcher = request.getRequestDispatcher(includeUrl);
HttpServletResponse wrapped = WrappedResponseHolder.getWrappedResponse();
response = wrapped != null ? wrapped : response;
exposeForwardRequestAttributes(request);
exposeRequestAttributes(request, model);
try {
final IncludeResponseWrapper responseWrapper = new IncludeResponseWrapper(response);
try {
WrappedResponseHolder.setWrappedResponse(responseWrapper);
dispatcher.include(request, responseWrapper);
return new IncludedContent(responseWrapper.getContentType(), responseWrapper.getContent());
}
finally {
WrappedResponseHolder.setWrappedResponse(wrapped);
}
}
catch (Exception e) {
GrailsUtil.deepSanitize(e);
throw new ControllerExecutionException("Unable to execute include: " + e.getMessage(),e );
}
}
/**
* Converts the given params into a query string started with ?
* @param params The params
* @param encoding The encoding to use
* @return The query string
* @throws UnsupportedEncodingException If the given encoding is not supported
*/
public static String toQueryString(Map params, String encoding) throws UnsupportedEncodingException {
if(encoding == null) encoding = "UTF-8";
StringBuilder queryString = new StringBuilder("?");
for (Iterator i = params.entrySet().iterator(); i.hasNext();) {
Map.Entry entry = (Map.Entry) i.next();
boolean hasMore = i.hasNext();
boolean wasAppended = appendEntry(entry, queryString, encoding, "");
if(hasMore && wasAppended) queryString.append('&');
}
return queryString.toString();
}
/**
* Converts the given parameters to a query string using the default UTF-8 encoding
* @param parameters The parameters
* @return The query string
* @throws UnsupportedEncodingException If UTF-8 encoding is not supported
*/
public static String toQueryString(Map parameters) throws UnsupportedEncodingException {
return toQueryString(parameters, "UTF-8");
}
private static boolean appendEntry(Map.Entry entry, StringBuilder queryString, String encoding, String path) throws UnsupportedEncodingException {
String name = entry.getKey().toString();
if(name.indexOf(".") > -1) return false; // multi-d params handled by recursion
Object value = entry.getValue();
if(value == null) value = "";
else if(value instanceof GrailsParameterMap) {
GrailsParameterMap child = (GrailsParameterMap)value;
Set nestedEntrySet = child.entrySet();
for (Object aNestedEntrySet : nestedEntrySet) {
Map.Entry childEntry = (Map.Entry) aNestedEntrySet;
appendEntry(childEntry, queryString, encoding, entry.getKey().toString() + '.');
}
}
else {
queryString.append(URLEncoder.encode(path+name, encoding))
.append('=')
.append(URLEncoder.encode(value.toString(), encoding));
}
return true;
}
static class IncludeResponseWrapper extends HttpServletResponseWrapper {
private FastStringWriter sw = new FastStringWriter();
private PrintWriter pw = sw;
private FastByteArrayOutputStream os = new FastByteArrayOutputStream();
private ServletOutputStream sos = new ServletOutputStream() {
public void write(int i) throws IOException {
os.write(i);
}
};
private boolean usingStream;
private boolean usingWriter;
private int status;
private String contentType;
public IncludeResponseWrapper(HttpServletResponse httpServletResponse) {
super(httpServletResponse);
}
public String getContentType() {
return contentType;
}
@Override
public void setStatus(int i) {
this.status = i;
}
public int getStatus() {
return status;
}
@Override
public void setContentType(String s) {
this.contentType = s;
}
@Override
public void setLocale(Locale locale) {
// do nothing
}
@Override
public void sendError(int i, String s) throws IOException {
setStatus(i);
}
@Override
public void sendError(int i) throws IOException {
setStatus(i);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
if(usingWriter) throw new IllegalStateException("Method getWriter() already called");
usingStream = true;
return sos;
}
@Override
public PrintWriter getWriter() throws IOException {
if(usingStream) throw new IllegalStateException("Method getOutputStream() already called");
usingWriter = true;
return pw;
}
public String getContent() throws UnsupportedEncodingException {
return getContent("UTF-8");
}
public String getContent(String encoding) throws UnsupportedEncodingException {
if(usingWriter) return sw.toString();
else if(usingStream) {
return os.toString(encoding);
}
return "".intern();
}
}
/**
* Obtains the format from the URI. The format is the string following the . file extension in the last token of the URI.
* If nothing comes after the ".", this method assumes that there is no format and returns null.
*
* @param uri The URI
* @return The format or null if none
*/
public static String getFormatFromURI(String uri) {
if(uri.endsWith("/")) {
return null;
}
int idx = uri.lastIndexOf('/');
if(idx > -1) {
String lastToken = uri.substring(idx+1, uri.length());
idx = lastToken.lastIndexOf('.');
if(idx > -1 && idx != lastToken.length() - 1) {
return lastToken.substring(idx+1);
}
}
return null;
}
/**
* Returns the value of the "grails.mime.file.extensions" setting configured in COnfig.groovy
*
* @return True if file extensions are enabled
*/
public static boolean areFileExtensionsEnabled() {
Map config = ConfigurationHolder.getFlatConfig();
Object o = config.get(ENABLE_FILE_EXTENSIONS);
return !(o != null && o instanceof Boolean) || ((Boolean) o).booleanValue();
}
/**
* Returns the GrailsWebRequest associated with the current request.
* This is the preferred means of accessing the GrailsWebRequest
* instance. If the exception is undesired, you can use
* RequestContextHolder.getRequestAttributes() instead.
* @throws IllegalStateException if this is called outside of a
* request.
*/
public static GrailsWebRequest retrieveGrailsWebRequest() {
return (GrailsWebRequest) RequestContextHolder.currentRequestAttributes();
}
/**
* Helper method to store the given GrailsWebRequest for the current
* request. Ensures consistency between RequestContextHolder and the
* relevant request attribute. This is the preferred means of updating
* the current web request.
*/
public static void storeGrailsWebRequest(GrailsWebRequest webRequest) {
RequestContextHolder.setRequestAttributes(webRequest);
webRequest.getRequest().setAttribute(GrailsApplicationAttributes.WEB_REQUEST, webRequest);
}
/**
* Removes any GrailsWebRequest instance from the current request.
*/
public static void clearGrailsWebRequest() {
RequestAttributes reqAttrs = RequestContextHolder.getRequestAttributes();
if (reqAttrs != null) {
// First remove the web request from the HTTP request
// attributes.
GrailsWebRequest webRequest = (GrailsWebRequest) reqAttrs;
webRequest.getRequest().removeAttribute(GrailsApplicationAttributes.WEB_REQUEST);
// Now remove it from RequestContextHolder.
RequestContextHolder.setRequestAttributes(null);
}
}
/**
* Obtains the forwardURI from the request, since Grails uses a forwarding technique for URL mappings. The actual
* request URI is held within a request attribute
*
* @param request The request
* @return The forward URI
*/
public static String getForwardURI(HttpServletRequest request) {
String result = (String) request.getAttribute(WebUtils.FORWARD_REQUEST_URI_ATTRIBUTE);
if(StringUtils.isBlank(result)) result = request.getRequestURI();
return result;
}
}