DATAREST-1386 - Add HAL Explorer module

We now ship a module containing the HAL Explorer [0] as an alternative to the HAL Browser which seems to be mostly out of maintenance and the WebJar refers to a jQuery version with a CVE. The new module still contains some customization code that we had in place for the browser to customize the POST/PUT forms to use the JSON Schema exposed for the aggregates.

The HAL Browser extension is now deprecated in the form that its inclusion causes a warn log to recommend switching to the explorer. A couple of general coding improvements: package scope for controllers and handler methods. Better use of constants and newer request mapping annotations.

Tweaked the configuration of static resource handlers to resolve the implicit cyclic dependency between the browser/explorer and the WebMVC module by introducing a Spring Factories backed extension SPI.

[0] https://github.com/toedter/hal-explorer
This commit is contained in:
Oliver Drotbohm
2019-06-05 11:09:44 +02:00
parent 49605b2b1a
commit e744c1c94f
15 changed files with 842 additions and 21 deletions

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2015-2019 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
*
* https://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.springframework.data.rest.webmvc.halexplorer;
import static org.hamcrest.CoreMatchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
/**
* Integration tests for {@link HalExplorer}.
*
* @author Oliver Gierke
* @soundtrack Miles Davis - Blue in green (Kind of blue)
*/
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration
public class HalExplorerIntegrationTests {
static final String BASE_PATH = "/api";
static final String EXPLORER_INDEX = "/explorer/index.html";
static final String TARGET = BASE_PATH.concat(EXPLORER_INDEX).concat("#").concat(BASE_PATH);
@Configuration
@EnableWebMvc
static class TestConfiguration extends RepositoryRestMvcConfiguration {
public TestConfiguration(ApplicationContext context, ObjectFactory<ConversionService> conversionService) {
super(context, conversionService);
}
@Bean
RepositoryRestConfigurer configExtension() {
return RepositoryRestConfigurer.withConfig(config -> config.setBasePath(BASE_PATH));
}
}
@Autowired WebApplicationContext context;
MockMvc mvc;
@Before
public void setUp() {
this.mvc = MockMvcBuilders.webAppContextSetup(context).//
defaultRequest(get(BASE_PATH).accept(MediaType.TEXT_HTML)).build();
}
@Test // DATAREST-293
public void exposesJsonUnderApiRootByDefault() throws Exception {
mvc.perform(get(BASE_PATH).accept(MediaType.ALL)).//
andExpect(status().isOk()).//
andExpect(header().string(HttpHeaders.CONTENT_TYPE, startsWith(MediaTypes.HAL_JSON.toString())));
}
@Test // DATAREST-293
public void redirectsToBrowserForApiRootAndHtml() throws Exception {
mvc.perform(get(BASE_PATH).accept(MediaType.TEXT_HTML)).//
andExpect(status().isFound()).//
andExpect(header().string(HttpHeaders.LOCATION, endsWith(TARGET)));
}
@Test // DATAREST-293
public void forwardsBrowserToIndexHtml() throws Exception {
mvc.perform(get(BASE_PATH.concat("/explorer"))).//
andExpect(status().isFound()).//
andExpect(header().string(HttpHeaders.LOCATION, endsWith(TARGET)));
}
@Test // DATAREST-293
public void exposesHalBrowser() throws Exception {
mvc.perform(get(BASE_PATH.concat("/explorer/index.html"))).//
andExpect(status().isOk()).//
andExpect(content().string(containsString("HAL Explorer")));
}
@Test // DATAREST-293
public void retrunsApiIfHtmlIsNotExplicitlyListed() throws Exception {
mvc.perform(get(BASE_PATH).accept(MediaType.APPLICATION_JSON, MediaType.ALL)).//
andExpect(status().isOk()).//
andExpect(header().string(HttpHeaders.CONTENT_TYPE, startsWith(MediaType.APPLICATION_JSON_VALUE)));
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2015-2019 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
*
* https://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.springframework.data.rest.webmvc.halexplorer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import java.io.IOException;
import java.util.Collections;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import org.junit.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.filter.ForwardedHeaderFilter;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.AbstractView;
import org.springframework.web.servlet.view.RedirectView;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Unit tests for {@link HalExplorer}.
*
* @author Oliver Gierke
* @author Mark Paluch
* @soundtrack Nils Wülker - Homeless Diamond (feat. Lauren Flynn)
*/
public class HalExplorerUnitTests {
@Test // DATAREST-565, DATAREST-720
public void createsContextRelativeRedirectForBrowser() throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestURI("/context");
request.setContextPath("/context");
View view = new HalExplorer().explorer(request);
assertThat(view).isInstanceOf(RedirectView.class);
((AbstractView) view).render(Collections.<String, Object> emptyMap(), request, response);
UriComponents components = UriComponentsBuilder.fromUriString(response.getHeader(HttpHeaders.LOCATION)).build();
assertThat(components.getPath(), startsWith("/context"));
assertThat(components.getFragment()).isEqualTo("/context");
}
@Test // DATAREST-1264
public void producesProxyRelativeRedirectIfNecessary() throws ServletException, IOException {
MockHttpServletResponse response = new MockHttpServletResponse();
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/explorer");
request.addHeader("X-Forwarded-Host", "somehost");
request.addHeader("X-Forwarded-Port", "4711");
request.addHeader("X-Forwarded-Proto", "https");
request.addHeader("X-Forwarded-Prefix", "/prefix");
ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
filter.doFilter(request, response, (req, resp) -> {
View view = new HalExplorer().explorer((HttpServletRequest) req);
assertThat(view).isInstanceOf(RedirectView.class);
String url = ((RedirectView) view).getUrl();
assertThat(url, startsWith("https://somehost:4711/prefix"));
assertThat(url, endsWith("/prefix"));
});
}
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d %5p %40.40c:%4L - %m%n</pattern>
</encoder>
</appender>
<logger name="org.springframework" level="warn" />
<root level="error">
<appender-ref ref="console" />
</root>
</configuration>