Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spring Portlet MVC - Unable to return JSON data from @ResourceMapping [SPR-7344] #12003

Closed
spring-projects-issues opened this issue Jul 2, 2010 · 14 comments
Assignees
Labels
has: votes-jira Issues migrated from JIRA with more than 10 votes at the time of import in: web Issues in web modules (web, webmvc, webflux, websocket) status: declined A suggestion or change that we don't feel we should currently apply type: enhancement A general enhancement

Comments

@spring-projects-issues
Copy link
Collaborator

spring-projects-issues commented Jul 2, 2010

Ashish Sarin opened SPR-7344 and commented

In many scenarios it is required to support returning JSON data from the @ResourceMapping annotated method of controller. The @ResponseBody and ContentNegotiatingViewResolver currently work only in the servlet environment.


Affects: 3.0.3

Issue Links:

60 votes, 46 watchers

@spring-projects-issues
Copy link
Collaborator Author

Josef Vychtrle commented

Could please anybody collaborate ? I'm just pointing to the related forum threads:

http://forum.springsource.org/showthread.php?t=83673
http://forum.springsource.org/showthread.php?p=335582

@spring-projects-issues
Copy link
Collaborator Author

@spring-projects-issues
Copy link
Collaborator Author

Josef Vychtrle commented

MessageConverters is also the concern, as in spring-web

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
   <property name="messageConverters">
       <list>
           <ref bean="jsonConverter" />
           <ref bean="marshallingConverter" />
           <ref bean="atomConverter" />
       </list>
    </property>
</bean>

@spring-projects-issues
Copy link
Collaborator Author

Wendy Cameron commented

I have had a close look at this problem and have actually implemented the @ResponseBody tag by enhancing the Spring Classes.

I found that the org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter required enhancements in-line with:
org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
I found I have to bring the Methods from PortletAnnotationMappingUtils into the AnnotationMethodHandlerAdapter class as static methods due to them being package protected.

I also found that you needed to create
PortletServerHttpRequest which extends org.springframework.http.server.ServerHttpRequest and
PortletServerHttpResponse which extends org.springframework.http.server.ServerHttpResponse

I did not find you needed to make any changes to the MessageConverters.
I will attach the full class listings to this post.

I did also implement:
org.springframework.web.servlet.view.ContentNegotiatingViewResolver
However I did find that I did not require this class to get the ResponseBody annotation to work.
This class was very straight forward to modify I simply removed the file extension parameter and preference for it as url file extensions in a portlet context do not make and sense from getMediaTypes and altered the method signature to use PortletRequest and use request.getProperty(ACCEPT_HEADER) instead of getHeader.
I altered resolveViewName to change the Assert to:

Assert.isInstanceOf(PortletRequestAttributes.class, attrs);

And the call to getMediaTypes to:

List<MediaType> requestedMediaTypes = getMediaTypes(((PortletRequestAttributes) attrs).getRequest());

I did not implement the @RequestBody annotation as this something of very different coding implementation.
Also we don't use this tag. The majority of our requests are form submit requests using normal form submission processes.

BTW: I am the Same Wendy Cameron, I used to be in New Zealand however now contracting at Optus in Sydney.

The following are my full code listings (the most complex to write was the AnnotationMethodHandlerAdapter. They reside in a package that fits in with out structure. This would obviously need to be changed to be put into correct structure for Spring.\

Replacement org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter

package au.com.optus.oneportal.customercentre;

/*
 * Copyright 2002-2010 the original author or authors.
 *
 * 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.
 */

import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.BeanExpressionContext;
import org.springframework.beans.factory.config.BeanExpressionResolver;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.Ordered;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.support.BindingAwareModelMap;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.support.HandlerMethodInvoker;
import org.springframework.web.bind.annotation.support.HandlerMethodResolver;
import org.springframework.web.bind.support.DefaultSessionAttributeStore;
import org.springframework.web.bind.support.SessionAttributeStore;
import org.springframework.web.bind.support.WebArgumentResolver;
import org.springframework.web.bind.support.WebBindingInitializer;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestScope;
import org.springframework.web.multipart.MultipartRequest;
import org.springframework.web.portlet.HandlerAdapter;
import org.springframework.web.portlet.ModelAndView;
import org.springframework.web.portlet.NoHandlerFoundException;
import org.springframework.web.portlet.bind.MissingPortletRequestParameterException;
import org.springframework.web.portlet.bind.PortletRequestDataBinder;
import org.springframework.web.portlet.bind.annotation.ActionMapping;
import org.springframework.web.portlet.bind.annotation.EventMapping;
import org.springframework.web.portlet.bind.annotation.RenderMapping;
import org.springframework.web.portlet.bind.annotation.ResourceMapping;
import org.springframework.web.portlet.context.PortletWebRequest;
import org.springframework.web.portlet.handler.PortletContentGenerator;
import org.springframework.web.portlet.handler.PortletSessionRequiredException;
import org.springframework.web.portlet.util.PortletUtils;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.mvc.annotation.ModelAndViewResolver;

import javax.portlet.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Method;
import java.security.Principal;
import java.util.*;

/**
 * Implementation of the {@link org.springframework.web.portlet.HandlerAdapter}
 * interface that maps handler methods based on portlet modes, action/render phases
 * and request parameters expressed through the {@link org.springframework.web.bind.annotation.RequestMapping} annotation.
 * <p/>
 * <p>Supports request parameter binding through the {@link org.springframework.web.bind.annotation.RequestParam} annotation.
 * Also supports the {@link org.springframework.web.bind.annotation.ModelAttribute} annotation for exposing model attribute
 * values to the view, as well as {@link org.springframework.web.bind.annotation.InitBinder} for binder initialization methods
 * and {@link org.springframework.web.bind.annotation.SessionAttributes} for automatic session management of specific attributes.
 * <p/>
 * <p>This adapter can be customized through various bean properties.
 * A common use case is to apply shared binder initialization logic through
 * a custom {@link #setWebBindingInitializer WebBindingInitializer}.
 *
 * @author Juergen Hoeller
 * @author Arjen Poutsma
 * @see #setWebBindingInitializer
 * @see #setSessionAttributeStore
 * @since 2.5
 */
public class AnnotationMethodHandlerAdapter extends PortletContentGenerator
        implements HandlerAdapter, Ordered, BeanFactoryAware {

    public static final String IMPLICIT_MODEL_SESSION_ATTRIBUTE =
            AnnotationMethodHandlerAdapter.class.getName() + ".IMPLICIT_MODEL";

    public static final String IMPLICIT_MODEL_RENDER_PARAMETER = "implicitModel";


    private WebBindingInitializer webBindingInitializer;

    private SessionAttributeStore sessionAttributeStore = new DefaultSessionAttributeStore();

    private int cacheSecondsForSessionAttributeHandlers = 0;

    private boolean synchronizeOnSession = false;

    private ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();

    private WebArgumentResolver[] customArgumentResolvers;

    private ModelAndViewResolver[] customModelAndViewResolvers;

    private int order = Ordered.LOWEST_PRECEDENCE;

    private ConfigurableBeanFactory beanFactory;

    private BeanExpressionContext expressionContext;

    private final Map<Class<?>, PortletHandlerMethodResolver> methodResolverCache =
            new HashMap<Class<?>, PortletHandlerMethodResolver>();

    private HttpMessageConverter<?>[] messageConverters;


    /**
     * Specify a WebBindingInitializer which will apply pre-configured
     * configuration to every DataBinder that this controller uses.
     */
    public void setWebBindingInitializer(WebBindingInitializer webBindingInitializer) {
        this.webBindingInitializer = webBindingInitializer;
    }

    /**
     * Specify the strategy to store session attributes with.
     * <p>Default is {@link org.springframework.web.bind.support.DefaultSessionAttributeStore},
     * storing session attributes in the PortletSession, using the same
     * attribute name as in the model.
     */
    public void setSessionAttributeStore(SessionAttributeStore sessionAttributeStore) {
        Assert.notNull(sessionAttributeStore, "SessionAttributeStore must not be null");
        this.sessionAttributeStore = sessionAttributeStore;
    }

    /**
     * Cache content produced by <code>@SessionAttributes</code> annotated handlers
     * for the given number of seconds. Default is 0, preventing caching completely.
     * <p>In contrast to the "cacheSeconds" property which will apply to all general
     * handlers (but not to <code>@SessionAttributes</code> annotated handlers), this
     * setting will apply to <code>@SessionAttributes</code> annotated handlers only.
     *
     * @see #setCacheSeconds
     * @see org.springframework.web.bind.annotation.SessionAttributes
     */
    public void setCacheSecondsForSessionAttributeHandlers(int cacheSecondsForSessionAttributeHandlers) {
        this.cacheSecondsForSessionAttributeHandlers = cacheSecondsForSessionAttributeHandlers;
    }

    /**
     * Set if controller execution should be synchronized on the session,
     * to serialize parallel invocations from the same client.
     * <p>More specifically, the execution of each handler method will get
     * synchronized if this flag is "true". The best available session mutex
     * will be used for the synchronization; ideally, this will be a mutex
     * exposed by HttpSessionMutexListener.
     * <p>The session mutex is guaranteed to be the same object during
     * the entire lifetime of the session, available under the key defined
     * by the <code>SESSION_MUTEX_ATTRIBUTE</code> constant. It serves as a
     * safe reference to synchronize on for locking on the current session.
     * <p>In many cases, the PortletSession reference itself is a safe mutex
     * as well, since it will always be the same object reference for the
     * same active logical session. However, this is not guaranteed across
     * different servlet containers; the only 100% safe way is a session mutex.
     *
     * @see org.springframework.web.util.HttpSessionMutexListener
     * @see org.springframework.web.portlet.util.PortletUtils#getSessionMutex(javax.portlet.PortletSession)
     */
    public void setSynchronizeOnSession(boolean synchronizeOnSession) {
        this.synchronizeOnSession = synchronizeOnSession;
    }

    /**
     * Set the ParameterNameDiscoverer to use for resolving method parameter
     * names if needed (e.g. for default attribute names).
     * <p>Default is a {@link org.springframework.core.LocalVariableTableParameterNameDiscoverer}.
     */
    public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
        this.parameterNameDiscoverer = parameterNameDiscoverer;
    }

    /**
     * Set a custom WebArgumentResolver to use for special method parameter types.
     * Such a custom WebArgumentResolver will kick in first, having a chance to
     * resolve an argument value before the standard argument handling kicks in.
     */
    public void setCustomArgumentResolver(WebArgumentResolver argumentResolver) {
        this.customArgumentResolvers = new WebArgumentResolver[]{argumentResolver};
    }

    /**
     * Set one or more custom WebArgumentResolvers to use for special method
     * parameter types. Any such custom WebArgumentResolver will kick in first,
     * having a chance to resolve an argument value before the standard
     * argument handling kicks in.
     */
    public void setCustomArgumentResolvers(WebArgumentResolver[] argumentResolvers) {
        this.customArgumentResolvers = argumentResolvers;
    }

    /**
     * Set a custom ModelAndViewResolvers to use for special method return types. Such a custom ModelAndViewResolver will kick
     * in first, having a chance to resolve an return value before the standard ModelAndView handling kicks in.
     */
    public void setCustomModelAndViewResolver(ModelAndViewResolver customModelAndViewResolver) {
        this.customModelAndViewResolvers = new ModelAndViewResolver[]{customModelAndViewResolver};
    }

    /**
     * Set one or more custom ModelAndViewResolvers to use for special method return types. Any such custom ModelAndViewResolver
     * will kick in first, having a chance to resolve an return value before the standard ModelAndView handling kicks in.
     */
    public void setCustomModelAndViewResolvers(ModelAndViewResolver[] customModelAndViewResolvers) {
        this.customModelAndViewResolvers = customModelAndViewResolvers;
    }

    /**
     * Set the message body converters to use.
     * <p>These converters are used to convert from and to HTTP requests and responses.
     */
    public void setMessageConverters(HttpMessageConverter<?>[] messageConverters) {
        this.messageConverters = messageConverters;
    }

    /**
     * Return the message body converters that this adapter has been configured with.
     */
    public HttpMessageConverter<?>[] getMessageConverters() {
        return messageConverters;
    }

    /**
     * Specify the order value for this HandlerAdapter bean.
     * <p>Default value is <code>Integer.MAX_VALUE</code>, meaning that it's non-ordered.
     *
     * @see org.springframework.core.Ordered#getOrder()
     */
    public void setOrder(int order) {
        this.order = order;
    }

    public int getOrder() {
        return this.order;
    }

    public void setBeanFactory(BeanFactory beanFactory) {
        if (beanFactory instanceof ConfigurableBeanFactory) {
            this.beanFactory = (ConfigurableBeanFactory) beanFactory;
            this.expressionContext = new BeanExpressionContext(this.beanFactory, new RequestScope());
        }
    }


    public boolean supports(Object handler) {
        return getMethodResolver(handler).hasHandlerMethods();
    }

    public void handleAction(ActionRequest request, ActionResponse response, Object handler) throws Exception {
        Object returnValue = doHandle(request, response, handler);
        if (returnValue != null) {
            throw new IllegalStateException("Invalid action method return value: " + returnValue);
        }
    }

    public ModelAndView handleRender(RenderRequest request, RenderResponse response, Object handler) throws Exception {
        checkAndPrepare(request, response);
        return doHandle(request, response, handler);
    }

    public ModelAndView handleResource(ResourceRequest request, ResourceResponse response, Object handler) throws Exception {
        checkAndPrepare(request, response);
        return doHandle(request, response, handler);
    }

    public void handleEvent(EventRequest request, EventResponse response, Object handler) throws Exception {
        Object returnValue = doHandle(request, response, handler);
        if (returnValue != null) {
            throw new IllegalStateException("Invalid event method return value: " + returnValue);
        }
    }

    protected ModelAndView doHandle(PortletRequest request, PortletResponse response, Object handler) throws Exception {
        ExtendedModelMap implicitModel = null;

        if (response instanceof MimeResponse) {
            MimeResponse mimeResponse = (MimeResponse) response;
            // Detect implicit model from associated action phase.
            if (response instanceof RenderResponse) {
                PortletSession session = request.getPortletSession(false);
                if (session != null) {
                    if (request.getParameter(IMPLICIT_MODEL_RENDER_PARAMETER) != null) {
                        implicitModel = (ExtendedModelMap) session.getAttribute(IMPLICIT_MODEL_SESSION_ATTRIBUTE);
                    } else {
                        session.removeAttribute(IMPLICIT_MODEL_SESSION_ATTRIBUTE);
                    }
                }
            }
            if (handler.getClass().getAnnotation(SessionAttributes.class) != null) {
                // Always prevent caching in case of session attribute management.
                checkAndPrepare(request, mimeResponse, this.cacheSecondsForSessionAttributeHandlers);
            } else {
                // Uses configured default cacheSeconds setting.
                checkAndPrepare(request, mimeResponse);
            }
        }

        if (implicitModel == null) {
            implicitModel = new BindingAwareModelMap();
        }

        // Execute invokeHandlerMethod in synchronized block if required.
        if (this.synchronizeOnSession) {
            PortletSession session = request.getPortletSession(false);
            if (session != null) {
                Object mutex = PortletUtils.getSessionMutex(session);
                synchronized (mutex) {
                    return invokeHandlerMethod(request, response, handler, implicitModel);
                }
            }
        }

        return invokeHandlerMethod(request, response, handler, implicitModel);
    }

    @SuppressWarnings("unchecked")
    private ModelAndView invokeHandlerMethod(
            PortletRequest request, PortletResponse response, Object handler, ExtendedModelMap implicitModel)
            throws Exception {

        PortletWebRequest webRequest = new PortletWebRequest(request, response);
        PortletHandlerMethodResolver methodResolver = getMethodResolver(handler);
        Method handlerMethod = methodResolver.resolveHandlerMethod(request);
        PortletHandlerMethodInvoker methodInvoker = new PortletHandlerMethodInvoker(methodResolver);

        Object result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel);
        ModelAndView mav = methodInvoker.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel,
                webRequest);
        methodInvoker.updateModelAttributes(
                handler, (mav != null ? mav.getModel() : null), implicitModel, webRequest);

        // Expose implicit model for subsequent render phase.
        if (response instanceof StateAwareResponse && !implicitModel.isEmpty()) {
            StateAwareResponse stateResponse = (StateAwareResponse) response;
            Map<?, ?> modelToStore = implicitModel;
            try {
                stateResponse.setRenderParameter(IMPLICIT_MODEL_RENDER_PARAMETER, Boolean.TRUE.toString());
                if (response instanceof EventResponse) {
                    // Update the existing model, if any, when responding to an event -
                    // whereas we're replacing the model in case of an action response.
                    Map existingModel = (Map) request.getPortletSession().getAttribute(IMPLICIT_MODEL_SESSION_ATTRIBUTE);
                    if (existingModel != null) {
                        existingModel.putAll(implicitModel);
                        modelToStore = existingModel;
                    }
                }
                request.getPortletSession().setAttribute(IMPLICIT_MODEL_SESSION_ATTRIBUTE, modelToStore);
            } catch (IllegalStateException ex) {
                // Probably sendRedirect called... no need to expose model to render phase.
            }
        }

        return mav;
    }

    /**
     * Build a HandlerMethodResolver for the given handler type.
     */
    private PortletHandlerMethodResolver getMethodResolver(Object handler) {
        Class handlerClass = ClassUtils.getUserClass(handler);
        synchronized (this.methodResolverCache) {
            PortletHandlerMethodResolver resolver = this.methodResolverCache.get(handlerClass);
            if (resolver == null) {
                resolver = new PortletHandlerMethodResolver(handlerClass);
                this.methodResolverCache.put(handlerClass, resolver);
            }
            return resolver;
        }
    }


    /**
     * Template method for creating a new PortletRequestDataBinder instance.
     * <p>The default implementation creates a standard PortletRequestDataBinder.
     * This can be overridden for custom PortletRequestDataBinder subclasses.
     *
     * @param request    current portlet request
     * @param target     the target object to bind onto (or <code>null</code>
     *                   if the binder is just used to convert a plain parameter value)
     * @param objectName the objectName of the target object
     * @return the PortletRequestDataBinder instance to use
     * @throws Exception in case of invalid state or arguments
     * @see PortletRequestDataBinder#bind(javax.portlet.PortletRequest)
     */
    protected PortletRequestDataBinder createBinder(
            PortletRequest request, Object target, String objectName) throws Exception {

        return new PortletRequestDataBinder(target, objectName);
    }


    /**
     * Portlet-specific subclass of {@link org.springframework.web.bind.annotation.support.HandlerMethodResolver}.
     */
    private static class PortletHandlerMethodResolver extends HandlerMethodResolver {

        private final Map<Method, RequestMappingInfo> mappings = new HashMap<Method, RequestMappingInfo>();

        public PortletHandlerMethodResolver(Class<?> handlerType) {
            init(handlerType);
        }

        @Override
        protected boolean isHandlerMethod(Method method) {
            if (this.mappings.containsKey(method)) {
                return true;
            }
            RequestMappingInfo mappingInfo = new RequestMappingInfo();
            RequestMapping requestMapping = AnnotationUtils.findAnnotation(method, RequestMapping.class);
            ActionMapping actionMapping = AnnotationUtils.findAnnotation(method, ActionMapping.class);
            RenderMapping renderMapping = AnnotationUtils.findAnnotation(method, RenderMapping.class);
            ResourceMapping resourceMapping = AnnotationUtils.findAnnotation(method, ResourceMapping.class);
            EventMapping eventMapping = AnnotationUtils.findAnnotation(method, EventMapping.class);
            if (actionMapping != null) {
                mappingInfo.initPhaseMapping(PortletRequest.ACTION_PHASE, actionMapping.value(), actionMapping.params());
            }
            if (renderMapping != null) {
                mappingInfo.initPhaseMapping(PortletRequest.RENDER_PHASE, renderMapping.value(), renderMapping.params());
            }
            if (resourceMapping != null) {
                mappingInfo.initPhaseMapping(PortletRequest.RESOURCE_PHASE, resourceMapping.value(), new String[0]);
            }
            if (eventMapping != null) {
                mappingInfo.initPhaseMapping(PortletRequest.EVENT_PHASE, eventMapping.value(), new String[0]);
            }
            if (requestMapping != null) {
                mappingInfo.initStandardMapping(requestMapping.value(), requestMapping.method(),
                        requestMapping.params(), requestMapping.headers());
                if (mappingInfo.phase == null) {
                    mappingInfo.phase = determineDefaultPhase(method);
                }
            }
            if (mappingInfo.phase != null) {
                this.mappings.put(method, mappingInfo);
                return true;
            }
            return false;
        }

        public Method resolveHandlerMethod(PortletRequest request) throws PortletException {
            Map<RequestMappingInfo, Method> targetHandlerMethods = new LinkedHashMap<RequestMappingInfo, Method>();
            for (Method handlerMethod : getHandlerMethods()) {
                RequestMappingInfo mappingInfo = this.mappings.get(handlerMethod);
                if (mappingInfo.match(request)) {
                    Method oldMappedMethod = targetHandlerMethods.put(mappingInfo, handlerMethod);
                    if (oldMappedMethod != null && oldMappedMethod != handlerMethod) {
                        throw new IllegalStateException("Ambiguous handler methods mapped for portlet mode '" +
                                request.getPortletMode() + "': {" + oldMappedMethod + ", " + handlerMethod +
                                "}. If you intend to handle the same mode in multiple methods, then factor " +
                                "them out into a dedicated handler class with that mode mapped at the type level!");
                    }
                }
            }
            if (!targetHandlerMethods.isEmpty()) {
                if (targetHandlerMethods.size() == 1) {
                    return targetHandlerMethods.values().iterator().next();
                } else {
                    RequestMappingInfo bestMappingMatch = null;
                    for (RequestMappingInfo mapping : targetHandlerMethods.keySet()) {
                        if (bestMappingMatch == null) {
                            bestMappingMatch = mapping;
                        } else {
                            if (mapping.isBetterMatchThan(bestMappingMatch)) {
                                bestMappingMatch = mapping;
                            }
                        }
                    }
                    return targetHandlerMethods.get(bestMappingMatch);
                }
            } else {
                throw new NoHandlerFoundException("No matching handler method found for portlet request", request);
            }
        }

        private String determineDefaultPhase(Method handlerMethod) {
            if (!void.class.equals(handlerMethod.getReturnType())) {
                return PortletRequest.RENDER_PHASE;
            }
            for (Class<?> argType : handlerMethod.getParameterTypes()) {
                if (ActionRequest.class.isAssignableFrom(argType) || ActionResponse.class.isAssignableFrom(argType) ||
                        InputStream.class.isAssignableFrom(argType) || Reader.class.isAssignableFrom(argType)) {
                    return PortletRequest.ACTION_PHASE;
                } else if (RenderRequest.class.isAssignableFrom(argType) || RenderResponse.class.isAssignableFrom(argType) ||
                        OutputStream.class.isAssignableFrom(argType) || Writer.class.isAssignableFrom(argType)) {
                    return PortletRequest.RENDER_PHASE;
                } else if (ResourceRequest.class.isAssignableFrom(argType) || ResourceResponse.class.isAssignableFrom(argType)) {
                    return PortletRequest.RESOURCE_PHASE;
                } else if (EventRequest.class.isAssignableFrom(argType) || EventResponse.class.isAssignableFrom(argType)) {
                    return PortletRequest.EVENT_PHASE;
                }
            }
            return "";
        }
    }

    /**
     * Portlet-specific subclass of {@link org.springframework.web.bind.annotation.support.HandlerMethodInvoker}.
     */
    private class PortletHandlerMethodInvoker extends HandlerMethodInvoker {

        private boolean responseArgumentUsed = false;

        public PortletHandlerMethodInvoker(HandlerMethodResolver resolver) {
            super(resolver, webBindingInitializer, sessionAttributeStore,
                    parameterNameDiscoverer, customArgumentResolvers, null);
        }

        @Override
        protected void raiseMissingParameterException(String paramName, Class paramType) throws Exception {
            throw new MissingPortletRequestParameterException(paramName, paramType.getSimpleName());
        }

        @Override
        protected void raiseSessionRequiredException(String message) throws Exception {
            throw new PortletSessionRequiredException(message);
        }

        @Override
        protected WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName)
                throws Exception {

            return AnnotationMethodHandlerAdapter.this.createBinder(
                    webRequest.getNativeRequest(PortletRequest.class), target, objectName);
        }

        @Override
        protected void doBind(WebDataBinder binder, NativeWebRequest webRequest) throws Exception {
            PortletRequestDataBinder portletBinder = (PortletRequestDataBinder) binder;
            portletBinder.bind(webRequest.getNativeRequest(PortletRequest.class));
        }

        @Override
        protected Object resolveDefaultValue(String value) {
            if (beanFactory == null) {
                return value;
            }
            String placeholdersResolved = beanFactory.resolveEmbeddedValue(value);
            BeanExpressionResolver exprResolver = beanFactory.getBeanExpressionResolver();
            if (exprResolver == null) {
                return value;
            }
            return exprResolver.evaluate(placeholdersResolved, expressionContext);
        }

        @Override
        protected Object resolveCookieValue(String cookieName, Class paramType, NativeWebRequest webRequest)
                throws Exception {

            PortletRequest portletRequest = webRequest.getNativeRequest(PortletRequest.class);
            Cookie cookieValue = PortletUtils.getCookie(portletRequest, cookieName);
            if (Cookie.class.isAssignableFrom(paramType)) {
                return cookieValue;
            } else if (cookieValue != null) {
                return cookieValue.getValue();
            } else {
                return null;
            }
        }

        @Override
        protected Object resolveStandardArgument(Class<?> parameterType, NativeWebRequest webRequest)
                throws Exception {

            PortletRequest request = webRequest.getNativeRequest(PortletRequest.class);
            PortletResponse response = webRequest.getNativeResponse(PortletResponse.class);

            if (PortletRequest.class.isAssignableFrom(parameterType) ||
                    MultipartRequest.class.isAssignableFrom(parameterType)) {
                Object nativeRequest = webRequest.getNativeRequest(parameterType);
                if (nativeRequest == null) {
                    throw new IllegalStateException(
                            "Current request is not of type [" + parameterType.getName() + "]: " + request);
                }
                return nativeRequest;
            } else if (PortletResponse.class.isAssignableFrom(parameterType)) {
                Object nativeResponse = webRequest.getNativeResponse(parameterType);
                if (nativeResponse == null) {
                    throw new IllegalStateException(
                            "Current response is not of type [" + parameterType.getName() + "]: " + response);
                }
                return nativeResponse;
            } else if (PortletSession.class.isAssignableFrom(parameterType)) {
                return request.getPortletSession();
            } else if (PortletPreferences.class.isAssignableFrom(parameterType)) {
                return request.getPreferences();
            } else if (PortletMode.class.isAssignableFrom(parameterType)) {
                return request.getPortletMode();
            } else if (WindowState.class.isAssignableFrom(parameterType)) {
                return request.getWindowState();
            } else if (PortalContext.class.isAssignableFrom(parameterType)) {
                return request.getPortalContext();
            } else if (Principal.class.isAssignableFrom(parameterType)) {
                return request.getUserPrincipal();
            } else if (Locale.class.equals(parameterType)) {
                return request.getLocale();
            } else if (InputStream.class.isAssignableFrom(parameterType)) {
                if (!(request instanceof ClientDataRequest)) {
                    throw new IllegalStateException("InputStream can only get obtained for Action/ResourceRequest");
                }
                return ((ClientDataRequest) request).getPortletInputStream();
            } else if (Reader.class.isAssignableFrom(parameterType)) {
                if (!(request instanceof ClientDataRequest)) {
                    throw new IllegalStateException("Reader can only get obtained for Action/ResourceRequest");
                }
                return ((ClientDataRequest) request).getReader();
            } else if (OutputStream.class.isAssignableFrom(parameterType)) {
                if (!(response instanceof MimeResponse)) {
                    throw new IllegalStateException("OutputStream can only get obtained for Render/ResourceResponse");
                }
                return ((MimeResponse) response).getPortletOutputStream();
            } else if (Writer.class.isAssignableFrom(parameterType)) {
                if (!(response instanceof MimeResponse)) {
                    throw new IllegalStateException("Writer can only get obtained for Render/ResourceResponse");
                }
                return ((MimeResponse) response).getWriter();
            } else if (Event.class.equals(parameterType)) {
                if (!(request instanceof EventRequest)) {
                    throw new IllegalStateException("Event can only get obtained from EventRequest");
                }
                return ((EventRequest) request).getEvent();
            }
            return super.resolveStandardArgument(parameterType, webRequest);
        }

        @SuppressWarnings("unchecked")
        public ModelAndView getModelAndView(Method handlerMethod, Class handlerType, Object returnValue, ExtendedModelMap implicitModel,
                                            PortletWebRequest webRequest) throws Exception {
            // Invoke custom resolvers if present...
            if (customModelAndViewResolvers != null) {
                for (ModelAndViewResolver mavResolver : customModelAndViewResolvers) {
                    org.springframework.web.servlet.ModelAndView smav =
                            mavResolver.resolveModelAndView(handlerMethod, handlerType, returnValue, implicitModel, webRequest);
                    if (smav != ModelAndViewResolver.UNRESOLVED) {
                        return (smav.isReference() ?
                                new ModelAndView(smav.getViewName(), smav.getModelMap()) :
                                new ModelAndView(smav.getView(), smav.getModelMap()));
                    }
                }
            }

            if (AnnotationUtils.findAnnotation(handlerMethod, ResponseBody.class) != null) {
                handleResponseBody(returnValue, webRequest);
                return null;
            } else if (returnValue instanceof ModelAndView) {
                ModelAndView mav = (ModelAndView) returnValue;
                mav.getModelMap().mergeAttributes(implicitModel);
                return mav;
            } else if (returnValue instanceof org.springframework.web.servlet.ModelAndView) {
                org.springframework.web.servlet.ModelAndView smav = (org.springframework.web.servlet.ModelAndView) returnValue;
                ModelAndView mav = (smav.isReference() ?
                        new ModelAndView(smav.getViewName(), smav.getModelMap()) :
                        new ModelAndView(smav.getView(), smav.getModelMap()));
                mav.getModelMap().mergeAttributes(implicitModel);
                return mav;
            } else if (returnValue instanceof Model) {
                return new ModelAndView().addAllObjects(implicitModel).addAllObjects(((Model) returnValue).asMap());
            } else if (returnValue instanceof View) {
                return new ModelAndView(returnValue).addAllObjects(implicitModel);
            } else if (handlerMethod.isAnnotationPresent(ModelAttribute.class)) {
                addReturnValueAsModelAttribute(handlerMethod, handlerType, returnValue, implicitModel);
                return new ModelAndView().addAllObjects(implicitModel);
            } else if (returnValue instanceof Map) {
                return new ModelAndView().addAllObjects(implicitModel).addAllObjects((Map) returnValue);
            } else if (returnValue instanceof String) {
                return new ModelAndView((String) returnValue).addAllObjects(implicitModel);
            } else if (returnValue == null) {
                // Either returned null or was 'void' return.
                return null;
            } else if (!BeanUtils.isSimpleProperty(returnValue.getClass())) {
                // Assume a single model attribute...
                addReturnValueAsModelAttribute(handlerMethod, handlerType, returnValue, implicitModel);
                return new ModelAndView().addAllObjects(implicitModel);
            } else {
                throw new IllegalArgumentException("Invalid handler method return value: " + returnValue);
            }
        }


        private void handleResponseBody(Object returnValue, PortletWebRequest webRequest)
                throws Exception {
            if (returnValue == null) {
                return;
            }
            HttpInputMessage inputMessage = createHttpInputMessage(webRequest);
            HttpOutputMessage outputMessage = createHttpOutputMessage(webRequest);
            writeWithMessageConverters(returnValue, inputMessage, outputMessage);
        }

        protected HttpInputMessage createHttpInputMessage(NativeWebRequest webRequest) throws Exception {
            PortletRequest portletRequest = webRequest.getNativeRequest(PortletRequest.class);
            return AnnotationMethodHandlerAdapter.this.createHttpInputMessage(portletRequest);
        }

        protected HttpOutputMessage createHttpOutputMessage(NativeWebRequest webRequest) throws Exception {
            MimeResponse servletResponse = (MimeResponse) webRequest.getNativeResponse();
            return AnnotationMethodHandlerAdapter.this.createHttpOutputMessage(servletResponse);
        }

        private void writeWithMessageConverters(Object returnValue,
                                                HttpInputMessage inputMessage, HttpOutputMessage outputMessage)
                throws IOException, HttpMediaTypeNotAcceptableException {
            List<MediaType> acceptedMediaTypes = inputMessage.getHeaders().getAccept();
            if (acceptedMediaTypes.isEmpty()) {
                acceptedMediaTypes = Collections.singletonList(MediaType.ALL);
            }
            MediaType.sortByQualityValue(acceptedMediaTypes);
            Class<?> returnValueType = returnValue.getClass();
            List<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>();
            if (getMessageConverters() != null) {
                for (MediaType acceptedMediaType : acceptedMediaTypes) {
                    for (HttpMessageConverter messageConverter : getMessageConverters()) {
                        if (messageConverter.canWrite(returnValueType, acceptedMediaType)) {
                            messageConverter.write(returnValue, acceptedMediaType, outputMessage);
                            if (logger.isDebugEnabled()) {
                                MediaType contentType = outputMessage.getHeaders().getContentType();
                                if (contentType == null) {
                                    contentType = acceptedMediaType;
                                }
                                logger.debug("Written [" + returnValue + "] as \"" + contentType +
                                        "\" using [" + messageConverter + "]");
                            }
                            this.responseArgumentUsed = true;
                            return;
                        }
                    }
                }
                for (HttpMessageConverter messageConverter : messageConverters) {
                    allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());
                }
            }
            throw new HttpMediaTypeNotAcceptableException(allSupportedMediaTypes);
        }
    }

    /**
     * Template method for creating a new HttpInputMessage instance.
     * <p>The default implementation creates a standard {@link org.springframework.http.server.ServletServerHttpRequest}.
     * This can be overridden for custom {@code HttpInputMessage} implementations
     *
     * @param portletRequest current HTTP request
     * @return the HttpInputMessage instance to use
     * @throws Exception in case of errors
     */
    protected HttpInputMessage createHttpInputMessage(PortletRequest portletRequest) throws Exception {
        return new PortletServerHttpRequest(portletRequest);
    }

    /**
     * Template method for creating a new HttpOuputMessage instance.
     * <p>The default implementation creates a standard {@link org.springframework.http.server.ServletServerHttpResponse}.
     * This can be overridden for custom {@code HttpOutputMessage} implementations
     *
     * @param portletResponse current PortletResponse
     * @return the HttpInputMessage instance to use
     * @throws Exception in case of errors
     */
    protected HttpOutputMessage createHttpOutputMessage(PortletResponse portletResponse) throws Exception {
        return new PortletServerHttpResponse((MimeResponse)portletResponse);
    }


    /**
     * Holder for request mapping metadata. Allows for finding a best matching candidate.
     */
    protected static class RequestMappingInfo {

        public final Set<PortletMode> modes = new HashSet<PortletMode>();

        public String phase;

        public String value;

        public final Set<String> methods = new HashSet<String>();

        public String[] params = new String[0];

        public String[] headers = new String[0];

        public void initStandardMapping(String[] modes, RequestMethod[] methods, String[] params, String[] headers) {
            for (String mode : modes) {
                this.modes.add(new PortletMode(mode));
            }
            for (RequestMethod method : methods) {
                this.methods.add(method.name());
            }
            this.params = StringUtils.mergeStringArrays(this.params, params);
            this.headers = StringUtils.mergeStringArrays(this.headers, headers);
        }

        public void initPhaseMapping(String phase, String value, String[] params) {
            if (this.phase != null) {
                throw new IllegalStateException(
                        "Invalid mapping - more than one phase specified: '" + this.phase + "', '" + phase + "'");
            }
            this.phase = phase;
            this.value = value;
            this.params = StringUtils.mergeStringArrays(this.params, params);
        }

        public boolean match(PortletRequest request) {
            if (!this.modes.isEmpty() && !this.modes.contains(request.getPortletMode())) {
                return false;
            }
            if (StringUtils.hasLength(this.phase) &&
                    !this.phase.equals(request.getAttribute(PortletRequest.LIFECYCLE_PHASE))) {
                return false;
            }
            if (StringUtils.hasLength(this.value)) {
                if (this.phase.equals(PortletRequest.ACTION_PHASE) &&
                        !this.value.equals(request.getParameter(ActionRequest.ACTION_NAME))) {
                    return false;
                } else if (this.phase.equals(PortletRequest.RENDER_PHASE) &&
                        !(new WindowState(this.value)).equals(request.getWindowState())) {
                    return false;
                } else if (this.phase.equals(PortletRequest.RESOURCE_PHASE) &&
                        !this.value.equals(((ResourceRequest) request).getResourceID())) {
                    return false;
                } else if (this.phase.equals(PortletRequest.EVENT_PHASE)) {
                    Event event = ((EventRequest) request).getEvent();
                    if (!this.value.equals(event.getName()) && !this.value.equals(event.getQName().toString())) {
                        return false;
                    }
                }
            }
            return AnnotationMethodHandlerAdapter.checkRequestMethod(this.methods, request) &&
                    AnnotationMethodHandlerAdapter.checkParameters(this.params, request) &&
                    AnnotationMethodHandlerAdapter.checkHeaders(this.headers, request);
        }

        public boolean isBetterMatchThan(RequestMappingInfo other) {
            return ((!this.modes.isEmpty() && other.modes.isEmpty()) ||
                    (StringUtils.hasLength(this.phase) && !StringUtils.hasLength(other.phase)) ||
                    (StringUtils.hasLength(this.value) && !StringUtils.hasLength(other.value)) ||
                    (!this.methods.isEmpty() && other.methods.isEmpty()) ||
                    this.params.length > other.params.length);
        }

        @Override
        public boolean equals(Object obj) {
            RequestMappingInfo other = (RequestMappingInfo) obj;
            return (this.modes.equals(other.modes) &&
                    ObjectUtils.nullSafeEquals(this.phase, other.phase) &&
                    ObjectUtils.nullSafeEquals(this.value, other.value) &&
                    this.methods.equals(other.methods) &&
                    Arrays.equals(this.params, other.params) &&
                    Arrays.equals(this.headers, other.headers));
        }

        @Override
        public int hashCode() {
            return (ObjectUtils.nullSafeHashCode(this.modes) * 29 + this.phase.hashCode());
        }
    }


    /**
     * Check whether the given portlet modes matches the specified type-level modes.
     *
     * @param modes          the mapped portlet modes to check
     * @param typeLevelModes the type-level mode mappings to check against
     */
    public static boolean validateModeMapping(String[] modes, String[] typeLevelModes) {
        if (!ObjectUtils.isEmpty(modes) && !ObjectUtils.isEmpty(typeLevelModes)) {
            for (String mode : modes) {
                boolean match = false;
                for (String typeLevelMode : typeLevelModes) {
                    if (mode.equalsIgnoreCase(typeLevelMode)) {
                        match = true;
                    }
                }
                if (!match) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Check whether the given request matches the specified request methods.
     *
     * @param methods the request methods to check against
     * @param request the current request to check
     */
    public static boolean checkRequestMethod(RequestMethod[] methods, PortletRequest request) {
        if (methods.length == 0) {
            return true;
        }
        if (!(request instanceof ClientDataRequest)) {
            return false;
        }
        String method = ((ClientDataRequest) request).getMethod();
        for (RequestMethod candidate : methods) {
            if (method.equals(candidate.name())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Check whether the given request matches the specified request methods.
     *
     * @param methods the request methods to check against
     * @param request the current request to check
     */
    public static boolean checkRequestMethod(Set<String> methods, PortletRequest request) {
        if (!methods.isEmpty()) {
            if (!(request instanceof ClientDataRequest)) {
                return false;
            }
            String method = ((ClientDataRequest) request).getMethod();
            if (!methods.contains(method)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Check whether the given request matches the specified parameter conditions.
     *
     * @param params  the parameter conditions, following
     *                {@link org.springframework.web.bind.annotation.RequestMapping#params()}
     * @param request the current request to check
     */
    public static boolean checkParameters(String[] params, PortletRequest request) {
        if (!ObjectUtils.isEmpty(params)) {
            for (String param : params) {
                int separator = param.indexOf('=');
                if (separator == -1) {
                    if (param.startsWith("!")) {
                        if (PortletUtils.hasSubmitParameter(request, param.substring(1))) {
                            return false;
                        }
                    } else if (!PortletUtils.hasSubmitParameter(request, param)) {
                        return false;
                    }
                } else {
                    String key = param.substring(0, separator);
                    String value = param.substring(separator + 1);
                    if (!value.equals(request.getParameter(key))) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    /**
     * Check whether the given request matches the specified header conditions.
     *
     * @param headers the header conditions, following {@link RequestMapping#headers()}
     * @param request the current HTTP request to check
     */
    public static boolean checkHeaders(String[] headers, PortletRequest request) {
        if (!ObjectUtils.isEmpty(headers)) {
            for (String header : headers) {
                int separator = header.indexOf('=');
                if (separator == -1) {
                    if (header.startsWith("!")) {
                        if (request.getProperty(header.substring(1)) != null) {
                            return false;
                        }
                    } else if (request.getProperty(header) == null) {
                        return false;
                    }
                } else {
                    String key = header.substring(0, separator);
                    String value = header.substring(separator + 1);
                    if (isMediaTypeHeader(key)) {
                        List<MediaType> requestMediaTypes = MediaType.parseMediaTypes(request.getProperty(key));
                        List<MediaType> valueMediaTypes = MediaType.parseMediaTypes(value);
                        boolean found = false;
                        for (Iterator<MediaType> valIter = valueMediaTypes.iterator(); valIter.hasNext() && !found; ) {
                            MediaType valueMediaType = valIter.next();
                            for (Iterator<MediaType> reqIter = requestMediaTypes.iterator(); reqIter.hasNext() && !found; ) {
                                MediaType requestMediaType = reqIter.next();
                                if (valueMediaType.includes(requestMediaType)) {
                                    found = true;
                                }
                            }

                        }
                        if (!found) {
                            return false;
                        }
                    } else if (!value.equals(request.getProperty(key))) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    private static boolean isMediaTypeHeader(String headerName) {
        return "Accept".equalsIgnoreCase(headerName) || "Content-Type".equalsIgnoreCase(headerName);
    }


}

New PortletServerHttpRequest

package au.com.optus.oneportal.customercentre;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.util.Assert;

import javax.portlet.ActionRequest;
import javax.portlet.PortletRequest;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;

/**
 * Created by IntelliJ IDEA.
 * User: dev
 * Date: 25/07/11
 * Time: 11:21 AM
 * To change this template use File | Settings | File Templates.
 */
public class PortletServerHttpRequest implements ServerHttpRequest {

    private final PortletRequest portletRequest;

    private HttpHeaders headers;

    public PortletServerHttpRequest(PortletRequest portletRequest) {
        Assert.notNull(portletRequest, "'servletRequest' must not be null");
        this.portletRequest = portletRequest;
    }


    @Override
    public HttpMethod getMethod() {
        //This is not available in Portlet context
        return null;
    }

    @Override
    public URI getURI() {
        throw new IllegalStateException("Not allowed in a portlet");
    }

    @Override
    public InputStream getBody() throws IOException {
        if (portletRequest instanceof ActionRequest) {
            return ((ActionRequest) portletRequest).getPortletInputStream();
        } else {
            throw new IllegalStateException("Not allowed in render phase");
        }
    }

    @Override
    public HttpHeaders getHeaders() {
        //Algorithm to Retrieve The Headers from the Portlet Request
        HttpHeaders headers = new HttpHeaders();
        Enumeration<String> propertyNames = portletRequest.getPropertyNames();
        while (propertyNames.hasMoreElements()) {
            String name = propertyNames.nextElement();
            List<String> propertiesList = new ArrayList<String>();
            Enumeration<String> properties = portletRequest.getProperties(name);
            while (properties.hasMoreElements()) {
                propertiesList.add(properties.nextElement());
            }
            headers.put(name, propertiesList);
        }
        return headers;
    }
}

New

 ServerHttpResponse
package au.com.optus.oneportal.customercentre;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.Assert;

import javax.portlet.MimeResponse;
import javax.portlet.PortletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;

public class PortletServerHttpResponse implements ServerHttpResponse {

    private final PortletResponse portletResponse;

	private final HttpHeaders headers = new HttpHeaders();

	private boolean headersWritten = false;

    public PortletServerHttpResponse(PortletResponse portletResponse) {
        Assert.notNull(portletResponse, "'servletResponse' must not be null");
        this.portletResponse = portletResponse;
    }

    @Override
    public void setStatusCode(HttpStatus status) {
        //this.portletResponse.setProperty();
    }

    @Override
    public void close() {
        writeHeaders();
    }

    @Override
    public OutputStream getBody() throws IOException {
        writeHeaders();
        if (this.portletResponse instanceof MimeResponse) {
		    return ((MimeResponse)this.portletResponse).getPortletOutputStream();
        } else {
            throw new IllegalStateException("Not allowed in a action phase");
        }
    }

    @Override
    public HttpHeaders getHeaders() {
        return headersWritten ? HttpHeaders.readOnlyHttpHeaders(headers) : this.headers;
    }

    private void writeHeaders() {
		if (!this.headersWritten) {
			for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
				String headerName = entry.getKey();
				for (String headerValue : entry.getValue()) {
					this.portletResponse.addProperty(headerName, headerValue);
				}
			}
			this.headersWritten = true;
		}
	}
}

Replacement org.springframework.web.servlet.view.ContentNegotiatingViewResolver

/*
 * Copyright 2002-2010 the original author or authors.
 *
 * 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 au.com.optus.oneportal.customercentre;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.activation.FileTypeMap;
import javax.activation.MimetypesFileTypeMap;
import javax.portlet.PortletRequest;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.core.OrderComparator;
import org.springframework.core.Ordered;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.support.WebApplicationObjectSupport;
import org.springframework.web.portlet.context.PortletRequestAttributes;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.util.UrlPathHelper;
import org.springframework.web.util.WebUtils;

/**
 * Implementation of {@link ViewResolver} that resolves a view based on the request file name or {@code Accept} header.
 *
 * <p>The {@code ContentNegotiatingViewResolver} does not resolve views itself, but delegates to other {@link
 * ViewResolver}s. By default, these other view resolvers are picked up automatically from the application context,
 * though they can also be set explicitly by using the {@link #setViewResolvers(List) viewResolvers} property.
 * <strong>Note</strong> that in order for this view resolver to work properly, the {@link #setOrder(int) order}
 * property needs to be set to a higher precedence than the others (the default is {@link Ordered#HIGHEST_PRECEDENCE}.)
 *
 * <p>This view resolver uses the requested {@linkplain MediaType media type} to select a suitable {@link View} for a
 * request. This media type is determined by using the following criteria:
 * <ol>
 * <li>If the request contains a parameter defining the extension and if the {@link #setFavorParameter(boolean)}
 * property is <code>true</code>, the {@link #setMediaTypes(Map) mediaTypes} property is inspected for a matching
 * media type. The default name of the parameter is <code>format</code> and it can be configured using the
 * {@link #setParameterName(String) parameterName} property.</li>
 * <li>If there is no match in the {@link #setMediaTypes(Map) mediaTypes} property and if the Java Activation
 * Framework (JAF) is both {@linkplain #setUseJaf(boolean) enabled} and present on the class path,
 * {@link FileTypeMap#getContentType(String)} is used instead.</li>
 * <li>If the previous steps did not result in a media type, and
 * {@link #setIgnoreAcceptHeader(boolean) ignoreAcceptHeader} is {@code false}, the request {@code Accept} header is
 * used.</li>
 * </ol>
 *
 * Once the requested media type has been determined, this resolver queries each delegate view resolver for a
 * {@link View} and determines if the requested media type is {@linkplain MediaType#includes(MediaType) compatible}
 * with the view's {@linkplain View#getContentType() content type}). The most compatible view is returned.
 *
 * <p>Additionally, this view resolver exposes the {@link #setDefaultViews(List) defaultViews} property, allowing you to
 * override the views provided by the view resolvers. Note that these default views are offered as candicates, and
 * still need have the content type requested (via file extension, parameter, or {@code Accept} header, described above).
 * You can also set the {@linkplain #setDefaultContentType(MediaType) default content type} directly, which will be
 * returned when the other mechanisms ({@code Accept} header, file extension or parameter) do not result in a match.
 *
 * <p>For example, if the request path is {@code /view.html}, this view resolver will look for a view that has the
 * {@code text/html} content type (based on the {@code html} file extension). A request for {@code /view} with a {@code
 * text/html} request {@code Accept} header has the same result.
 *
 * @author Arjen Poutsma
 * @author Juergen Hoeller
 * @since 3.0
 * @see ViewResolver
 * @see org.springframework.web.servlet.view.InternalResourceViewResolver
 * @see org.springframework.web.servlet.view.BeanNameViewResolver
 */
public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered {

	private static final Log logger = LogFactory.getLog(ContentNegotiatingViewResolver.class);

	private static final String ACCEPT_HEADER = "Accept";

	private static final boolean jafPresent =
			ClassUtils.isPresent("javax.activation.FileTypeMap", ContentNegotiatingViewResolver.class.getClassLoader());

	private static final UrlPathHelper urlPathHelper = new UrlPathHelper();


	private int order = Ordered.HIGHEST_PRECEDENCE;

	private boolean favorParameter = false;

	private String parameterName = "format";

	private boolean useNotAcceptableStatusCode = false;

	private boolean ignoreAcceptHeader = false;

	private boolean useJaf = true;

	private ConcurrentMap<String, MediaType> mediaTypes = new ConcurrentHashMap<String, MediaType>();

	private List<View> defaultViews;

	private MediaType defaultContentType;

	private List<ViewResolver> viewResolvers;


	public void setOrder(int order) {
		this.order = order;
	}

	public int getOrder() {
		return this.order;
	}

	/**
	 * Indicates whether a request parameter should be used to determine the requested media type,
	 * in favor of looking at the {@code Accept} header. The default value is {@code false}.
	 * <p>For instance, when this flag is <code>true</code>, a request for {@code /hotels?format=pdf} will result
	 * in an {@code AbstractPdfView} being resolved, while the {@code Accept} header can be the browser-defined
	 * {@code text/html,application/xhtml+xml}.
	 */
	public void setFavorParameter(boolean favorParameter) {
		this.favorParameter = favorParameter;
	}

	/**
	 * Sets the parameter name that can be used to determine the requested media type if the {@link
	 * #setFavorParameter(boolean)} property is {@code true}. The default parameter name is {@code format}.
	 */
	public void setParameterName(String parameterName) {
		this.parameterName = parameterName;
	}

	/**
	 * Indicates whether the HTTP {@code Accept} header should be ignored. Default is {@code false}.
	 */
	public void setIgnoreAcceptHeader(boolean ignoreAcceptHeader) {
		this.ignoreAcceptHeader = ignoreAcceptHeader;
	}

	/**
	 * Indicates whether a {@link HttpServletResponse#SC_NOT_ACCEPTABLE 406 Not Acceptable} status code should be
	 * returned if no suitable view can be found.
	 *
	 * <p>Default is {@code false}, meaning that this view resolver returns {@code null} for
	 * {@link #resolveViewName(String, Locale)} when an acceptable view cannot be found. This will allow for view
	 * resolvers chaining. When this property is set to {@code true},
	 * {@link #resolveViewName(String, Locale)} will respond with a view that sets the response status to
	 * {@code 406 Not Acceptable} instead.
	 */
	public void setUseNotAcceptableStatusCode(boolean useNotAcceptableStatusCode) {
		this.useNotAcceptableStatusCode = useNotAcceptableStatusCode;
	}

	/**
	 * Sets the mapping from file extensions to media types.
	 * <p>When this mapping is not set or when an extension is not present, this view resolver
	 * will fall back to using a {@link FileTypeMap} when the Java Action Framework is available.
	 */
	public void setMediaTypes(Map<String, String> mediaTypes) {
		Assert.notNull(mediaTypes, "'mediaTypes' must not be null");
		for (Map.Entry<String, String> entry : mediaTypes.entrySet()) {
			String extension = entry.getKey().toLowerCase(Locale.ENGLISH);
			MediaType mediaType = MediaType.parseMediaType(entry.getValue());
			this.mediaTypes.put(extension, mediaType);
		}
	}

	/**
	 * Sets the default views to use when a more specific view can not be obtained
	 * from the {@link ViewResolver} chain.
	 */
	public void setDefaultViews(List<View> defaultViews) {
		this.defaultViews = defaultViews;
	}

	/**
	 * Sets the default content type.
	 * <p>This content type will be used when file extension, parameter, nor {@code Accept}
	 * header define a content-type, either through being disabled or empty.
	 */
	public void setDefaultContentType(MediaType defaultContentType) {
		this.defaultContentType = defaultContentType;
	}

	/**
	 * Indicates whether to use the Java Activation Framework to map from file extensions to media types.
	 * <p>Default is {@code true}, i.e. the Java Activation Framework is used (if available).
	 */
	public void setUseJaf(boolean useJaf) {
		this.useJaf = useJaf;
	}

	/**
	 * Sets the view resolvers to be wrapped by this view resolver.
	 * <p>If this property is not set, view resolvers will be detected automatically.
	 */
	public void setViewResolvers(List<ViewResolver> viewResolvers) {
		this.viewResolvers = viewResolvers;
	}


	@Override
	protected void initServletContext(ServletContext servletContext) {
		if (this.viewResolvers == null) {
			Map<String, ViewResolver> matchingBeans =
					BeanFactoryUtils.beansOfTypeIncludingAncestors(getApplicationContext(), ViewResolver.class);
			this.viewResolvers = new ArrayList<ViewResolver>(matchingBeans.size());
			for (ViewResolver viewResolver : matchingBeans.values()) {
				if (this != viewResolver) {
					this.viewResolvers.add(viewResolver);
				}
			}
		}
		if (this.viewResolvers.isEmpty()) {
			logger.warn("Did not find any ViewResolvers to delegate to; please configure them using the " +
					"'viewResolvers' property on the ContentNegotiatingViewResolver");
		}
		OrderComparator.sort(this.viewResolvers);
	}

	/**
	 * Determines the list of {@link MediaType} for the given {@link HttpServletRequest}.
	 * <p>The default implementation invokes {@link #getMediaTypeFromFilename(String)}. If the property is
	 * <code>false</code>, or when a media type cannot be determined from the request path, this method will
	 * inspect the {@code Accept} header of the request.
	 * <p>This method can be overriden to provide a different algorithm.
	 * @param request the current servlet request
	 * @return the list of media types requested, if any
	 */
	protected List<MediaType> getMediaTypes(PortletRequest request) {
		if (this.favorParameter) {
			if (request.getParameter(this.parameterName) != null) {
				String parameterValue = request.getParameter(this.parameterName);
				MediaType mediaType = getMediaTypeFromParameter(parameterValue);
				if (mediaType != null) {
					if (logger.isDebugEnabled()) {
						logger.debug("Requested media type is '" + mediaType + "' (based on parameter '" +
								this.parameterName + "'='" + parameterValue + "')");
					}
					return Collections.singletonList(mediaType);
				}
			}
		}
		if (!this.ignoreAcceptHeader) {
			String acceptHeader = request.getProperty(ACCEPT_HEADER);
			if (StringUtils.hasText(acceptHeader)) {
				List<MediaType> mediaTypes = MediaType.parseMediaTypes(acceptHeader);
				MediaType.sortByQualityValue(mediaTypes);
				if (logger.isDebugEnabled()) {
					logger.debug("Requested media types are " + mediaTypes + " (based on Accept header)");
				}
				return mediaTypes;
			}
		}
		if (this.defaultContentType != null) {
			if (logger.isDebugEnabled()) {
				logger.debug("Requested media types is " + this.defaultContentType +
						" (based on defaultContentType property)");
			}
			return Collections.singletonList(this.defaultContentType);
		}
		else {
			return Collections.emptyList();
		}
	}

	/**
	 * Determines the {@link MediaType} for the given filename.
	 * <p>The default implementation will check the {@linkplain #setMediaTypes(Map) media types}
	 * property first for a defined mapping. If not present, and if the Java Activation Framework
	 * can be found on the classpath, it will call {@link FileTypeMap#getContentType(String)}
	 * <p>This method can be overriden to provide a different algorithm.
	 * @param filename the current request file name (i.e. {@code hotels.html})
	 * @return the media type, if any
	 */
	protected MediaType getMediaTypeFromFilename(String filename) {
		String extension = StringUtils.getFilenameExtension(filename);
		if (!StringUtils.hasText(extension)) {
			return null;
		}
		extension = extension.toLowerCase(Locale.ENGLISH);
		MediaType mediaType = this.mediaTypes.get(extension);
		if (mediaType == null && this.useJaf && jafPresent) {
			mediaType = ActivationMediaTypeFactory.getMediaType(filename);
			if (mediaType != null) {
				this.mediaTypes.putIfAbsent(extension, mediaType);
			}
		}
		return mediaType;
	}

	/**
	 * Determines the {@link MediaType} for the given parameter value.
	 * <p>The default implementation will check the {@linkplain #setMediaTypes(Map) media types}
	 * property for a defined mapping.
	 * <p>This method can be overriden to provide a different algorithm.
	 * @param parameterValue the parameter value (i.e. {@code pdf}).
	 * @return the media type, if any
	 */
	protected MediaType getMediaTypeFromParameter(String parameterValue) {
		return this.mediaTypes.get(parameterValue.toLowerCase(Locale.ENGLISH));
	}

	public View resolveViewName(String viewName, Locale locale) throws Exception {
		RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
		Assert.isInstanceOf(PortletRequestAttributes.class, attrs);
		List<MediaType> requestedMediaTypes = getMediaTypes(((PortletRequestAttributes) attrs).getRequest());
		List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
		View bestView = getBestView(candidateViews, requestedMediaTypes);
		if (bestView != null) {
			return bestView;
		}
		else {
			if (this.useNotAcceptableStatusCode) {
				if (logger.isDebugEnabled()) {
					logger.debug("No acceptable view found; returning 406 (Not Acceptable) status code");
				}
				return NOT_ACCEPTABLE_VIEW;
			}
			else {
				if (logger.isDebugEnabled()) {
					logger.debug("No acceptable view found; returning null");
				}
				return null;
			}
		}
	}

	private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
			throws Exception {

		List<View> candidateViews = new ArrayList<View>();
		for (ViewResolver viewResolver : this.viewResolvers) {
			View view = viewResolver.resolveViewName(viewName, locale);
			if (view != null) {
				candidateViews.add(view);
			}
			for (MediaType requestedMediaType : requestedMediaTypes) {
				List<String> extensions = getExtensionsForMediaType(requestedMediaType);
				for (String extension : extensions) {
					String viewNameWithExtension = viewName + "." + extension;
					view = viewResolver.resolveViewName(viewNameWithExtension, locale);
					if (view != null) {
						candidateViews.add(view);
					}
				}

			}
		}
		if (!CollectionUtils.isEmpty(this.defaultViews)) {
			candidateViews.addAll(this.defaultViews);
		}
		return candidateViews;
	}

	private List<String> getExtensionsForMediaType(MediaType requestedMediaType) {
		List<String> result = new ArrayList<String>();
		for (Entry<String, MediaType> entry : mediaTypes.entrySet()) {
			if (requestedMediaType.includes(entry.getValue())) {
				result.add(entry.getKey());
			}
		}
		return result;
	}

	private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes) {
		MediaType bestRequestedMediaType = null;
		View bestView = null;
		for (MediaType requestedMediaType : requestedMediaTypes) {
			for (View candidateView : candidateViews) {
				if (StringUtils.hasText(candidateView.getContentType())) {
					MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType());
					if (requestedMediaType.includes(candidateContentType)) {
						bestRequestedMediaType = requestedMediaType;
						bestView = candidateView;
						break;
					}
				}
			}
			if (bestView != null) {
				if (logger.isDebugEnabled()) {
					logger.debug(
							"Returning [" + bestView + "] based on requested media type '" + bestRequestedMediaType +
									"'");
				}
				break;
			}
		}
		return bestView;

	}


	/**
	 * Inner class to avoid hard-coded JAF dependency.
	 */
	private static class ActivationMediaTypeFactory {

		private static final FileTypeMap fileTypeMap;

		static {
			fileTypeMap = loadFileTypeMapFromContextSupportModule();
		}

		private static FileTypeMap loadFileTypeMapFromContextSupportModule() {
			// see if we can find the extended mime.types from the context-support module
			Resource mappingLocation = new ClassPathResource("org/springframework/mail/javamail/mime.types");
			if (mappingLocation.exists()) {
				if (logger.isTraceEnabled()) {
					logger.trace("Loading Java Activation Framework FileTypeMap from " + mappingLocation);
				}
				InputStream inputStream = null;
				try {
					inputStream = mappingLocation.getInputStream();
					return new MimetypesFileTypeMap(inputStream);
				}
				catch (IOException ex) {
					// ignore
				}
				finally {
					if (inputStream != null) {
						try {
							inputStream.close();
						}
						catch (IOException ex) {
							// ignore
						}
					}
				}
			}
			if (logger.isTraceEnabled()) {
				logger.trace("Loading default Java Activation Framework FileTypeMap");
			}
			return FileTypeMap.getDefaultFileTypeMap();
		}

		public static MediaType getMediaType(String fileName) {
			String mediaType = fileTypeMap.getContentType(fileName);
			return StringUtils.hasText(mediaType) ? MediaType.parseMediaType(mediaType) : null;
		}
	}


	private static final View NOT_ACCEPTABLE_VIEW = new View() {

		public String getContentType() {
			return null;
		}

		public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
				throws Exception {
			response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
		}
	};

}

@spring-projects-issues
Copy link
Collaborator Author

Wendy Cameron commented

I also looked into supporting ResponseBody in the exception resolved and have created the following

package au.com.optus.oneportal.annotation;
/*
 * Copyright 2002-2010 the original author or authors.
 *
 * 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.
 */

import org.springframework.core.ExceptionDepthComparator;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter;
import org.springframework.ui.Model;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.support.WebArgumentResolver;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.portlet.ModelAndView;
import org.springframework.web.portlet.context.PortletWebRequest;
import org.springframework.web.portlet.handler.AbstractHandlerExceptionResolver;
import org.springframework.web.servlet.View;

import javax.portlet.*;
import javax.servlet.ServletException;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.Principal;
import java.util.*;

/**
 * Implementation of the {@link org.springframework.web.portlet.HandlerExceptionResolver} interface that handles
 * exceptions through the {@link org.springframework.web.bind.annotation.ExceptionHandler} annotation.
 * <p/>
 * <p>This exception resolver is enabled by default in the {@link org.springframework.web.portlet.DispatcherPortlet}.
 *
 * @author Arjen Poutsma
 * @author Juergen Hoeller
 * @since 3.0
 */
public class AnnotationMethodHandlerExceptionResolver extends AbstractHandlerExceptionResolver {

    private WebArgumentResolver[] customArgumentResolvers;


    /**
     * Set a custom ArgumentResolvers to use for special method parameter types.
     * <p>Such a custom ArgumentResolver will kick in first, having a chance to resolve
     * an argument value before the standard argument handling kicks in.
     */
    public void setCustomArgumentResolver(WebArgumentResolver argumentResolver) {
        this.customArgumentResolvers = new WebArgumentResolver[]{argumentResolver};
    }

    /**
     * Set one or more custom ArgumentResolvers to use for special method parameter types.
     * <p>Any such custom ArgumentResolver will kick in first, having a chance to resolve
     * an argument value before the standard argument handling kicks in.
     */
    public void setCustomArgumentResolvers(WebArgumentResolver[] argumentResolvers) {
        this.customArgumentResolvers = argumentResolvers;
    }


    private HttpMessageConverter<?>[] messageConverters =
            new HttpMessageConverter[]{new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(),
                    new SourceHttpMessageConverter(), new XmlAwareFormHttpMessageConverter()};

    @Override
    protected ModelAndView doResolveException(
            PortletRequest request, MimeResponse response, Object handler, Exception ex) {

        if (handler != null) {
            Method handlerMethod = findBestExceptionHandlerMethod(handler, ex);
            if (handlerMethod != null) {
                NativeWebRequest webRequest = new PortletWebRequest(request, response);
                try {
                    Object[] args = resolveHandlerArguments(handlerMethod, handler, webRequest, ex);
                    if (logger.isDebugEnabled()) {
                        logger.debug("Invoking request handler method: " + handlerMethod);
                    }
                    Object retVal = doInvokeMethod(handlerMethod, handler, args);
                    return getModelAndView(handlerMethod, retVal, webRequest);
                } catch (Exception invocationEx) {
                    logger.error("Invoking request method resulted in exception : " + handlerMethod, invocationEx);
                }
            }
        }
        return null;
    }


    /**
     * Finds the handler method that matches the thrown exception best.
     *
     * @param handler         the handler object
     * @param thrownException the exception to be handled
     * @return the best matching method; or <code>null</code> if none is found
     */
    private Method findBestExceptionHandlerMethod(Object handler, final Exception thrownException) {
        final Class<?> handlerType = handler.getClass();
        final Class<? extends Throwable> thrownExceptionType = thrownException.getClass();
        final Map<Class<? extends Throwable>, Method> resolverMethods =
                new LinkedHashMap<Class<? extends Throwable>, Method>();

        ReflectionUtils.doWithMethods(handlerType, new ReflectionUtils.MethodCallback() {
            public void doWith(Method method) {
                method = ClassUtils.getMostSpecificMethod(method, handlerType);
                List<Class<? extends Throwable>> handledExceptions = getHandledExceptions(method);
                for (Class<? extends Throwable> handledException : handledExceptions) {
                    if (handledException.isAssignableFrom(thrownExceptionType)) {
                        if (!resolverMethods.containsKey(handledException)) {
                            resolverMethods.put(handledException, method);
                        } else {
                            Method oldMappedMethod = resolverMethods.get(handledException);
                            if (!oldMappedMethod.equals(method)) {
                                throw new IllegalStateException(
                                        "Ambiguous exception handler mapped for " + handledException + "]: {" +
                                                oldMappedMethod + ", " + method + "}.");
                            }
                        }
                    }
                }
            }
        });

        return getBestMatchingMethod(resolverMethods, thrownException);
    }

    /**
     * Returns all the exception classes handled by the given method.
     * <p>Default implementation looks for exceptions in the {@linkplain org.springframework.web.bind.annotation.ExceptionHandler#value() annotation},
     * or - if that annotation element is empty - any exceptions listed in the method parameters if the
     * method is annotated with {@code @ExceptionHandler}.
     *
     * @param method the method
     * @return the handled exceptions
     */
    @SuppressWarnings("unchecked")
    protected List<Class<? extends Throwable>> getHandledExceptions(Method method) {
        List<Class<? extends Throwable>> result = new ArrayList<Class<? extends Throwable>>();
        ExceptionHandler exceptionHandler = AnnotationUtils.findAnnotation(method, ExceptionHandler.class);
        if (exceptionHandler != null) {
            if (!ObjectUtils.isEmpty(exceptionHandler.value())) {
                result.addAll(Arrays.asList(exceptionHandler.value()));
            } else {
                for (Class<?> param : method.getParameterTypes()) {
                    if (Throwable.class.isAssignableFrom(param)) {
                        result.add((Class<? extends Throwable>) param);
                    }
                }
            }
        }
        return result;
    }

    /**
     * Returns the best matching method. Uses the DepthComparator.
     */
    private Method getBestMatchingMethod(
            Map<Class<? extends Throwable>, Method> resolverMethods, Exception thrownException) {

        if (!resolverMethods.isEmpty()) {
            Class<? extends Throwable> closestMatch =
                    ExceptionDepthComparator.findClosestMatch(resolverMethods.keySet(), thrownException);
            return resolverMethods.get(closestMatch);
        } else {
            return null;
        }
    }

    /**
     * Resolves the arguments for the given method. Delegates to {@link #resolveCommonArgument}.
     */
    private Object[] resolveHandlerArguments(Method handlerMethod, Object handler,
                                             NativeWebRequest webRequest, Exception thrownException) throws Exception {

        Class[] paramTypes = handlerMethod.getParameterTypes();
        Object[] args = new Object[paramTypes.length];
        Class<?> handlerType = handler.getClass();
        for (int i = 0; i < args.length; i++) {
            MethodParameter methodParam = new MethodParameter(handlerMethod, i);
            GenericTypeResolver.resolveParameterType(methodParam, handlerType);
            Class paramType = methodParam.getParameterType();
            Object argValue = resolveCommonArgument(methodParam, webRequest, thrownException);
            if (argValue != WebArgumentResolver.UNRESOLVED) {
                args[i] = argValue;
            } else {
                throw new IllegalStateException("Unsupported argument [" + paramType.getName() +
                        "] for @ExceptionHandler method: " + handlerMethod);
            }
        }
        return args;
    }

    /**
     * Resolves common method arguments. Delegates to registered
     * {@link #setCustomArgumentResolver argumentResolvers} first,
     * then checking {@link #resolveStandardArgument}.
     *
     * @param methodParameter the method parameter
     * @param webRequest      the request
     * @param thrownException the exception thrown
     * @return the argument value, or {@link org.springframework.web.bind.support.WebArgumentResolver#UNRESOLVED}
     */
    protected Object resolveCommonArgument(MethodParameter methodParameter, NativeWebRequest webRequest,
                                           Exception thrownException) throws Exception {

        // Invoke custom argument resolvers if present...
        if (this.customArgumentResolvers != null) {
            for (WebArgumentResolver argumentResolver : this.customArgumentResolvers) {
                Object value = argumentResolver.resolveArgument(methodParameter, webRequest);
                if (value != WebArgumentResolver.UNRESOLVED) {
                    return value;
                }
            }
        }

        // Resolution of standard parameter types...
        Class paramType = methodParameter.getParameterType();
        Object value = resolveStandardArgument(paramType, webRequest, thrownException);
        if (value != WebArgumentResolver.UNRESOLVED && !ClassUtils.isAssignableValue(paramType, value)) {
            throw new IllegalStateException("Standard argument type [" + paramType.getName() +
                    "] resolved to incompatible value of type [" + (value != null ? value.getClass() : null) +
                    "]. Consider declaring the argument type in a less specific fashion.");
        }
        return value;
    }

    /**
     * Resolves standard method arguments. The default implementation handles {@link NativeWebRequest},
     * {@link javax.servlet.ServletRequest}, {@link javax.servlet.ServletResponse}, {@link javax.servlet.http.HttpSession}, {@link java.security.Principal},
     * {@link Locale}, request {@link java.io.InputStream}, request {@link java.io.Reader}, response {@link java.io.OutputStream},
     * response {@link java.io.Writer}, and the given {@code thrownException}.
     *
     * @param parameterType   the method parameter type
     * @param webRequest      the request
     * @param thrownException the exception thrown
     * @return the argument value, or {@link org.springframework.web.bind.support.WebArgumentResolver#UNRESOLVED}
     */
    protected Object resolveStandardArgument(Class parameterType, NativeWebRequest webRequest,
                                             Exception thrownException) throws Exception {

        if (parameterType.isInstance(thrownException)) {
            return thrownException;
        } else if (WebRequest.class.isAssignableFrom(parameterType)) {
            return webRequest;
        }

        PortletRequest request = webRequest.getNativeRequest(PortletRequest.class);
        PortletResponse response = webRequest.getNativeResponse(PortletResponse.class);

        if (PortletRequest.class.isAssignableFrom(parameterType)) {
            return request;
        } else if (PortletResponse.class.isAssignableFrom(parameterType)) {
            return response;
        } else if (PortletSession.class.isAssignableFrom(parameterType)) {
            return request.getPortletSession();
        } else if (PortletPreferences.class.isAssignableFrom(parameterType)) {
            return request.getPreferences();
        } else if (PortletMode.class.isAssignableFrom(parameterType)) {
            return request.getPortletMode();
        } else if (WindowState.class.isAssignableFrom(parameterType)) {
            return request.getWindowState();
        } else if (PortalContext.class.isAssignableFrom(parameterType)) {
            return request.getPortalContext();
        } else if (Principal.class.isAssignableFrom(parameterType)) {
            return request.getUserPrincipal();
        } else if (Locale.class.equals(parameterType)) {
            return request.getLocale();
        } else if (InputStream.class.isAssignableFrom(parameterType)) {
            if (!(request instanceof ClientDataRequest)) {
                throw new IllegalStateException("InputStream can only get obtained for Action/ResourceRequest");
            }
            return ((ClientDataRequest) request).getPortletInputStream();
        } else if (Reader.class.isAssignableFrom(parameterType)) {
            if (!(request instanceof ClientDataRequest)) {
                throw new IllegalStateException("Reader can only get obtained for Action/ResourceRequest");
            }
            return ((ClientDataRequest) request).getReader();
        } else if (OutputStream.class.isAssignableFrom(parameterType)) {
            if (!(response instanceof MimeResponse)) {
                throw new IllegalStateException("OutputStream can only get obtained for Render/ResourceResponse");
            }
            return ((MimeResponse) response).getPortletOutputStream();
        } else if (Writer.class.isAssignableFrom(parameterType)) {
            if (!(response instanceof MimeResponse)) {
                throw new IllegalStateException("Writer can only get obtained for Render/ResourceResponse");
            }
            return ((MimeResponse) response).getWriter();
        } else if (Event.class.equals(parameterType)) {
            if (!(request instanceof EventRequest)) {
                throw new IllegalStateException("Event can only get obtained from EventRequest");
            }
            return ((EventRequest) request).getEvent();
        } else {
            return WebArgumentResolver.UNRESOLVED;
        }
    }

    private Object doInvokeMethod(Method method, Object target, Object[] args) throws Exception {
        ReflectionUtils.makeAccessible(method);
        try {
            return method.invoke(target, args);
        } catch (InvocationTargetException ex) {
            ReflectionUtils.rethrowException(ex.getTargetException());
        }
        throw new IllegalStateException("Should never get here");
    }

    @SuppressWarnings("unchecked")
    private ModelAndView getModelAndView(Method handlerMethod, Object returnValue, NativeWebRequest webRequest) throws IOException, ServletException {
        PortletResponse response = webRequest.getNativeResponse(PortletResponse.class);
        ResponseStatus responseStatusAnn = AnnotationUtils.findAnnotation(handlerMethod, ResponseStatus.class);
        if (responseStatusAnn != null) {
            HttpStatus responseStatus = responseStatusAnn.value();
            String reason = responseStatusAnn.reason();
            if (!StringUtils.hasText(reason)) {
                //I have never used this branch
                response.setProperty(ResourceResponse.HTTP_STATUS_CODE, responseStatus.toString());
            } //Sending an error redirect doesn't make sense in a portlet.
        }

        if (returnValue != null && AnnotationUtils.findAnnotation(handlerMethod, ResponseBody.class) != null) {
            return handleResponseBody(returnValue, webRequest);
        }

        if (returnValue instanceof ModelAndView) {
            return (ModelAndView) returnValue;
        } else if (returnValue instanceof Model) {
            return new ModelAndView().addAllObjects(((Model) returnValue).asMap());
        } else if (returnValue instanceof Map) {
            return new ModelAndView().addAllObjects((Map) returnValue);
        } else if (returnValue instanceof View) {
            return new ModelAndView(returnValue);
        } else if (returnValue instanceof String) {
            return new ModelAndView((String) returnValue);
        } else if (returnValue == null) {
            return new ModelAndView();
        } else {
            throw new IllegalArgumentException("Invalid handler method return value: " + returnValue);
        }
    }

    @SuppressWarnings("unchecked")
    private ModelAndView handleResponseBody(Object returnValue, NativeWebRequest webRequest)
            throws ServletException, IOException {
        PortletResponse response = webRequest.getNativeResponse(PortletResponse.class);
        PortletRequest request = webRequest.getNativeResponse(PortletRequest.class);
        HttpInputMessage inputMessage = new PortletServerHttpRequest(request);
        List<MediaType> acceptedMediaTypes = inputMessage.getHeaders().getAccept();
        if (acceptedMediaTypes.isEmpty()) {
            acceptedMediaTypes = Collections.singletonList(MediaType.ALL);
        }
        MediaType.sortByQualityValue(acceptedMediaTypes);
        HttpOutputMessage outputMessage = new PortletServerHttpResponse(response);
        Class<?> returnValueType = returnValue.getClass();
        if (this.messageConverters != null) {
            for (MediaType acceptedMediaType : acceptedMediaTypes) {
                for (HttpMessageConverter messageConverter : this.messageConverters) {
                    if (messageConverter.canWrite(returnValueType, acceptedMediaType)) {
                        messageConverter.write(returnValue, acceptedMediaType, outputMessage);
                        return new ModelAndView();
                    }
                }
            }
        }
        if (logger.isWarnEnabled()) {
            logger.warn("Could not find HttpMessageConverter that supports return type [" + returnValueType + "] and " +
                    acceptedMediaTypes);
        }
        return null;
    }

    public void setMessageConverters(HttpMessageConverter<?>[] messageConverters) {
        this.messageConverters = messageConverters;
    }

}

I not the use with the ResponseStatus annotation it doesnt make sense to send an error redirect from a portlet so have lopped that code off.

@spring-projects-issues
Copy link
Collaborator Author

Wendy Cameron commented

I also notice in order to support @ResponseStatus the SimpleMappingExceptionResolver will need to have the properties and code relating to

private Integer defaultStatusCode;

private Map<String, Integer> statusCodes = new HashMap<String, Integer>();

Added and possibly need to create a portlet version of ResponseStatusExceptionResolver.

@spring-projects-issues
Copy link
Collaborator Author

Wendy Cameron commented

Tested out the following code:

package au.com.optus.oneportal.handler;

/*
 * Copyright 2002-2008 the original author or authors.
 *
 * 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.
 */

import org.springframework.web.portlet.ModelAndView;
import org.springframework.web.portlet.handler.AbstractHandlerExceptionResolver;
import org.springframework.web.util.WebUtils;

import javax.portlet.MimeResponse;
import javax.portlet.PortletRequest;
import javax.portlet.PortletResponse;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import static javax.portlet.ResourceResponse.HTTP_STATUS_CODE;

/**
 * {@link org.springframework.web.portlet.HandlerExceptionResolver} implementation
 * that allows for mapping exception class names to view names, either for a
 * set of given handlers or for all handlers in the DispatcherPortlet.
 *
 * <p>Error views are analogous to error page JSPs, but can be used with any
 * kind of exception including any checked one, with fine-granular mappings for
 * specific handlers.
 *
 * @author Juergen Hoeller
 * @author John A. Lewis
 * @author Arjen Poutsma
 * @since 2.0
 */
public class SimpleMappingExceptionResolver extends AbstractHandlerExceptionResolver {

	/**
	 * The default name of the exception attribute: "exception".
	 */
	public static final String DEFAULT_EXCEPTION_ATTRIBUTE = "exception";

	private Properties exceptionMappings;

	private String defaultErrorView;

    private Integer defaultStatusCode;

	private Map<String, Integer> statusCodes = new HashMap<String, Integer>();

	private String exceptionAttribute = DEFAULT_EXCEPTION_ATTRIBUTE;

	/**
	 * Set the mappings between exception class names and error view names.
	 * The exception class name can be a substring, with no wildcard support
	 * at present. A value of "PortletException" would match
	 * <code>javax.portet.PortletException</code> and subclasses, for example.
	 * <p><b>NB:</b> Consider carefully how specific the pattern is, and whether
	 * to include package information (which isn't mandatory). For example,
	 * "Exception" will match nearly anything, and will probably hide other rules.
	 * "java.lang.Exception" would be correct if "Exception" was meant to define
	 * a rule for all checked exceptions. With more unusual exception names such
	 * as "BaseBusinessException" there's no need to use a FQN.
	 * <p>Follows the same matching algorithm as RuleBasedTransactionAttribute
	 * and RollbackRuleAttribute.
	 * @param mappings exception patterns (can also be fully qualified class names)
	 * as keys, and error view names as values
	 * @see org.springframework.transaction.interceptor.RuleBasedTransactionAttribute
	 * @see org.springframework.transaction.interceptor.RollbackRuleAttribute
	 */
	public void setExceptionMappings(Properties mappings) {
		this.exceptionMappings = mappings;
	}

	/**
	 * Set the name of the default error view.
	 * This view will be returned if no specific mapping was found.
	 * <p>Default is none.
	 */
	public void setDefaultErrorView(String defaultErrorView) {
		this.defaultErrorView = defaultErrorView;
	}

    /**
	 * Set the HTTP status code that this exception resolver will apply for a given resolved error view. Keys are
	 * view names; values are status codes.
	 * <p>Note that this error code will only get applied in case of a top-level request. It will not be set for an include
	 * request, since the HTTP status cannot be modified from within an include.
	 * <p>If not specified, the default status code will be applied.
	 * @see #setDefaultStatusCode(int)
	 */
	public void setStatusCodes(Properties statusCodes) {
		for (Enumeration enumeration = statusCodes.propertyNames(); enumeration.hasMoreElements();) {
			String viewName = (String) enumeration.nextElement();
			Integer statusCode = new Integer(statusCodes.getProperty(viewName));
			this.statusCodes.put(viewName, statusCode);
		}
	}

	/**
	 * Set the default HTTP status code that this exception resolver will apply if it resolves an error view and if there
	 * is no status code mapping defined.
	 * <p>Note that this error code will only get applied in case of a top-level request. It will not be set for an
	 * include request, since the HTTP status cannot be modified from within an include.
	 * <p>If not specified, no status code will be applied, either leaving this to the controller or view, or keeping
	 * the servlet engine's default of 200 (OK).
	 * @param defaultStatusCode HTTP status code value, for example 500
	 * ({@link javax.servlet.http.HttpServletResponse#SC_INTERNAL_SERVER_ERROR}) or 404 ({@link javax.servlet.http.HttpServletResponse#SC_NOT_FOUND})
	 * @see #setStatusCodes(Properties)
	 */
	public void setDefaultStatusCode(int defaultStatusCode) {
		this.defaultStatusCode = defaultStatusCode;
	}

	/**
	 * Set the name of the model attribute as which the exception should
	 * be exposed. Default is "exception".
	 * @see #DEFAULT_EXCEPTION_ATTRIBUTE
	 */
	public void setExceptionAttribute(String exceptionAttribute) {
		this.exceptionAttribute = exceptionAttribute;
	}

	/**
	 * Actually resolve the given exception that got thrown during on handler execution,
	 * returning a ModelAndView that represents a specific error page if appropriate.
	 * @param request current portlet request
	 * @param response current portlet response
	 * @param handler the executed handler, or null if none chosen at the time of
	 * the exception (for example, if multipart resolution failed)
	 * @param ex the exception that got thrown during handler execution
	 * @return a corresponding ModelAndView to forward to, or null for default processing
	 */
	@Override
	protected ModelAndView doResolveException(
			PortletRequest request, MimeResponse response, Object handler, Exception ex) {

		// Log exception, both at debug log level and at warn level, if desired.
		if (logger.isDebugEnabled()) {
			logger.debug("Resolving exception from handler [" + handler + "]: " + ex);
		}
		logException(ex, request);

		// Expose ModelAndView for chosen error view.
		String viewName = determineViewName(ex, request);
		if (viewName != null) {
            Integer statusCode = determineStatusCode(request, viewName);
			if (statusCode != null) {
				applyStatusCodeIfPossible(request, response, statusCode);
			}
			return getModelAndView(viewName, ex, request);
		}
		else {
			return null;
		}
	}

	/**
	 * Determine the view name for the given exception, searching the
	 * {@link #setExceptionMappings "exceptionMappings"}, using the
	 * {@link #setDefaultErrorView "defaultErrorView"} as fallback.
	 * @param ex the exception that got thrown during handler execution
	 * @param request current portlet request (useful for obtaining metadata)
	 * @return the resolved view name, or <code>null</code> if none found
	 */
	protected String determineViewName(Exception ex, PortletRequest request) {
		String viewName = null;
		// Check for specific exception mappings.
		if (this.exceptionMappings != null) {
			viewName = findMatchingViewName(this.exceptionMappings, ex);
		}
		// Return default error view else, if defined.
		if (viewName == null && this.defaultErrorView != null) {
			if (logger.isDebugEnabled()) {
				logger.debug("Resolving to default view '" + this.defaultErrorView +
						"' for exception of type [" + ex.getClass().getName() + "]");
			}
			viewName = this.defaultErrorView;
		}
		return viewName;
	}

	/**
	 * Find a matching view name in the given exception mappings
	 * @param exceptionMappings mappings between exception class names and error view names
	 * @param ex the exception that got thrown during handler execution
	 * @return the view name, or <code>null</code> if none found
	 * @see #setExceptionMappings
	 */
	protected String findMatchingViewName(Properties exceptionMappings, Exception ex) {
		String viewName = null;
		String dominantMapping = null;
		int deepest = Integer.MAX_VALUE;
		for (Enumeration names = exceptionMappings.propertyNames(); names.hasMoreElements();) {
			String exceptionMapping = (String) names.nextElement();
			int depth = getDepth(exceptionMapping, ex);
			if (depth >= 0 && depth < deepest) {
				deepest = depth;
				dominantMapping = exceptionMapping;
				viewName = exceptionMappings.getProperty(exceptionMapping);
			}
		}
		if (viewName != null && logger.isDebugEnabled()) {
			logger.debug("Resolving to view '" + viewName + "' for exception of type [" + ex.getClass().getName() +
					"], based on exception mapping [" + dominantMapping + "]");
		}
		return viewName;
	}

	/**
	 * Return the depth to the superclass matching.
	 * <p>0 means ex matches exactly. Returns -1 if there's no match.
	 * Otherwise, returns depth. Lowest depth wins.
	 * <p>Follows the same algorithm as
	 * {@link org.springframework.transaction.interceptor.RollbackRuleAttribute}.
	 */
	protected int getDepth(String exceptionMapping, Exception ex) {
		return getDepth(exceptionMapping, ex.getClass(), 0);
	}

	private int getDepth(String exceptionMapping, Class exceptionClass, int depth) {
		if (exceptionClass.getName().contains(exceptionMapping)) {
			// Found it!
			return depth;
		}
		// If we've gone as far as we can go and haven't found it...
		if (exceptionClass.equals(Throwable.class)) {
			return -1;
		}
		return getDepth(exceptionMapping, exceptionClass.getSuperclass(), depth + 1);
	}

    /**
	 * Determine the HTTP status code to apply for the given error view.
	 * <p>The default implementation returns the status code for the given view name (specified through the
	 * {@link #setStatusCodes(Properties) statusCodes} property), or falls back to the
	 * {@link #setDefaultStatusCode defaultStatusCode} if there is no match.
	 * <p>Override this in a custom subclass to customize this behavior.
	 *
     * @param request current HTTP request
     * @param viewName the name of the error view
     * @return the HTTP status code to use, or <code>null</code> for the servlet container's default
	 * (200 in case of a standard error view)
	 * @see #setDefaultStatusCode
	 * @see #applyStatusCodeIfPossible
	 */
	protected Integer determineStatusCode(PortletRequest request, String viewName) {
		if (this.statusCodes.containsKey(viewName)) {
			return this.statusCodes.get(viewName);
		}
		return this.defaultStatusCode;
	}

	/**
	 * Apply the specified HTTP status code to the given response, if possible (that is,
	 * if not executing within an include request).
	 * @param request current HTTP request
	 * @param response current HTTP response
	 * @param statusCode the status code to apply
	 * @see #determineStatusCode
	 * @see #setDefaultStatusCode
	 * @see javax.servlet.http.HttpServletResponse#setStatus
	 */
	protected void applyStatusCodeIfPossible(PortletRequest request, PortletResponse response, int statusCode) {
        if (logger.isDebugEnabled()) {
            logger.debug("Applying HTTP status code " + statusCode);
        }
        response.setProperty(HTTP_STATUS_CODE, String.valueOf(statusCode));
        request.setAttribute(WebUtils.ERROR_STATUS_CODE_ATTRIBUTE, statusCode);
	}


	/**
	 * Return a ModelAndView for the given request, view name and exception.
	 * Default implementation delegates to <code>getModelAndView(viewName, ex)</code>.
	 * @param viewName the name of the error view
	 * @param ex the exception that got thrown during handler execution
	 * @param request current portlet request (useful for obtaining metadata)
	 * @return the ModelAndView instance
	 * @see #getModelAndView(String, Exception)
	 */
	protected ModelAndView getModelAndView(String viewName, Exception ex, PortletRequest request) {
		return getModelAndView(viewName, ex);
	}

	/**
	 * Return a ModelAndView for the given view name and exception.
	 * Default implementation adds the specified exception attribute.
	 * Can be overridden in subclasses.
	 * @param viewName the name of the error view
	 * @param ex the exception that got thrown during handler execution
	 * @return the ModelAndView instance
	 * @see #setExceptionAttribute
	 */
	protected ModelAndView getModelAndView(String viewName, Exception ex) {
		ModelAndView mv = new ModelAndView(viewName);
		if (this.exceptionAttribute != null) {
			if (logger.isDebugEnabled()) {
				logger.debug("Exposing Exception as model attribute '" + this.exceptionAttribute + "'");
			}
			mv.addObject(this.exceptionAttribute, ex);
		}
		return mv;
	}

}

This appears to work adding a header
portlet.http-status-code:500

I wonder if the solution for replacing sendError(statusCode, reason) is to create a header
portlet.http-status-code-reason: (Place Reason Here)

And set that instead.

@spring-projects-issues
Copy link
Collaborator Author

Michał Mela commented

Was there any progress on the subject? I am especially interested in any development in the areas of

  • @RequestBody handler method's annotation Wendy mentioned
  • MessageConverters property in portlet's version of AnnotationMethodHandlerAdapter - it would be nice to at least know why is this absent, because I couldn't find anything on that particular matter in documentation

@spring-projects-issues
Copy link
Collaborator Author

Eugene Petruhin commented

I can't believe it's been open for 2.5 years and still no implementation is available.
Spring's version is now 3.2 and servlets enjoy the benefits of @RequestBody and @Valid being used together but portlets can't even do @RequestBody for resource requests.

Please consider adding this functionality to the next release.

@spring-projects-issues
Copy link
Collaborator Author

Abdulrhman Nabih ALKoptan commented

Any Update ?

@spring-projects-issues
Copy link
Collaborator Author

Patrick Bergner commented

Happy new year, portlet people! Will we see the feature in the currently set 4.2 fix version or will it be bumped further and further?

@spring-projects-issues
Copy link
Collaborator Author

Ram Chinnakuzhandai commented

Can we have this feature to be part of upcoming release + 1, at least. This has been sitting in the rack for a long time, and would be a huge help for the portlet community.

@spring-projects-issues
Copy link
Collaborator Author

Sébastien Deleuze commented

Hi everybody,

While working on this issue, I figured out that rendering JSON data from @ResourceMapping is already possible out of the box (tested with Spring 3.2.6.RELEASE and 4.2.0.BUILD-SNAPSHOT) using MappingJackson2JsonView:

@ResourceMapping("json")
public ModelAndView listBooksJson() {
     ModelAndView mav = new ModelAndView();
     mav.addObject("books", bookService.getAllBooks());
     mav.setView(new MappingJackson2JsonView());
     return mav;
}

You can test this sample application and have a look to this commit that introduced such functionality in the sample application.

While this is not @ResponseBody support, the MappingJackson2JsonView alternative is really easy to use with @ResourceMapping and seems to me a reasonable solution for the need expressed here. Could anybody interested give it a try and send us some feedbacks to confirm it works as expected (or to report any issue)?

Thanks in advance for your feedbacks.

@spring-projects-issues
Copy link
Collaborator Author

Sébastien Deleuze commented

Since there are no feedback more than one week after my last comment, I resolve this issue as "Works as designed" since it is already possible to return JSON in Portlets using ModelAndView and MappingJackson2JsonView.

Supporting also ResponseBody would require a lot of changes, with little added value so I think the current solution is a reasonable one.

@spring-projects-issues spring-projects-issues added status: declined A suggestion or change that we don't feel we should currently apply type: enhancement A general enhancement has: votes-jira Issues migrated from JIRA with more than 10 votes at the time of import in: web Issues in web modules (web, webmvc, webflux, websocket) labels Jan 11, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
has: votes-jira Issues migrated from JIRA with more than 10 votes at the time of import in: web Issues in web modules (web, webmvc, webflux, websocket) status: declined A suggestion or change that we don't feel we should currently apply type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

2 participants