diff --git a/pom.xml b/pom.xml
index 32090c507..2ae0e94ff 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,6 +22,7 @@
spring-data-rest-webmvc
spring-data-rest-distribution
spring-data-rest-hal-browser
+ spring-data-rest-hal-explorer
diff --git a/spring-data-rest-hal-browser/src/main/java/org/springframework/data/rest/webmvc/halbrowser/HalBrowser.java b/spring-data-rest-hal-browser/src/main/java/org/springframework/data/rest/webmvc/halbrowser/HalBrowser.java
index cc6c94496..005b9bc6a 100644
--- a/spring-data-rest-hal-browser/src/main/java/org/springframework/data/rest/webmvc/halbrowser/HalBrowser.java
+++ b/spring-data-rest-hal-browser/src/main/java/org/springframework/data/rest/webmvc/halbrowser/HalBrowser.java
@@ -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());
}
diff --git a/spring-data-rest-hal-browser/src/main/java/org/springframework/data/rest/webmvc/halbrowser/HalBrowserConfiguration.java b/spring-data-rest-hal-browser/src/main/java/org/springframework/data/rest/webmvc/halbrowser/HalBrowserConfiguration.java
new file mode 100644
index 000000000..ea080ee99
--- /dev/null
+++ b/spring-data-rest-hal-browser/src/main/java/org/springframework/data/rest/webmvc/halbrowser/HalBrowserConfiguration.java
@@ -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);
+ }
+}
diff --git a/spring-data-rest-hal-browser/src/main/resources/META-INF/spring.factories b/spring-data-rest-hal-browser/src/main/resources/META-INF/spring.factories
new file mode 100644
index 000000000..45fc8f7e9
--- /dev/null
+++ b/spring-data-rest-hal-browser/src/main/resources/META-INF/spring.factories
@@ -0,0 +1 @@
+org.springframework.data.rest.webmvc.config.StaticResourceProvider=org.springframework.data.rest.webmvc.halbrowser.HalBrowserConfiguration
diff --git a/spring-data-rest-hal-browser/src/test/resources/logback.xml b/spring-data-rest-hal-browser/src/test/resources/logback.xml
index 7a648be10..19b6066c3 100644
--- a/spring-data-rest-hal-browser/src/test/resources/logback.xml
+++ b/spring-data-rest-hal-browser/src/test/resources/logback.xml
@@ -9,7 +9,7 @@
-
+
diff --git a/spring-data-rest-hal-explorer/pom.xml b/spring-data-rest-hal-explorer/pom.xml
new file mode 100644
index 000000000..6c00d9438
--- /dev/null
+++ b/spring-data-rest-hal-explorer/pom.xml
@@ -0,0 +1,177 @@
+
+
+ 4.0.0
+
+
+ org.springframework.data
+ spring-data-rest-parent
+ 3.2.0.BUILD-SNAPSHOT
+
+
+ spring-data-rest-hal-explorer
+
+ Spring Data REST - HAL Explorer
+
+
+ 0.9.5
+ 0.7.21
+ spring.data.rest.hal.explorer
+ ${basedir}/..
+
+
+
+
+
+ ${project.groupId}
+ spring-data-rest-webmvc
+ ${project.version}
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.0.1
+ provided
+
+
+
+ org.webjars
+ hal-explorer
+ ${explorer.version}
+ provided
+
+
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ unpack-hal-explorer
+ process-resources
+
+ unpack
+
+
+
+
+ org.webjars
+ hal-explorer
+
+
+ ${project.build.directory}/hal-explorer
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-antrun-plugin
+
+
+ process-resources
+
+ run
+
+
+
+
+
+
+
+
+
+ The HAL explorer
+ The HAL Explorer (for Spring Data REST)
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-data-rest-hal-explorer/src/main/java/org/springframework/data/rest/webmvc/halexplorer/HalExplorer.java b/spring-data-rest-hal-explorer/src/main/java/org/springframework/data/rest/webmvc/halexplorer/HalExplorer.java
new file mode 100644
index 000000000..05eac69ee
--- /dev/null
+++ b/spring-data-rest-hal-explorer/src/main/java/org/springframework/data/rest/webmvc/halexplorer/HalExplorer.java
@@ -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());
+ }
+}
diff --git a/spring-data-rest-hal-explorer/src/main/java/org/springframework/data/rest/webmvc/halexplorer/HalExplorerConfiguration.java b/spring-data-rest-hal-explorer/src/main/java/org/springframework/data/rest/webmvc/halexplorer/HalExplorerConfiguration.java
new file mode 100644
index 000000000..e43cc0ed5
--- /dev/null
+++ b/spring-data-rest-hal-explorer/src/main/java/org/springframework/data/rest/webmvc/halexplorer/HalExplorerConfiguration.java
@@ -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);
+ }
+}
diff --git a/spring-data-rest-hal-explorer/src/main/resources/META-INF/spring-data-rest/hal-explorer/js/CustomPostForm.js b/spring-data-rest-hal-explorer/src/main/resources/META-INF/spring-data-rest/hal-explorer/js/CustomPostForm.js
new file mode 100644
index 000000000..0b5f113f4
--- /dev/null
+++ b/spring-data-rest-hal-explorer/src/main/resources/META-INF/spring-data-rest/hal-explorer/js/CustomPostForm.js
@@ -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($('' + this.href + '
'));
+
+ var inputBox = $('');
+ editor.append(inputBox);
+
+ this.getNewResourceData = function() {
+ return inputBox.val();
+ }
+ },
+
+ /**
+ * Convert a W3C link header into a HAL-based set of _links.
+ *
+ * e.g.
+ * ; rel="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;
\ No newline at end of file
diff --git a/spring-data-rest-hal-explorer/src/main/resources/META-INF/spring.factories b/spring-data-rest-hal-explorer/src/main/resources/META-INF/spring.factories
new file mode 100644
index 000000000..107e87d16
--- /dev/null
+++ b/spring-data-rest-hal-explorer/src/main/resources/META-INF/spring.factories
@@ -0,0 +1 @@
+org.springframework.data.rest.webmvc.config.StaticResourceProvider=org.springframework.data.rest.webmvc.halexplorer.HalExplorerConfiguration
diff --git a/spring-data-rest-hal-explorer/src/test/java/org/springframework/data/rest/webmvc/halexplorer/HalExplorerIntegrationTests.java b/spring-data-rest-hal-explorer/src/test/java/org/springframework/data/rest/webmvc/halexplorer/HalExplorerIntegrationTests.java
new file mode 100755
index 000000000..22b27a36f
--- /dev/null
+++ b/spring-data-rest-hal-explorer/src/test/java/org/springframework/data/rest/webmvc/halexplorer/HalExplorerIntegrationTests.java
@@ -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) {
+ 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)));
+ }
+}
diff --git a/spring-data-rest-hal-explorer/src/test/java/org/springframework/data/rest/webmvc/halexplorer/HalExplorerUnitTests.java b/spring-data-rest-hal-explorer/src/test/java/org/springframework/data/rest/webmvc/halexplorer/HalExplorerUnitTests.java
new file mode 100755
index 000000000..a7be66424
--- /dev/null
+++ b/spring-data-rest-hal-explorer/src/test/java/org/springframework/data/rest/webmvc/halexplorer/HalExplorerUnitTests.java
@@ -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. 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"));
+ });
+ }
+}
diff --git a/spring-data-rest-hal-explorer/src/test/resources/logback.xml b/spring-data-rest-hal-explorer/src/test/resources/logback.xml
new file mode 100644
index 000000000..7a648be10
--- /dev/null
+++ b/spring-data-rest-hal-explorer/src/test/resources/logback.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ %d %5p %40.40c:%4L - %m%n
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java
index dd24887ab..6d356c974 100644
--- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java
+++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java
@@ -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
diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/StaticResourceProvider.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/StaticResourceProvider.java
new file mode 100644
index 000000000..1681d3ea3
--- /dev/null
+++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/StaticResourceProvider.java
@@ -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);
+}