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); +}