You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by se...@apache.org on 2017/10/26 15:45:54 UTC

[cxf] branch 3.1.x-fixes updated: Adding a Spring View-aware MessageBodyWriter, patch from Adrian Gonzalez applied, This closes #328

This is an automated email from the ASF dual-hosted git repository.

sergeyb pushed a commit to branch 3.1.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git


The following commit(s) were added to refs/heads/3.1.x-fixes by this push:
     new ab960da  Adding a Spring View-aware MessageBodyWriter, patch from Adrian Gonzalez applied, This closes #328
ab960da is described below

commit ab960dafe6a6432109b8a2aa92f73bd6ad6bc85f
Author: Sergey Beryozkin <sb...@gmail.com>
AuthorDate: Thu Oct 26 16:41:41 2017 +0100

    Adding a Spring View-aware MessageBodyWriter, patch from Adrian Gonzalez applied, This closes #328
---
 rt/frontend/jaxrs/pom.xml                          |   6 +
 .../apache/cxf/jaxrs/spring/Messages.properties    |  21 ++
 .../jaxrs/spring/SpringViewResolverProvider.java   | 395 ++++++++++++++++++++
 .../spring/SpringViewResolverProviderTest.java     | 414 +++++++++++++++++++++
 4 files changed, 836 insertions(+)

diff --git a/rt/frontend/jaxrs/pom.xml b/rt/frontend/jaxrs/pom.xml
index 1b4045d..61d243a 100644
--- a/rt/frontend/jaxrs/pom.xml
+++ b/rt/frontend/jaxrs/pom.xml
@@ -92,6 +92,12 @@
             <optional>true</optional>
         </dependency>
         <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-webmvc</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
             <groupId>javax.ws.rs</groupId>
             <artifactId>javax.ws.rs-api</artifactId>
         </dependency>
diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/spring/Messages.properties b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/spring/Messages.properties
new file mode 100644
index 0000000..7dd2773
--- /dev/null
+++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/spring/Messages.properties
@@ -0,0 +1,21 @@
+#
+#
+#    Licensed to the Apache Software Foundation (ASF) under one
+#    or more contributor license agreements. See the NOTICE file
+#    distributed with this work for additional information
+#    regarding copyright ownership. The ASF licenses this file
+#    to you 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.
+#
+#
+RESPONSE_REDIRECTED_TO=Setting an instance of \"{0}\" as HttpServletRequest attribute \"{1}\" and redirecting the response to \"{2}\".
diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/spring/SpringViewResolverProvider.java b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/spring/SpringViewResolverProvider.java
new file mode 100644
index 0000000..8fa38cd
--- /dev/null
+++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/spring/SpringViewResolverProvider.java
@@ -0,0 +1,395 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.cxf.jaxrs.spring;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.RequestDispatcher;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+
+import org.apache.cxf.common.i18n.BundleUtils;
+import org.apache.cxf.common.logging.LogUtils;
+import org.apache.cxf.common.util.StringUtils;
+import org.apache.cxf.jaxrs.ext.MessageContext;
+import org.apache.cxf.jaxrs.provider.AbstractConfigurableProvider;
+import org.apache.cxf.jaxrs.utils.ExceptionUtils;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.phase.PhaseInterceptorChain;
+import org.apache.cxf.transport.http.AbstractHTTPDestination;
+import org.springframework.web.servlet.LocaleResolver;
+import org.springframework.web.servlet.View;
+import org.springframework.web.servlet.ViewResolver;
+
+/**
+ * CXF view provider that delegates view rendering to Spring MVC Views.
+ *
+ * Sample usage in a spring application:
+ * <pre>
+ @Bean
+ public SpringViewResolverProvider springViewProvider(ViewResolver viewResolver) {
+     SpringViewResolverProvider viewProvider = new SpringViewResolverProvider(viewResolver,
+            new AcceptHeaderLocaleResolver());
+     viewProvider.setUseClassNames(true);
+     viewProvider.setBeanName("model");
+     viewProvider.setResourcePaths(Collections.singletonMap("/remove", "registeredClients"));
+     return viewProvider;
+ }
+ * </pre>
+ */
+@Produces("text/html")
+@Provider
+public class SpringViewResolverProvider extends AbstractConfigurableProvider implements MessageBodyWriter<Object> {
+
+    private static final ResourceBundle BUNDLE = BundleUtils.getBundle(SpringViewResolverProvider.class);
+
+    private static final Logger LOG = LogUtils.getL7dLogger(SpringViewResolverProvider.class);
+
+    private static final String MESSAGE_RESOURCE_PATH_PROPERTY = "redirect.resource.path";
+
+    private static final String DEFAULT_RESOURCE_EXTENSION = "";
+
+    private static final String DEFAULT_LOCATION_PREFIX = "";
+
+    private final ViewResolver viewResolver;
+
+    private String resourcePath;
+
+    private Map<String, String> resourcePaths = Collections.emptyMap();
+
+    private Map<String, String> classResources = Collections.emptyMap();
+
+    private Map<? extends Enum<?>, String> enumResources = Collections.emptyMap();
+
+    private boolean useClassNames;
+
+    private Map<String, String> beanNames = Collections.emptyMap();
+
+    private String beanName;
+
+    private boolean logRedirects;
+
+    private boolean strictPathCheck;
+
+    private String locationPrefix;
+
+    private String resourceExtension;
+
+    private MessageContext mc;
+
+    private LocaleResolver localeResolver;
+
+    private String errorView = "/error";
+
+    public SpringViewResolverProvider(ViewResolver viewResolver, LocaleResolver localeResolver) {
+        if (viewResolver == null) {
+            throw new IllegalArgumentException("Argument viewResolver is required");
+        }
+        if (localeResolver == null) {
+            throw new IllegalArgumentException("Argument localeResolver is required");
+        }
+        this.viewResolver = viewResolver;
+        this.localeResolver = localeResolver;
+    }
+
+    @Context
+    public void setMessageContext(MessageContext context) {
+        this.mc = context;
+    }
+
+    public void setStrictPathCheck(boolean use) {
+        strictPathCheck = use;
+    }
+
+    public void setUseClassNames(boolean use) {
+        useClassNames = use;
+    }
+
+    public long getSize(Object t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mt) {
+        return -1;
+    }
+
+    private String getViewName(Class<?> type) {
+        String viewName = doGetClassResourceName(type);
+        if (viewName == null) {
+            for (Class<?> in : type.getInterfaces()) {
+                viewName = doGetClassResourceName(in);
+                if (viewName != null) {
+                    break;
+                }
+            }
+        }
+        return viewName;
+    }
+
+    private Locale getLocale() {
+        return localeResolver.resolveLocale(mc.getHttpServletRequest());
+    }
+
+    private String doGetClassResourceName(Class<?> type) {
+        String simpleName = StringUtils.uncapitalize(type.getSimpleName());
+        String thePrefix = locationPrefix == null ? DEFAULT_LOCATION_PREFIX : locationPrefix;
+        String theExtension = resourceExtension == null ? DEFAULT_RESOURCE_EXTENSION : resourceExtension;
+        String viewName = thePrefix + simpleName + theExtension;
+        View view = resolveView(viewName);
+        return view != null ? viewName : null;
+    }
+
+    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mt) {
+
+        if (useClassNames && getViewName(type) != null) {
+            return true;
+        }
+        if (resourcePath != null || classResourceSupported(type)) {
+            return true;
+        }
+        if (!resourcePaths.isEmpty()) {
+            String path = getRequestPath();
+            for (String requestPath : resourcePaths.keySet()) {
+                boolean result = strictPathCheck ? path.endsWith(requestPath) : path.contains(requestPath);
+                if (result) {
+                    return true;
+                }
+            }
+        }
+        return mc != null && mc.get(MESSAGE_RESOURCE_PATH_PROPERTY) != null;
+    }
+
+    private boolean classResourceSupported(Class<?> type) {
+        String typeName = type.getName();
+        if (type.isEnum()) {
+            for (Object o : enumResources.keySet()) {
+                if (o.getClass().getName().equals(typeName)) {
+                    return true;
+                }
+            }
+            for (String name : classResources.keySet()) {
+                if (name.startsWith(typeName)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        return classResources.containsKey(typeName);
+    }
+
+    public void writeTo(Object o, Class<?> clazz, Type genericType, Annotation[] annotations, MediaType type,
+            MultivaluedMap<String, Object> headers, OutputStream os) throws IOException {
+
+        View view = getView(clazz, o);
+        String attributeName = getBeanName(o);
+        Map<String, Object> model = Collections.singletonMap(attributeName, o);
+
+        try {
+            mc.put(AbstractHTTPDestination.REQUEST_REDIRECTED, Boolean.TRUE);
+            logRedirection(view, attributeName, o);
+            view.render(model, mc.getHttpServletRequest(), mc.getHttpServletResponse());
+        } catch (Throwable ex) {
+            handleViewRenderingException(view, ex);
+        }
+    }
+
+    /**
+     * By default we'll try to forward to Spring error handler.
+     *
+     * If no such handler has been set, or if there is an error during error handling,
+     * we throw an error and let CXF handle the internal error.
+     *
+     * @param view view that produced the rendering error
+     * @param exception rendering error
+     */
+    private void handleViewRenderingException(View view, Throwable exception) {
+        LOG.log(Level.WARNING, String.format("Error forwarding to '%s': %s", view, exception.getMessage()), exception);
+        if (errorView != null) {
+            mc.getHttpServletRequest().setAttribute(RequestDispatcher.ERROR_EXCEPTION, exception);
+            mc.getHttpServletRequest().setAttribute(RequestDispatcher.ERROR_STATUS_CODE, 500);
+            mc.getHttpServletRequest().setAttribute(RequestDispatcher.ERROR_MESSAGE, exception.getMessage());
+            try {
+                mc.getServletContext().getRequestDispatcher(errorView).forward(mc.getHttpServletRequest(),
+                        mc.getHttpServletResponse());
+            } catch (Exception e) {
+                LOG.log(Level.SEVERE, String.format("Error forwarding to error page '%s': %s",
+                        errorView, e.toString()),
+                        e);
+                handleInternalViewRenderingException(exception);
+            }
+        } else {
+            handleInternalViewRenderingException(exception);
+        }
+    }
+
+    private void handleInternalViewRenderingException(Throwable exception) {
+        mc.put(AbstractHTTPDestination.REQUEST_REDIRECTED, Boolean.FALSE);
+        throw ExceptionUtils.toInternalServerErrorException(exception, null);
+    }
+
+    private void logRedirection(View view, String attributeName, Object o) {
+        Level level = logRedirects ? Level.INFO : Level.FINE;
+        if (LOG.isLoggable(level)) {
+            String message = new org.apache.cxf.common.i18n.Message("RESPONSE_REDIRECTED_TO",
+                    BUNDLE, o.getClass().getName(),
+                    attributeName, view).toString();
+            LOG.log(level, message);
+        }
+    }
+
+    View getView(Class<?> cls, Object o) {
+        String currentResourcePath = getPathFromMessageContext();
+        if (currentResourcePath != null) {
+            return resolveView(currentResourcePath);
+        }
+
+        if (!resourcePaths.isEmpty()) {
+
+            String path = getRequestPath();
+            for (Map.Entry<String, String> entry : resourcePaths.entrySet()) {
+                if (path.endsWith(entry.getKey())) {
+                    return resolveView(entry.getValue());
+                }
+            }
+        }
+        if (!enumResources.isEmpty() || !classResources.isEmpty()) {
+            String name = cls.getName();
+            if (cls.isEnum()) {
+                String enumResource = enumResources.get(o);
+                if (enumResource != null) {
+                    return resolveView(enumResource);
+                }
+                name += "." + o.toString();
+            }
+
+            String clsResourcePath = classResources.get(name);
+            if (clsResourcePath != null) {
+                return resolveView(clsResourcePath);
+            }
+        }
+
+        if (useClassNames) {
+            return resolveView(getViewName(cls));
+        }
+
+        return resolveView(resourcePath);
+    }
+
+    private View resolveView(String viewName) {
+        try {
+            return viewResolver.resolveViewName(viewName, getLocale());
+        } catch (Exception ex) {
+            LOG.warning(ExceptionUtils.getStackTrace(ex));
+            throw ExceptionUtils.toInternalServerErrorException(ex, null);
+        }
+    }
+
+    private String getPathFromMessageContext() {
+        if (mc != null) {
+            Object resourcePathProp = mc.get(MESSAGE_RESOURCE_PATH_PROPERTY);
+            if (resourcePathProp != null) {
+                StringBuilder sb = new StringBuilder();
+                if (locationPrefix != null) {
+                    sb.append(locationPrefix);
+                }
+                sb.append(resourcePathProp.toString());
+                if (resourceExtension != null) {
+                    sb.append(resourceExtension);
+                }
+                return sb.toString();
+            }
+        }
+        return null;
+    }
+
+    private String getRequestPath() {
+        Message inMessage = PhaseInterceptorChain.getCurrentMessage().getExchange().getInMessage();
+        return (String) inMessage.get(Message.REQUEST_URI);
+    }
+
+    public void setResourcePath(String resourcePath) {
+        this.resourcePath = resourcePath;
+    }
+
+    public void setBeanNames(Map<String, String> beanNames) {
+        this.beanNames = beanNames;
+    }
+
+    public void setBeanName(String beanName) {
+        this.beanName = beanName;
+    }
+
+    public void setLogRedirects(String value) {
+        this.logRedirects = Boolean.valueOf(value);
+    }
+
+    protected String getBeanName(Object bean) {
+        if (beanName != null) {
+            return beanName;
+        }
+        String name = beanNames.get(bean.getClass().getName());
+        if (name != null) {
+            return name;
+        }
+        Class<?> resourceClass = bean.getClass();
+        if (useClassNames && doGetClassResourceName(resourceClass) == null) {
+            for (Class<?> cls : bean.getClass().getInterfaces()) {
+                if (doGetClassResourceName(cls) != null) {
+                    resourceClass = cls;
+                    break;
+                }
+            }
+        }
+
+        return resourceClass.getSimpleName().toLowerCase();
+    }
+
+    public void setResourcePaths(Map<String, String> resourcePaths) {
+        this.resourcePaths = resourcePaths;
+    }
+
+    public void setClassResources(Map<String, String> resources) {
+        this.classResources = resources;
+    }
+
+    public void setEnumResources(Map<? extends Enum<?>, String> enumResources) {
+        this.enumResources = enumResources;
+    }
+
+    public void setLocationPrefix(String locationPrefix) {
+        this.locationPrefix = locationPrefix;
+    }
+
+    public void setResourceExtension(String resourceExtension) {
+        this.resourceExtension = resourceExtension;
+    }
+
+    public void setErrorView(String errorView) {
+        this.errorView = errorView;
+    }
+}
\ No newline at end of file
diff --git a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/spring/SpringViewResolverProviderTest.java b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/spring/SpringViewResolverProviderTest.java
new file mode 100644
index 0000000..1c830ba
--- /dev/null
+++ b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/spring/SpringViewResolverProviderTest.java
@@ -0,0 +1,414 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.cxf.jaxrs.spring;
+
+import java.io.Closeable;
+import java.lang.annotation.Annotation;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.InternalServerErrorException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
+
+import org.apache.cxf.Bus;
+import org.apache.cxf.binding.Binding;
+import org.apache.cxf.endpoint.Endpoint;
+import org.apache.cxf.feature.Feature;
+import org.apache.cxf.interceptor.AbstractAttributedInterceptorProvider;
+import org.apache.cxf.interceptor.Interceptor;
+import org.apache.cxf.jaxrs.ext.MessageContextImpl;
+import org.apache.cxf.jaxrs.provider.ServerProviderFactory;
+import org.apache.cxf.message.ExchangeImpl;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.message.MessageImpl;
+import org.apache.cxf.service.Service;
+import org.apache.cxf.service.model.BindingInfo;
+import org.apache.cxf.service.model.EndpointInfo;
+import org.apache.cxf.transport.MessageObserver;
+import org.apache.cxf.transport.http.AbstractHTTPDestination;
+import org.easymock.EasyMockRule;
+import org.easymock.EasyMockSupport;
+import org.easymock.Mock;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.web.servlet.LocaleResolver;
+import org.springframework.web.servlet.View;
+import org.springframework.web.servlet.ViewResolver;
+import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
+import org.springframework.web.servlet.view.BeanNameViewResolver;
+
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class SpringViewResolverProviderTest extends EasyMockSupport {
+
+    @Rule
+    public EasyMockRule rule = new EasyMockRule(this);
+
+    @Mock
+    private ViewResolver viewResolverMock;
+
+    @Mock
+    private LocaleResolver localeResolverMock;
+
+    @Mock
+    private View viewMock;
+
+    @Mock
+    private HttpServletRequest requestMock;
+
+    @Mock
+    private HttpServletResponse responseMock;
+
+    @Mock
+    private ServletContext servletContextMock;
+
+    @Mock
+    private RequestDispatcher requestDispatcherMock;
+
+    private SpringViewResolverProvider viewResolver;
+
+    private Locale locale = Locale.US;
+
+    @Before
+    public void setUp() {
+        this.viewResolver = new SpringViewResolverProvider(viewResolverMock, localeResolverMock);
+        ExchangeImpl exchange = new ExchangeImpl();
+        Endpoint endpoint = new MockEndpoint();
+        endpoint.put(ServerProviderFactory.class.getName(), ServerProviderFactory.getInstance());
+        exchange.put(Endpoint.class, endpoint);
+        exchange.put(ServerProviderFactory.class.getName(), ServerProviderFactory.getInstance());
+        MessageImpl message = new MessageImpl();
+        message.setExchange(exchange);
+        message.put(AbstractHTTPDestination.HTTP_REQUEST, requestMock);
+        message.put(AbstractHTTPDestination.HTTP_RESPONSE, responseMock);
+        message.put(AbstractHTTPDestination.HTTP_CONTEXT, servletContextMock);
+        viewResolver.setMessageContext(new MessageContextImpl(message));
+    }
+
+    @Test
+    public void testIsWriteableEnum() throws Exception {
+        String viewName = "/test";
+        View view = expectGetView(viewName);
+        viewResolver.setClassResources(Collections.singletonMap(TestEnum.class.getName() + "."
+                + TestEnum.ONE, viewName));
+        replayAll();
+        assertTrue(viewResolver.isWriteable(TestEnum.ONE.getClass(), null, null, null));
+        assertEquals(view, viewResolver.getView(TestEnum.ONE.getClass(), TestEnum.ONE));
+    }
+
+    @Test
+    public void testIsWriteableEnum2() {
+        String viewName = "/test";
+        View view = expectGetView(viewName);
+        viewResolver.setEnumResources(Collections.singletonMap(TestEnum.ONE, viewName));
+        replayAll();
+        assertTrue(viewResolver.isWriteable(TestEnum.ONE.getClass(), null, null, null));
+        assertEquals(view, viewResolver.getView(TestEnum.ONE.getClass(), TestEnum.ONE));
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testWriteTo() throws Exception {
+        String viewName = "/test";
+        expectWriteTo(viewName);
+        viewMock.render(anyObject(Map.class), anyObject(HttpServletRequest.class),
+                anyObject(HttpServletResponse.class));
+        expectLastCall();
+        replayAll();
+        viewResolver.writeTo(TestEnum.ONE, TestEnum.ONE.getClass(), null, new Annotation[] {},
+                MediaType.TEXT_HTML_TYPE,
+                new MultivaluedHashMap<String, Object>(), null);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testWriteToWithRenderingError() throws Exception {
+        String viewName = "/test";
+        Exception exception = new RuntimeException("my exception");
+        expectWriteTo(viewName);
+        viewMock.render(anyObject(Map.class), anyObject(HttpServletRequest.class),
+                anyObject(HttpServletResponse.class));
+        expectLastCall().andThrow(exception);
+        requestMock.setAttribute(RequestDispatcher.ERROR_EXCEPTION, exception);
+        requestMock.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, 500);
+        requestMock.setAttribute(RequestDispatcher.ERROR_MESSAGE, exception.getMessage());
+        expect(servletContextMock.getRequestDispatcher("/error")).andReturn(requestDispatcherMock);
+        requestDispatcherMock.forward(anyObject(HttpServletRequest.class), anyObject(HttpServletResponse.class));
+        expectLastCall();
+        replayAll();
+        viewResolver.writeTo(TestEnum.ONE, TestEnum.ONE.getClass(), null, new Annotation[] {},
+                MediaType.TEXT_HTML_TYPE,
+                new MultivaluedHashMap<String, Object>(), null);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test(expected = InternalServerErrorException.class)
+    public void testWriteToWithInternalRenderingError() throws Exception {
+        String viewName = "/test";
+        Exception exception = new RuntimeException("my exception");
+        expectWriteTo(viewName);
+        viewMock.render(anyObject(Map.class), anyObject(HttpServletRequest.class),
+                anyObject(HttpServletResponse.class));
+        expectLastCall().andThrow(exception);
+        requestMock.setAttribute(RequestDispatcher.ERROR_EXCEPTION, exception);
+        requestMock.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, 500);
+        requestMock.setAttribute(RequestDispatcher.ERROR_MESSAGE, exception.getMessage());
+        expect(servletContextMock.getRequestDispatcher("/error")).andReturn(requestDispatcherMock);
+        requestDispatcherMock.forward(anyObject(HttpServletRequest.class), anyObject(HttpServletResponse.class));
+        expectLastCall().andThrow(new RuntimeException("internal"));
+        replayAll();
+        viewResolver.writeTo(TestEnum.ONE, TestEnum.ONE.getClass(), null, new Annotation[] {},
+                MediaType.TEXT_HTML_TYPE,
+                new MultivaluedHashMap<String, Object>(), null);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test(expected = InternalServerErrorException.class)
+    public void testWriteToWithNullErrorView() throws Exception {
+        viewResolver.setErrorView(null);
+        String viewName = "/test";
+        Exception exception = new RuntimeException("my exception");
+        expectWriteTo(viewName);
+        viewMock.render(anyObject(Map.class), anyObject(HttpServletRequest.class),
+                anyObject(HttpServletResponse.class));
+        expectLastCall().andThrow(exception);
+        replayAll();
+        viewResolver.writeTo(TestEnum.ONE, TestEnum.ONE.getClass(), null, new Annotation[] {},
+                MediaType.TEXT_HTML_TYPE,
+                new MultivaluedHashMap<String, Object>(), null);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testConstructorWithViewResolverNull() {
+        new SpringViewResolverProvider(null, new AcceptHeaderLocaleResolver());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testConstructorWithLocaleResolverNull() {
+        new SpringViewResolverProvider(new BeanNameViewResolver(), null);
+    }
+
+    private View expectGetView(String viewName) {
+        expect(localeResolverMock.resolveLocale(anyObject(HttpServletRequest.class))).andReturn(locale);
+        try {
+            expect(viewResolverMock.resolveViewName(viewName, locale)).andReturn(viewMock);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        return viewMock;
+    }
+
+    private void expectWriteTo(String viewName) {
+        expectGetView(viewName);
+        viewResolver.setEnumResources(Collections.singletonMap(TestEnum.ONE, viewName));
+        expect(localeResolverMock.resolveLocale(anyObject(HttpServletRequest.class))).andReturn(locale);
+        try {
+            expect(viewResolverMock.resolveViewName(viewName, locale)).andReturn(viewMock);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private enum TestEnum {
+        ONE,
+        TWO
+    }
+
+    @SuppressWarnings("unused")
+    private static final class TestView implements View {
+
+        private String viewName;
+
+        TestView(String viewName) {
+            this.viewName = viewName;
+        }
+
+        @Override
+        public String getContentType() {
+            return null;
+        }
+
+        @Override
+        public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
+                throws Exception {
+        }
+
+        public String getViewName() {
+            return viewName;
+        }
+    }
+
+    private static final class MockEndpoint extends AbstractAttributedInterceptorProvider implements Endpoint {
+
+        private static final long serialVersionUID = 1L;
+
+        private EndpointInfo epi = new EndpointInfo();
+
+        MockEndpoint() {
+            epi.setBinding(new BindingInfo(null, null));
+        }
+
+        public List<Feature> getActiveFeatures() {
+            return null;
+        }
+
+        public Binding getBinding() {
+            return null;
+        }
+
+        public EndpointInfo getEndpointInfo() {
+            return this.epi;
+        }
+
+        public Executor getExecutor() {
+            return null;
+        }
+
+        public void setExecutor(Executor executor) {
+        }
+
+        public MessageObserver getInFaultObserver() {
+            return null;
+        }
+
+        public void setInFaultObserver(MessageObserver observer) {
+        }
+
+        public MessageObserver getOutFaultObserver() {
+            return null;
+        }
+
+        public void setOutFaultObserver(MessageObserver observer) {
+        }
+
+        public Service getService() {
+            return null;
+        }
+
+        public void addCleanupHook(Closeable c) {
+        }
+
+        public List<Closeable> getCleanupHooks() {
+            return null;
+        }
+    }
+
+    @SuppressWarnings("unused")
+    private static class MockBus implements Bus {
+
+        @Override
+        public <T> T getExtension(Class<T> extensionType) {
+            return null;
+        }
+
+        @Override
+        public <T> void setExtension(T extension, Class<T> extensionType) {
+
+        }
+
+        @Override
+        public boolean hasExtensionByName(String name) {
+            return false;
+        }
+
+        @Override
+        public String getId() {
+            return null;
+        }
+
+        @Override
+        public void setId(String i) {
+
+        }
+
+        @Override
+        public void shutdown(boolean wait) {
+
+        }
+
+        @Override
+        public void setProperty(String s, Object o) {
+
+        }
+
+        @Override
+        public Object getProperty(String s) {
+            return null;
+        }
+
+        @Override
+        public Map<String, Object> getProperties() {
+            return null;
+        }
+
+        @Override
+        public void setProperties(Map<String, Object> properties) {
+
+        }
+
+        @Override
+        public Collection<Feature> getFeatures() {
+            return null;
+        }
+
+        @Override
+        public void setFeatures(Collection<? extends Feature> features) {
+
+        }
+
+        @Override
+        public BusState getState() {
+            return null;
+        }
+
+        @Override
+        public List<Interceptor<? extends Message>> getInInterceptors() {
+            return null;
+        }
+
+        @Override
+        public List<Interceptor<? extends Message>> getOutInterceptors() {
+            return null;
+        }
+
+        @Override
+        public List<Interceptor<? extends Message>> getInFaultInterceptors() {
+            return null;
+        }
+
+        @Override
+        public List<Interceptor<? extends Message>> getOutFaultInterceptors() {
+            return null;
+        }
+    }
+}
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
['"commits@cxf.apache.org" <co...@cxf.apache.org>'].