diff --git a/src/main/java/org/springframework/hateoas/core/CachingMappingDiscoverer.java b/src/main/java/org/springframework/hateoas/core/CachingMappingDiscoverer.java new file mode 100644 index 00000000..f060f0b2 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/core/CachingMappingDiscoverer.java @@ -0,0 +1,100 @@ +/* + * 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.hateoas.core; + +import lombok.RequiredArgsConstructor; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Map; + +import org.springframework.http.HttpMethod; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.StringUtils; + +/** + * Caching adapter of {@link MappingDiscoverer}. + * + * @author Michal Stochmialek + * @author Oliver Drotbohm + */ +@RequiredArgsConstructor(staticName = "of") +public class CachingMappingDiscoverer implements MappingDiscoverer { + + private static final Map MAPPINGS = new ConcurrentReferenceHashMap<>(); + private static final Map> METHODS = new ConcurrentReferenceHashMap<>(); + + private final MappingDiscoverer delegate; + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.core.MappingDiscoverer#getMapping(java.lang.Class) + */ + @Override + public String getMapping(Class type) { + + String key = key(type, null); + + return MAPPINGS.computeIfAbsent(key, __ -> delegate.getMapping(type)); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.core.MappingDiscoverer#getMapping(java.lang.reflect.Method) + */ + @Override + public String getMapping(Method method) { + + String key = key(method.getDeclaringClass(), method); + + return MAPPINGS.computeIfAbsent(key, __ -> delegate.getMapping(method)); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.core.MappingDiscoverer#getMapping(java.lang.Class, java.lang.reflect.Method) + */ + @Override + public String getMapping(Class type, Method method) { + + String key = key(type, method); + + return MAPPINGS.computeIfAbsent(key, __ -> delegate.getMapping(type, method)); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.core.MappingDiscoverer#getRequestMethod(java.lang.Class, java.lang.reflect.Method) + */ + @Override + public Collection getRequestMethod(Class type, Method method) { + return METHODS.computeIfAbsent(key(type, method), __ -> delegate.getRequestMethod(type, method)); + } + + private static String key(Class type, Method method) { + + StringBuilder builder = new StringBuilder(type.getName()); + + if (method == null) { + return builder.toString(); + } + + builder.append(method.getName()); + builder.append(StringUtils.arrayToCommaDelimitedString(method.getParameterTypes())); + + return builder.toString(); + } +} diff --git a/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java index 5148a1db..f4e49c1f 100755 --- a/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java +++ b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java @@ -15,31 +15,23 @@ */ package org.springframework.hateoas.mvc; -import lombok.RequiredArgsConstructor; -import lombok.experimental.Delegate; - import java.lang.reflect.Method; import java.net.URI; import java.util.Collection; import java.util.Map; -import javax.servlet.http.HttpServletRequest; - import org.springframework.hateoas.Affordance; import org.springframework.hateoas.Link; import org.springframework.hateoas.TemplateVariables; import org.springframework.hateoas.core.AnnotationMappingDiscoverer; +import org.springframework.hateoas.core.CachingMappingDiscoverer; import org.springframework.hateoas.core.DummyInvocationUtils; import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation; import org.springframework.hateoas.core.LinkBuilderSupport; import org.springframework.hateoas.core.MappingDiscoverer; import org.springframework.util.Assert; -import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; -import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.util.DefaultUriTemplateHandler; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; @@ -58,9 +50,8 @@ import org.springframework.web.util.UriTemplate; */ public class ControllerLinkBuilder extends LinkBuilderSupport { - private static final String REQUEST_ATTRIBUTES_MISSING = "Could not find current request via RequestContextHolder. Is this being called from a Spring MVC handler?"; - private static final CachingAnnotationMappingDiscoverer DISCOVERER = new CachingAnnotationMappingDiscoverer( - new AnnotationMappingDiscoverer(RequestMapping.class)); + private static final MappingDiscoverer DISCOVERER = CachingMappingDiscoverer + .of(new AnnotationMappingDiscoverer(RequestMapping.class)); private static final ControllerLinkBuilderFactory FACTORY = new ControllerLinkBuilderFactory(); private static final CustomUriTemplateHandler HANDLER = new CustomUriTemplateHandler(); @@ -164,7 +155,7 @@ public class ControllerLinkBuilder extends LinkBuilderSupport templates = new ConcurrentReferenceHashMap<>(); - - public UriTemplate getMappingAsUriTemplate(Class type, Method method) { - - String mapping = delegate.getMapping(type, method); - return templates.computeIfAbsent(mapping, UriTemplate::new); - } - } - private static class CustomUriTemplateHandler extends DefaultUriTemplateHandler { public CustomUriTemplateHandler() { diff --git a/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactory.java b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactory.java index 11806823..6aa2471e 100644 --- a/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactory.java +++ b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactory.java @@ -37,6 +37,7 @@ import org.springframework.hateoas.TemplateVariable; import org.springframework.hateoas.TemplateVariables; import org.springframework.hateoas.core.AnnotationAttribute; import org.springframework.hateoas.core.AnnotationMappingDiscoverer; +import org.springframework.hateoas.core.CachingMappingDiscoverer; import org.springframework.hateoas.core.DummyInvocationUtils.LastInvocationAware; import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation; import org.springframework.hateoas.core.LinkBuilderSupport; @@ -69,7 +70,8 @@ import org.springframework.web.util.UriTemplate; */ public class ControllerLinkBuilderFactory implements MethodLinkBuilderFactory { - private static final MappingDiscoverer DISCOVERER = new AnnotationMappingDiscoverer(RequestMapping.class); + private static final MappingDiscoverer DISCOVERER = CachingMappingDiscoverer + .of(new AnnotationMappingDiscoverer(RequestMapping.class)); private static final AnnotatedParametersParameterAccessor PATH_VARIABLE_ACCESSOR = new AnnotatedParametersParameterAccessor( new AnnotationAttribute(PathVariable.class)); private static final AnnotatedParametersParameterAccessor REQUEST_PARAM_ACCESSOR = new RequestParamParameterAccessor(); @@ -141,7 +143,7 @@ public class ControllerLinkBuilderFactory implements MethodLinkBuilderFactory values = new HashMap<>(); Iterator names = template.getVariableNames().iterator(); diff --git a/src/main/java/org/springframework/hateoas/mvc/UriComponentsBuilderFactory.java b/src/main/java/org/springframework/hateoas/mvc/UriComponentsBuilderFactory.java new file mode 100644 index 00000000..a1921583 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mvc/UriComponentsBuilderFactory.java @@ -0,0 +1,98 @@ +/* + * 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.hateoas.mvc; + +import java.net.URI; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.util.Assert; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * Factory class for {@link UriComponentsBuilder} instances caching the lookups to avoid unnecessary subsequent lookups. + * + * @author Michal Stochmialek + * @author Oliver Gierke + */ +class UriComponentsBuilderFactory { + + static final String REQUEST_ATTRIBUTES_MISSING = "Could not find current request via RequestContextHolder. Is this being called from a Spring MVC handler?"; + private static final String CACHE_KEY = ControllerLinkBuilder.class.getName() + "#BUILDER_CACHE"; + + /** + * Returns a {@link UriComponentsBuilder} obtained from the current servlet mapping with scheme tweaked in case the + * request contains an {@code X-Forwarded-Ssl} header, which is not (yet) supported by the underlying + * {@link UriComponentsBuilder}. If no {@link RequestContextHolder} exists (you're outside a Spring Web call), fall + * back to relative URIs. + * + * @return + */ + public static UriComponentsBuilder getBuilder() { + + if (RequestContextHolder.getRequestAttributes() == null) { + return UriComponentsBuilder.fromPath("/"); + } + + URI baseUri = getCachedBaseUri(); + + return baseUri != null // + ? UriComponentsBuilder.fromUri(baseUri) // + : cacheBaseUri(ServletUriComponentsBuilder.fromServletMapping(getCurrentRequest())); + } + + /** + * Copy of {@link ServletUriComponentsBuilder#getCurrentRequest()} until SPR-10110 gets fixed. + * + * @return + */ + private static HttpServletRequest getCurrentRequest() { + + RequestAttributes requestAttributes = getRequestAttributes(); + HttpServletRequest servletRequest = ((ServletRequestAttributes) requestAttributes).getRequest(); + + Assert.state(servletRequest != null, "Could not find current HttpServletRequest"); + + return servletRequest; + } + + private static RequestAttributes getRequestAttributes() { + + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + + Assert.state(requestAttributes != null, REQUEST_ATTRIBUTES_MISSING); + Assert.isInstanceOf(ServletRequestAttributes.class, requestAttributes); + + return requestAttributes; + } + + private static UriComponentsBuilder cacheBaseUri(UriComponentsBuilder builder) { + + URI uri = builder.build().toUri(); + + getRequestAttributes().setAttribute(CACHE_KEY, uri, RequestAttributes.SCOPE_REQUEST); + + return builder; + } + + private static URI getCachedBaseUri() { + return (URI) getRequestAttributes().getAttribute(CACHE_KEY, RequestAttributes.SCOPE_REQUEST); + } +} diff --git a/src/main/java/org/springframework/hateoas/mvc/UriTemplateFactory.java b/src/main/java/org/springframework/hateoas/mvc/UriTemplateFactory.java new file mode 100644 index 00000000..80eb3959 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mvc/UriTemplateFactory.java @@ -0,0 +1,46 @@ +/* + * 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.hateoas.mvc; + +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.web.util.UriTemplate; + +/** + * Builds and caches {@link UriTemplate} instances. + * + * @author Michal Stochmialek + * @author Oliver Drotbohm + */ +class UriTemplateFactory { + + private static final Map CACHE = new ConcurrentReferenceHashMap(); + + /** + * Returns the the {@link UriTemplate} for the given mapping. + * + * @param mapping must not be {@literal null} or empty. + * @return + */ + public static UriTemplate templateFor(String mapping) { + + Assert.hasText(mapping, "Mapping must not be null or empty!"); + + return CACHE.computeIfAbsent(mapping, UriTemplate::new); + } +}