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

@@ -22,6 +22,7 @@
<module>spring-data-rest-webmvc</module>
<module>spring-data-rest-distribution</module>
<module>spring-data-rest-hal-browser</module>
<module>spring-data-rest-hal-explorer</module>
</modules>
<properties>

View File

@@ -15,12 +15,13 @@
*/
package org.springframework.data.rest.webmvc.halbrowser;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.HttpServletRequest;
import org.springframework.data.rest.webmvc.BasePathAwareController;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.servlet.view.RedirectView;
@@ -32,19 +33,27 @@ import org.springframework.web.util.UriComponents;
* @author Oliver Gierke
* @soundtrack Miles Davis - So what (Kind of blue)
*/
@Slf4j
@BasePathAwareController
public class HalBrowser {
class HalBrowser {
private static String BROWSER = "/browser";
private static String INDEX = "/index.html";
static final String BROWSER = "/browser";
static final String INDEX = "/index.html";
HalBrowser() {
LOG.warn("---");
LOG.warn(
"Spring Data REST HAL Browser is deprecated! Prefer the HAL Explorer (artifactId: spring-data-rest-hal-explorer)!");
LOG.warn("---");
}
/**
* Redirects requests to the API root asking for HTML to the HAL browser.
*
* @return
*/
@RequestMapping(value = { "/", "" }, method = RequestMethod.GET, produces = MediaType.TEXT_HTML_VALUE)
public View index(HttpServletRequest request) {
@GetMapping(path = { "/", "" }, produces = MediaType.TEXT_HTML_VALUE)
View index(HttpServletRequest request) {
return getRedirectView(request, false);
}
@@ -53,9 +62,9 @@ public class HalBrowser {
*
* @return
*/
@RequestMapping(value = "/browser", method = RequestMethod.GET)
public View browser(HttpServletRequest request) {
return getRedirectView(request, request.getRequestURI().endsWith("/browser"));
@GetMapping(path = BROWSER)
View browser(HttpServletRequest request) {
return getRedirectView(request, request.getRequestURI().endsWith(BROWSER));
}
/**
@@ -77,7 +86,7 @@ public class HalBrowser {
}
builder.path(INDEX);
builder.fragment(browserRelative ? path.substring(0, path.lastIndexOf("/browser")) : path);
builder.fragment(browserRelative ? path.substring(0, path.lastIndexOf(BROWSER)) : path);
return new RedirectView(builder.build().toUriString());
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 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
*
* 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.springframework.data.rest.webmvc.halbrowser;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.config.StaticResourceProvider;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
/**
* {@link StaticResourceProvider} to expose the HAL Browser WebJar content via a static resource route.
*
* @author Oliver Drotbohm
* @since 3.2
* @soundtrack Tedeschi Trucks Band - Signs, High Times (Signs)
*/
class HalBrowserConfiguration implements StaticResourceProvider {
/*
* (non-Javadoc)
* @see org.springframework.data.rest.webmvc.config.StaticResourceProvider#customizeResources(org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry, org.springframework.data.rest.core.config.RepositoryRestConfiguration)
*/
@Override
public void customizeResources(ResourceHandlerRegistry registry, RepositoryRestConfiguration configuration) {
String basePath = configuration.getBasePath().toString().concat(HalBrowser.BROWSER);
String rootLocation = "classpath:META-INF/spring-data-rest/hal-browser/";
registry.addResourceHandler(basePath.concat("/**")).addResourceLocations(rootLocation);
}
}

View File

@@ -0,0 +1 @@
org.springframework.data.rest.webmvc.config.StaticResourceProvider=org.springframework.data.rest.webmvc.halbrowser.HalBrowserConfiguration

View File

@@ -9,7 +9,7 @@
<logger name="org.springframework" level="warn" />
<root level="error">
<root level="warn">
<appender-ref ref="console" />
</root>

View File

@@ -0,0 +1,177 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-parent</artifactId>
<version>3.2.0.BUILD-SNAPSHOT</version>
</parent>
<artifactId>spring-data-rest-hal-explorer</artifactId>
<name>Spring Data REST - HAL Explorer</name>
<properties>
<explorer.version>0.9.5</explorer.version>
<json-editor.version>0.7.21</json-editor.version>
<java-module-name>spring.data.rest.hal.explorer</java-module-name>
<project.root>${basedir}/..</project.root>
</properties>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-data-rest-webmvc</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>hal-explorer</artifactId>
<version>${explorer.version}</version>
<scope>provided</scope>
</dependency>
<!--
<dependency>
<groupId>org.webjars</groupId>
<artifactId>json-editor</artifactId>
<version>${json-editor.version}</version>
<scope>provided</scope>
</dependency>
-->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>unpack-hal-explorer</id>
<phase>process-resources</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.webjars</groupId>
<artifactId>hal-explorer</artifactId>
</artifactItem>
</artifactItems>
<outputDirectory>${project.build.directory}/hal-explorer</outputDirectory>
</configuration>
</execution>
<!--
<execution>
<id>unpack-json-editor</id>
<phase>process-resources</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.webjars</groupId>
<artifactId>json-editor</artifactId>
</artifactItem>
</artifactItems>
<outputDirectory>${project.build.directory}/json-editor</outputDirectory>
</configuration>
</execution>
-->
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<phase>process-resources</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<copy todir="${project.build.outputDirectory}/META-INF/spring-data-rest/hal-explorer">
<fileset dir="${project.build.directory}/hal-explorer/META-INF/resources/webjars/hal-explorer/${explorer.version}" />
</copy>
<!--
<copy todir="${project.build.outputDirectory}/META-INF/spring-data-rest/hal-explorer/vendor/js">
<fileset dir="${project.build.directory}/json-editor/META-INF/resources/webjars/json-editor/${json-editor.version}" />
</copy>
<copy file="${project.build.outputDirectory}/META-INF/spring-data-rest/hal-explorer/explorer.html"
tofile="${project.build.outputDirectory}/META-INF/spring-data-rest/hal-explorer/index.html" />
<replace file="${project.build.outputDirectory}/META-INF/spring-data-rest/hal-explorer/index.html">
<replacefilter>
<replacetoken><![CDATA[</body>]]></replacetoken>
<replacevalue><![CDATA[
<script id="dynamic-request-template" type="text/template">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>Create/Update</h3>
</div>
<form class="non-safe" action="<%= href %>">
<div class="modal-body" style="padding-top: 0px">
<div id="jsoneditor"></div>
<div class="well well-small" style="padding-bottom: 0px;">
<div class="container-fluid">
<div class="row-fluid">
<div class="control-group">
<label class="control-label" style="display: inline-block; font-weight: bold;">Action:</label>
<input name="method" type="text" class="method controls" style="width: 98%" value="POST" />
<input name="url" type="text" class="url controls" style="width: 98%" value="<%= href %>" />
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Make Request</button>
</div>
</form>
</script>
<script src="vendor/js/jsoneditor.js"></script>
<script src="js/CustomPostForm.js"></script>
</body>]]>
</replacevalue>
</replacefilter>
</replace>
-->
<replace file="${project.build.outputDirectory}/META-INF/spring-data-rest/hal-explorer/index.html">
<replacefilter>
<replacetoken>The HAL explorer</replacetoken>
<replacevalue>The HAL Explorer (for Spring Data REST)</replacevalue>
</replacefilter>
</replace>
<!--
<delete file="${project.build.outputDirectory}/META-INF/spring-data-rest/hal-explorer/explorer.html" />
-->
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,83 @@
/*
* 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 javax.servlet.http.HttpServletRequest;
import org.springframework.data.rest.webmvc.BasePathAwareController;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.servlet.view.RedirectView;
import org.springframework.web.util.UriComponents;
/**
* Controller with a few convenience redirects to expose the HAL explorer shipped as static content.
*
* @author Oliver Gierke
* @soundtrack Miles Davis - So what (Kind of blue)
*/
@BasePathAwareController
class HalExplorer {
static final String EXPLORER = "/explorer";
static final String INDEX = "/index.html";
/**
* Redirects requests to the API root asking for HTML to the HAL explorer.
*
* @return
*/
@GetMapping(value = { "/", "" }, produces = MediaType.TEXT_HTML_VALUE)
View index(HttpServletRequest request) {
return getRedirectView(request, false);
}
/**
* Redirects to the actual {@code index.html}.
*
* @return
*/
@GetMapping(value = EXPLORER)
public View explorer(HttpServletRequest request) {
return getRedirectView(request, request.getRequestURI().endsWith(EXPLORER));
}
/**
* Returns the View to redirect to to access the HAL explorer.
*
* @param request must not be {@literal null}.
* @param explorerRelative
* @return
*/
private View getRedirectView(HttpServletRequest request, boolean explorerRelative) {
ServletUriComponentsBuilder builder = ServletUriComponentsBuilder.fromRequest(request);
UriComponents components = builder.build();
String path = components.getPath() == null ? "" : components.getPath();
if (!explorerRelative) {
builder.path(EXPLORER);
}
builder.path(INDEX);
builder.fragment(explorerRelative ? path.substring(0, path.lastIndexOf(EXPLORER)) : path);
return new RedirectView(builder.build().toUriString());
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 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
*
* 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.springframework.data.rest.webmvc.halexplorer;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.config.StaticResourceProvider;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
/**
* {@link StaticResourceProvider} to expose the HAL Browser WebJar content via a static resource route.
*
* @author Oliver Drotbohm
* @since 3.2
* @soundtrack Tedeschi Trucks Band - Signs, High Times (Signs)
*/
class HalExplorerConfiguration implements StaticResourceProvider {
/*
* (non-Javadoc)
* @see org.springframework.data.rest.webmvc.config.StaticResourceProvider#customizeResources(org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry, org.springframework.data.rest.core.config.RepositoryRestConfiguration)
*/
public void customizeResources(ResourceHandlerRegistry registry, RepositoryRestConfiguration configuration) {
String basePath = configuration.getBasePath().toString().concat(HalExplorer.EXPLORER);
String rootLocation = "classpath:META-INF/spring-data-rest/hal-explorer/";
registry.addResourceHandler(basePath.concat("/**")).addResourceLocations(rootLocation);
}
}

View File

@@ -0,0 +1,203 @@
/**
* Custom Backbone view that uses JSON Schema metadata to create pop-up dialog with actual field names instead of
* asking user to input raw JSON.
*
* NOTE: Because JSON Schema lists all properties, including those that are links, they have to be filtered out.
* Links have to be set via a PUT operation with the proper media type.
*
* @author Greg Turnquist
* @author Gregory Frank
* @since 2.4
* @see DATAREST-627, DATAREST-1077
*/
/* jshint strict: true */
/* globals HAL, Backbone, _, $, window, jqxhr */
'use strict';
var CustomPostForm = Backbone.View.extend({
initialize: function (opts) {
this.href = opts.href.split('{')[0];
this.vent = opts.vent;
_.bindAll(this, 'createNewResource');
},
events: {
'submit form': 'createNewResource'
},
className: 'modal fade',
/**
* Perform a POST/PUT operation on the resource.
*
* @param e
*/
createNewResource: function (e) {
e.preventDefault();
var self = this;
var opts = {
url: this.$('.url').val(),
headers: _.defaults({'Content-Type': 'application/json'}, HAL.client.getHeaders()),
method: this.$('.method').val(),
data: this.getNewResourceData()
};
HAL.client.request(opts).done(function (response) {
self.vent.trigger('response', {resource: response, jqxhr: jqxhr});
}).fail(function (e) {
self.vent.trigger('fail-response', {jqxhr: jqxhr});
}).always(function (e) {
self.vent.trigger('response-headers', {jqxhr: jqxhr});
window.location.hash = 'NON-GET:' + opts.url;
});
this.$el.modal('hide');
},
/**
* Draw the dialog after fetching the resource's JSON Schema metadata. If no metadata is available, use the
* fallback editor.
*
* @param opts
* @returns {CustomPostForm}
*/
render: function (opts) {
var self = this;
try {
HAL.client.request({
method: 'HEAD',
headers: HAL.client.getHeaders(),
url: this.href
}).done(function (message, text, jqXHR) {
self.$el.html(self.template({href: self.href}));
try {
var hal = self.w3cLinksToHalLinks(jqXHR.getResponseHeader('Link'));
HAL.client.request({
method: 'GET',
url: hal._links.profile.href,
headers: _.defaults({'Accept': 'application/schema+json'}, HAL.client.getHeaders())
}).done(function (schema) {
self.loadJsonEditor(schema);
});
} catch (e) {
self.loadFallbackEditor();
}
self.$el.modal();
});
} catch (e) {
self.loadFallbackEditor();
self.$el.modal();
}
return this;
},
/**
* Load the JSON Schema-driven editor.
*
* @see https://github.com/jdorn/json-editor
*/
loadJsonEditor: function (schema) {
var self = this;
/**
* Remove URI-based fields since this dialog doesn't handle relationships.
*/
Object.keys(schema.properties).forEach(function (property) {
if (schema.properties[property].hasOwnProperty('format') &&
schema.properties[property].format === 'uri') {
delete schema.properties[property];
}
});
/**
* See https://github.com/jdorn/json-editor#options for more customizing options.
*/
this.editor = new window.JSONEditor(this.$('#jsoneditor')[0], {
theme: 'bootstrap2',
schema: schema,
disable_collapse: true,
disable_edit_json: true,
disable_properties: true
});
this.getNewResourceData = function() {
return JSON.stringify(self.editor.getValue());
}
},
/**
* Load fallback editor that doesn't depend on any form of metadata.
*/
loadFallbackEditor: function () {
var editor = this.$('#jsoneditor');
editor.append($('<h4>' + this.href + '</h4>'));
var inputBox = $('<textarea name="body" class="body" style="height: 200px">{\n}</textarea>');
editor.append(inputBox);
this.getNewResourceData = function() {
return inputBox.val();
}
},
/**
* Convert a W3C link header into a HAL-based set of _links.
*
* e.g.
* <http://localhost:8080/persons>; rel="persons",<http://localhost:8080/profile/persons>; rel="profile"
* to
* {
* _links: {
* persons: {
* href: http://localhost:8080/persons
* },
* profile: {
* href: http://localhost:8080/profile/persons
* }
* }
* }
*
* @param linkHeader - HTTP Response header containing a list of W3C compliant links with rels.
* @see http://www.w3.org/wiki/LinkHeader
*/
w3cLinksToHalLinks: function (linkHeader) {
var w3cLinks = linkHeader.split(',');
var halLinks = {_links: {}};
w3cLinks.forEach(function (w3cLink) {
var parts = w3cLink.split(';');
var hrefWrappedWithBrackets = parts[0];
var href = hrefWrappedWithBrackets.slice(1, parts[0].length - 1);
var w3cRel = parts[1];
var relWrappedWithQuotes = w3cRel.split('=')[1];
var rel = relWrappedWithQuotes.slice(1, relWrappedWithQuotes.length - 1);
halLinks._links[rel] = { "href": href };
});
return halLinks;
},
/**
* Look up the HTML template.
*/
template: _.template($('#dynamic-request-template').html())
});
/**
* Inject the form into the HAL Browser.
*/
HAL.customPostForm = CustomPostForm;

View File

@@ -0,0 +1 @@
org.springframework.data.rest.webmvc.config.StaticResourceProvider=org.springframework.data.rest.webmvc.halexplorer.HalExplorerConfiguration

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>

View File

@@ -46,6 +46,7 @@ import org.springframework.context.support.ReloadableResourceBundleMessageSource
import org.springframework.core.Ordered;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.data.auditing.AuditableBeanWrapperFactory;
import org.springframework.data.auditing.MappingAuditableBeanWrapperFactory;
import org.springframework.data.domain.PageRequest;
@@ -883,16 +884,10 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Register HAL browser if present
RepositoryRestConfiguration configuration = repositoryRestConfiguration();
if (ClassUtils.isPresent("org.springframework.data.rest.webmvc.halbrowser.HalBrowser",
getClass().getClassLoader())) {
String basePath = repositoryRestConfiguration().getBasePath().toString().concat("/browser");
String rootLocation = "classpath:META-INF/spring-data-rest/hal-browser/";
registry.addResourceHandler(basePath.concat("/**")).addResourceLocations(rootLocation);
}
SpringFactoriesLoader.loadFactories(StaticResourceProvider.class, beanClassLoader)
.forEach(it -> it.customizeResources(registry, configuration));
}
private static class ResourceSupportHttpMessageConverter extends TypeConstrainedMappingJackson2HttpMessageConverter

View File

@@ -0,0 +1,38 @@
/*
* Copyright 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
*
* 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.springframework.data.rest.webmvc.config;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
/**
* SPI to be able to register extensions that add static resource routes.
*
* @author Oliver Drotbohm
* @since 3.2
* @see org.springframework.data.rest.webmvc.halbrowser.HalBrowserConfiguration
* @see org.springframework.data.rest.webmvc.halexplorer.HalExplorerConfiguration
*/
public interface StaticResourceProvider {
/**
* Customize the given {@link ResourceHandlerRegistry}.
*
* @param registry must not be {@literal null}.
* @param configuration must not be {@literal null}.
*/
void customizeResources(ResourceHandlerRegistry registry, RepositoryRestConfiguration configuration);
}