From 5fb8c84e5ef8d88b5733db773f0a770308d13b8d Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Fri, 11 Dec 2015 11:15:07 +0100 Subject: [PATCH] Improve resources caching Update controllers to add an ETag information so that meta-data is cached on the client. Also enables the compression for json, css and html resources. Closes gh-165 --- initializr-service/application.yml | 6 +++++ .../initializr/web/MainController.groovy | 26 +++++++++++++++---- .../spring/initializr/web/UiController.groovy | 17 ++++++++++-- .../web/MainControllerIntegrationTests.groovy | 8 ++++++ 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/initializr-service/application.yml b/initializr-service/application.yml index 75703ea6..52fda007 100644 --- a/initializr-service/application.yml +++ b/initializr-service/application.yml @@ -6,6 +6,12 @@ info: spring-boot: version: 1.3.0.RELEASE +server: + compression: + enabled: true + mime-types: application/json,text/css,text/html + min-response-size: 2048 + initializr: env: boms: diff --git a/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy b/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy index c79d4093..ccba3016 100644 --- a/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy @@ -16,6 +16,9 @@ package io.spring.initializr.web +import java.nio.charset.StandardCharsets +import java.util.concurrent.TimeUnit + import groovy.util.logging.Slf4j import io.spring.initializr.generator.CommandLineHelpGenerator import io.spring.initializr.mapper.InitializrMetadataJsonMapper @@ -27,11 +30,13 @@ import io.spring.initializr.generator.ProjectRequest import io.spring.initializr.metadata.InitializrMetadata import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.CacheControl import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.stereotype.Controller +import org.springframework.util.DigestUtils import org.springframework.web.bind.annotation.ModelAttribute import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestMapping @@ -86,16 +91,20 @@ class MainController extends AbstractInitializrController { def builder = ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN) if (userAgent) { if (userAgent.startsWith(WebConfig.CURL_USER_AGENT_PREFIX)) { - return builder.body(commandLineHelpGenerator.generateCurlCapabilities(metadata, appUrl)) + def content = commandLineHelpGenerator.generateCurlCapabilities(metadata, appUrl) + return builder.eTag(createUniqueId(content)).body(content) } if (userAgent.startsWith(WebConfig.HTTPIE_USER_AGENT_PREFIX)) { - return builder.body(commandLineHelpGenerator.generateHttpieCapabilities(metadata, appUrl)) + def content = commandLineHelpGenerator.generateHttpieCapabilities(metadata, appUrl) + return builder.eTag(createUniqueId(content)).body(content) } if (userAgent.startsWith(WebConfig.SPRING_BOOT_CLI_AGENT_PREFIX)) { - return builder.body(commandLineHelpGenerator.generateSpringBootCliCapabilities(metadata, appUrl)) + def content = commandLineHelpGenerator.generateSpringBootCliCapabilities(metadata, appUrl) + return builder.eTag(createUniqueId(content)).body(content) } } - builder.body(commandLineHelpGenerator.generateGenericCapabilities(metadata, appUrl)) + def content = commandLineHelpGenerator.generateGenericCapabilities(metadata, appUrl) + builder.eTag(createUniqueId(content)).body(content) } @RequestMapping(value = "/", produces = ["application/hal+json"]) @@ -120,7 +129,8 @@ class MainController extends AbstractInitializrController { private ResponseEntity serviceCapabilitiesFor(InitializrMetadataVersion version, MediaType contentType) { String appUrl = generateAppUrl() def content = getJsonMapper(version).write(metadataProvider.get(), appUrl) - return ResponseEntity.ok().contentType(contentType).body(content) + return ResponseEntity.ok().contentType(contentType).eTag(createUniqueId(content)) + .cacheControl(CacheControl.maxAge(7, TimeUnit.DAYS)).body(content) } private static InitializrMetadataJsonMapper getJsonMapper(InitializrMetadataVersion version) { @@ -220,4 +230,10 @@ class MainController extends AbstractInitializrController { result } + private String createUniqueId(String content) { + StringBuilder builder = new StringBuilder() + DigestUtils.appendMd5DigestAsHex(content.getBytes(StandardCharsets.UTF_8), builder) + builder.toString() + } + } diff --git a/initializr/src/main/groovy/io/spring/initializr/web/UiController.groovy b/initializr/src/main/groovy/io/spring/initializr/web/UiController.groovy index 6b5150c7..681fb865 100644 --- a/initializr/src/main/groovy/io/spring/initializr/web/UiController.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/web/UiController.groovy @@ -16,6 +16,8 @@ package io.spring.initializr.web +import java.nio.charset.StandardCharsets + import groovy.json.JsonBuilder import io.spring.initializr.metadata.Dependency import io.spring.initializr.metadata.InitializrMetadataProvider @@ -23,6 +25,9 @@ import io.spring.initializr.util.Version import io.spring.initializr.util.VersionRange import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.util.DigestUtils import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @@ -40,7 +45,7 @@ class UiController { protected InitializrMetadataProvider metadataProvider @RequestMapping(value = "/ui/dependencies", produces = ["application/json"]) - String dependencies(@RequestParam(required = false) String version) { + ResponseEntity dependencies(@RequestParam(required = false) String version) { def dependencyGroups = metadataProvider.get().dependencies.content def content = [] Version v = version ? Version.parse(version) : null @@ -55,7 +60,9 @@ class UiController { } } } - writeDependencies(content) + def json = writeDependencies(content) + ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON). + eTag(createUniqueId(json)).body(json) } private static String writeDependencies(List items) { @@ -96,4 +103,10 @@ class UiController { } } + private String createUniqueId(String content) { + StringBuilder builder = new StringBuilder() + DigestUtils.appendMd5DigestAsHex(content.getBytes(StandardCharsets.UTF_8), builder) + builder.toString() + } + } diff --git a/initializr/src/test/groovy/io/spring/initializr/web/MainControllerIntegrationTests.groovy b/initializr/src/test/groovy/io/spring/initializr/web/MainControllerIntegrationTests.groovy index 8416b891..e37b3d2b 100644 --- a/initializr/src/test/groovy/io/spring/initializr/web/MainControllerIntegrationTests.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/web/MainControllerIntegrationTests.groovy @@ -24,6 +24,7 @@ import org.junit.Ignore import org.junit.Test import org.skyscreamer.jsonassert.JSONCompareMode +import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity @@ -32,6 +33,7 @@ import org.springframework.web.client.HttpClientErrorException import static org.hamcrest.CoreMatchers.allOf import static org.hamcrest.CoreMatchers.containsString +import static org.hamcrest.CoreMatchers.nullValue import static org.hamcrest.core.IsNot.not import static org.junit.Assert.assertEquals import static org.junit.Assert.assertFalse @@ -169,6 +171,7 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra @Test void metadataWithCurrentAcceptHeader() { ResponseEntity response = invokeHome(null, 'application/vnd.initializr.v2.1+json') + assertThat(response.getHeaders().getFirst(HttpHeaders.ETAG), not(nullValue())) validateContentType(response, CURRENT_METADATA_MEDIA_TYPE) validateCurrentMetadata(new JSONObject(response.body)) } @@ -184,6 +187,7 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra @Test void metadataWithHalAcceptHeader() { ResponseEntity response = invokeHome(null, 'application/hal+json') + assertThat(response.getHeaders().getFirst(HttpHeaders.ETAG), not(nullValue())) validateContentType(response, MainController.HAL_JSON_CONTENT_TYPE) validateCurrentMetadata(new JSONObject(response.body)) } @@ -283,6 +287,7 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra private void validateCurlHelpContent(ResponseEntity response) { validateContentType(response, MediaType.TEXT_PLAIN) + assertThat(response.getHeaders().getFirst(HttpHeaders.ETAG), not(nullValue())) assertThat(response.body, allOf( containsString("Spring Initializr"), containsString('Examples:'), @@ -291,6 +296,7 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra private void validateHttpIeHelpContent(ResponseEntity response) { validateContentType(response, MediaType.TEXT_PLAIN) + assertThat(response.getHeaders().getFirst(HttpHeaders.ETAG), not(nullValue())) assertThat(response.body, allOf( containsString("Spring Initializr"), containsString('Examples:'), @@ -300,6 +306,7 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra private void validateGenericHelpContent(ResponseEntity response) { validateContentType(response, MediaType.TEXT_PLAIN) + assertThat(response.getHeaders().getFirst(HttpHeaders.ETAG), not(nullValue())) assertThat(response.body, allOf( containsString("Spring Initializr"), not(containsString('Examples:')), @@ -308,6 +315,7 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra private void validateSpringBootHelpContent(ResponseEntity response) { validateContentType(response, MediaType.TEXT_PLAIN) + assertThat(response.getHeaders().getFirst(HttpHeaders.ETAG), not(nullValue())) assertThat(response.body, allOf( containsString("Service capabilities"), containsString("Supported dependencies"),