From 0584c289abaf850993bb69501772823296ea2f54 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 9 Jul 2020 11:26:04 +0300 Subject: [PATCH] Support for direct path lookups in WebFlux Closes gh-22961 --- .../condition/PatternsRequestCondition.java | 35 ++++++++-- .../method/AbstractHandlerMethodMapping.java | 66 ++++++++++++++++--- .../result/method/RequestMappingInfo.java | 8 +++ .../RequestMappingInfoHandlerMapping.java | 5 ++ .../method/HandlerMethodMappingTests.java | 47 +++++++++---- .../handler/AbstractHandlerMethodMapping.java | 54 +++++++-------- .../handler/HandlerMethodMappingTests.java | 45 ++++++++----- 7 files changed, 188 insertions(+), 72 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java index 0fe4803e66..251d4759a9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java @@ -19,6 +19,7 @@ package org.springframework.web.reactive.result.condition; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; @@ -45,6 +46,8 @@ public final class PatternsRequestCondition extends AbstractRequestCondition EMPTY_PATH_PATTERN = new TreeSet<>(Collections.singleton(PathPatternParser.defaultInstance.parse(""))); + private static final Set EMPTY_PATH = Collections.singleton(""); + private final SortedSet patterns; @@ -83,6 +86,28 @@ public final class PatternsRequestCondition extends AbstractRequestCondition getDirectPaths() { + if (isEmptyPathMapping()) { + return EMPTY_PATH; + } + Set result = Collections.emptySet(); + for (PathPattern pattern : this.patterns) { + if (!pattern.hasPatternSyntax()) { + result = (result.isEmpty() ? new HashSet<>(1) : result); + result.add(pattern.getPatternString()); + } + } + return result; + } + /** * Returns a new instance with URL patterns from the current instance ("this") and * the "other" instance as follows: @@ -95,13 +120,13 @@ public final class PatternsRequestCondition extends AbstractRequestCondition extends AbstractHandlerMap @Nullable protected HandlerMethod lookupHandlerMethod(ServerWebExchange exchange) throws Exception { List matches = new ArrayList<>(); - addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, exchange); - + List directPathMatches = this.mappingRegistry.getMappingsByDirectPath(exchange); + if (directPathMatches != null) { + addMatchingMappings(directPathMatches, matches, exchange); + } + if (matches.isEmpty()) { + addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, exchange); + } if (!matches.isEmpty()) { Comparator comparator = new MatchComparator(getMappingComparator(exchange)); matches.sort(comparator); @@ -412,6 +419,14 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap @Nullable protected abstract T getMappingForMethod(Method method, Class handlerType); + /** + * Return the request mapping paths that are not patterns. + * @since 5.3 + */ + protected Set getDirectPaths(T mapping) { + return Collections.emptySet(); + } + /** * Check if a mapping matches the current request and return a (potentially * new) mapping with conditions relevant to the current request. @@ -443,6 +458,8 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap private final Map mappingLookup = new LinkedHashMap<>(); + private final MultiValueMap pathLookup = new LinkedMultiValueMap<>(); + private final Map corsLookup = new ConcurrentHashMap<>(); private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); @@ -455,6 +472,17 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap return this.mappingLookup; } + /** + * Return matches for the given URL path. Not thread-safe. + * @since 5.3 + * @see #acquireReadLock() + */ + @Nullable + public List getMappingsByDirectPath(ServerWebExchange exchange) { + String path = exchange.getRequest().getPath().pathWithinApplication().value(); + return this.pathLookup.get(path); + } + /** * Return CORS configuration. Thread-safe for concurrent use. */ @@ -485,13 +513,18 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap validateMethodMapping(handlerMethod, mapping); this.mappingLookup.put(mapping, handlerMethod); + Set directPaths = AbstractHandlerMethodMapping.this.getDirectPaths(mapping); + for (String path : directPaths) { + this.pathLookup.add(path, mapping); + } + CorsConfiguration config = initCorsConfiguration(handler, method, mapping); if (config != null) { config.validateAllowCredentials(); this.corsLookup.put(handlerMethod, config); } - this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod)); + this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directPaths)); } finally { this.readWriteLock.writeLock().unlock(); @@ -512,13 +545,24 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap public void unregister(T mapping) { this.readWriteLock.writeLock().lock(); try { - MappingRegistration definition = this.registry.remove(mapping); - if (definition == null) { + MappingRegistration registration = this.registry.remove(mapping); + if (registration == null) { return; } - this.mappingLookup.remove(definition.getMapping()); - this.corsLookup.remove(definition.getHandlerMethod()); + this.mappingLookup.remove(registration.getMapping()); + + for (String path : registration.getDirectPaths()) { + List mappings = this.pathLookup.get(path); + if (mappings != null) { + mappings.remove(registration.getMapping()); + if (mappings.isEmpty()) { + this.pathLookup.remove(path); + } + } + } + + this.corsLookup.remove(registration.getHandlerMethod()); } finally { this.readWriteLock.writeLock().unlock(); @@ -533,11 +577,14 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap private final HandlerMethod handlerMethod; - public MappingRegistration(T mapping, HandlerMethod handlerMethod) { + private final Set directPaths; + + public MappingRegistration(T mapping, HandlerMethod handlerMethod, @Nullable Set directPaths) { Assert.notNull(mapping, "Mapping must not be null"); Assert.notNull(handlerMethod, "HandlerMethod must not be null"); this.mapping = mapping; this.handlerMethod = handlerMethod; + this.directPaths = (directPaths != null ? directPaths : Collections.emptySet()); } public T getMapping() { @@ -548,6 +595,9 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap return this.handlerMethod; } + public Set getDirectPaths() { + return this.directPaths; + } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java index bf5c3514d4..57c64954c4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java @@ -145,6 +145,14 @@ public final class RequestMappingInfo implements RequestCondition getDirectPaths() { + return this.patternsCondition.getDirectPaths(); + } + /** * Returns the HTTP request methods of this {@link RequestMappingInfo}; * or instance with 0 request methods, never {@code null}. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java index eaccc21222..8bb0bd16b1 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java @@ -71,6 +71,11 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe } + @Override + protected Set getDirectPaths(RequestMappingInfo info) { + return info.getDirectPaths(); + } + /** * Check if the given RequestMappingInfo matches the current request and * return a (potentially new) instance with conditions that match the diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java index 8e9aa3e41f..17cc8aa93c 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -17,7 +17,11 @@ package org.springframework.web.reactive.result.method; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; +import java.util.List; +import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -45,7 +49,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; */ public class HandlerMethodMappingTests { - private AbstractHandlerMethodMapping mapping; + private MyHandlerMethodMapping mapping; private MyHandler handler; @@ -71,17 +75,18 @@ public class HandlerMethodMappingTests { } @Test - public void directMatch() throws Exception { - String key = "foo"; - this.mapping.registerMapping(key, this.handler, this.method1); - MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(key)); + public void directMatch() { + this.mapping.registerMapping("/foo", this.handler, this.method1); + this.mapping.registerMapping("/fo*", this.handler, this.method2); + MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/foo")); Mono result = this.mapping.getHandler(exchange); assertThat(((HandlerMethod) result.block()).getMethod()).isEqualTo(this.method1); + assertThat(this.mapping.getMatches()).containsExactly("/foo"); } @Test - public void patternMatch() throws Exception { + public void patternMatch() { this.mapping.registerMapping("/fo*", this.handler, this.method1); this.mapping.registerMapping("/f*", this.handler, this.method2); @@ -91,7 +96,7 @@ public class HandlerMethodMappingTests { } @Test - public void ambiguousMatch() throws Exception { + public void ambiguousMatch() { this.mapping.registerMapping("/f?o", this.handler, this.method1); this.mapping.registerMapping("/fo?", this.handler, this.method2); MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/foo")); @@ -101,7 +106,7 @@ public class HandlerMethodMappingTests { } @Test - public void registerMapping() throws Exception { + public void registerMapping() { String key1 = "/foo"; String key2 = "/foo*"; this.mapping.registerMapping(key1, this.handler, this.method1); @@ -112,7 +117,7 @@ public class HandlerMethodMappingTests { } @Test - public void registerMappingWithSameMethodAndTwoHandlerInstances() throws Exception { + public void registerMappingWithSameMethodAndTwoHandlerInstances() { String key1 = "foo"; String key2 = "bar"; MyHandler handler1 = new MyHandler(); @@ -125,7 +130,7 @@ public class HandlerMethodMappingTests { } @Test - public void unregisterMapping() throws Exception { + public void unregisterMapping() { String key = "foo"; this.mapping.registerMapping(key, this.handler, this.method1); Mono result = this.mapping.getHandler(MockServerWebExchange.from(MockServerHttpRequest.get(key))); @@ -144,6 +149,13 @@ public class HandlerMethodMappingTests { private PathPatternParser parser = new PathPatternParser(); + private final List matches = new ArrayList<>(); + + + public List getMatches() { + return this.matches; + } + @Override protected boolean isHandler(Class beanType) { return true; @@ -155,18 +167,27 @@ public class HandlerMethodMappingTests { return methodName.startsWith("handler") ? methodName : null; } + @Override + protected Set getDirectPaths(String mapping) { + return (parser.parse(mapping).hasPatternSyntax() ? + Collections.emptySet() : Collections.singleton(mapping)); + } + @Override protected String getMatchingMapping(String pattern, ServerWebExchange exchange) { PathContainer lookupPath = exchange.getRequest().getPath().pathWithinApplication(); PathPattern parsedPattern = this.parser.parse(pattern); - return (parsedPattern.matches(lookupPath) ? pattern : null); + String match = parsedPattern.matches(lookupPath) ? pattern : null; + if (match != null) { + matches.add(match); + } + return match; } @Override protected Comparator getMappingComparator(ServerWebExchange exchange) { return (o1, o2) -> PathPattern.SPECIFICITY_COMPARATOR.compare(parser.parse(o1), parser.parse(o2)); } - } @Controller diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java index c2eb271028..03df143c94 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java @@ -381,15 +381,13 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap @Nullable protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { List matches = new ArrayList<>(); - List directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath); + List directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath); if (directPathMatches != null) { addMatchingMappings(directPathMatches, matches, request); } if (matches.isEmpty()) { - // No choice but to go through all mappings... addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request); } - if (!matches.isEmpty()) { Match bestMatch = matches.get(0); if (matches.size() > 1) { @@ -503,7 +501,9 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap * {@link #getDirectPaths(Object)} instead */ @Deprecated - protected abstract Set getMappingPathPatterns(T mapping); + protected Set getMappingPathPatterns(T mapping) { + return Collections.emptySet(); + } /** * Return the request mapping paths that are not patterns. @@ -550,7 +550,7 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap private final Map mappingLookup = new LinkedHashMap<>(); - private final MultiValueMap urlLookup = new LinkedMultiValueMap<>(); + private final MultiValueMap pathLookup = new LinkedMultiValueMap<>(); private final Map> nameLookup = new ConcurrentHashMap<>(); @@ -571,8 +571,8 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap * @see #acquireReadLock() */ @Nullable - public List getMappingsByUrl(String urlPath) { - return this.urlLookup.get(urlPath); + public List getMappingsByDirectPath(String urlPath) { + return this.pathLookup.get(urlPath); } /** @@ -619,9 +619,9 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap validateMethodMapping(handlerMethod, mapping); this.mappingLookup.put(mapping, handlerMethod); - Set directUrls = AbstractHandlerMethodMapping.this.getDirectPaths(mapping); - for (String url : directUrls) { - this.urlLookup.add(url, mapping); + Set directPaths = AbstractHandlerMethodMapping.this.getDirectPaths(mapping); + for (String path : directPaths) { + this.pathLookup.add(path, mapping); } String name = null; @@ -636,7 +636,7 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap this.corsLookup.put(handlerMethod, config); } - this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name)); + this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directPaths, name)); } finally { this.readWriteLock.writeLock().unlock(); @@ -675,26 +675,26 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap public void unregister(T mapping) { this.readWriteLock.writeLock().lock(); try { - MappingRegistration definition = this.registry.remove(mapping); - if (definition == null) { + MappingRegistration registration = this.registry.remove(mapping); + if (registration == null) { return; } - this.mappingLookup.remove(definition.getMapping()); + this.mappingLookup.remove(registration.getMapping()); - for (String url : definition.getDirectUrls()) { - List list = this.urlLookup.get(url); - if (list != null) { - list.remove(definition.getMapping()); - if (list.isEmpty()) { - this.urlLookup.remove(url); + for (String path : registration.getDirectPaths()) { + List mappings = this.pathLookup.get(path); + if (mappings != null) { + mappings.remove(registration.getMapping()); + if (mappings.isEmpty()) { + this.pathLookup.remove(path); } } } - removeMappingName(definition); + removeMappingName(registration); - this.corsLookup.remove(definition.getHandlerMethod()); + this.corsLookup.remove(registration.getHandlerMethod()); } finally { this.readWriteLock.writeLock().unlock(); @@ -732,19 +732,19 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap private final HandlerMethod handlerMethod; - private final Set directUrls; + private final Set directPaths; @Nullable private final String mappingName; public MappingRegistration(T mapping, HandlerMethod handlerMethod, - @Nullable Set directUrls, @Nullable String mappingName) { + @Nullable Set directPaths, @Nullable String mappingName) { Assert.notNull(mapping, "Mapping must not be null"); Assert.notNull(handlerMethod, "HandlerMethod must not be null"); this.mapping = mapping; this.handlerMethod = handlerMethod; - this.directUrls = (directUrls != null ? directUrls : Collections.emptySet()); + this.directPaths = (directPaths != null ? directPaths : Collections.emptySet()); this.mappingName = mappingName; } @@ -756,8 +756,8 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap return this.handlerMethod; } - public Set getDirectUrls() { - return this.directUrls; + public Set getDirectPaths() { + return this.directPaths; } @Nullable diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java index 4935fd8432..33f68e6452 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java @@ -17,6 +17,7 @@ package org.springframework.web.servlet.handler; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -51,7 +52,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @SuppressWarnings("unused") public class HandlerMethodMappingTests { - private AbstractHandlerMethodMapping mapping; + private MyHandlerMethodMapping mapping; private MyHandler handler; @@ -78,13 +79,15 @@ public class HandlerMethodMappingTests { @Test public void directMatch() throws Exception { - String key = "foo"; - this.mapping.registerMapping(key, this.handler, this.method1); + this.mapping.registerMapping("/foo", this.handler, this.method1); + this.mapping.registerMapping("/fo*", this.handler, this.method2); - MockHttpServletRequest request = new MockHttpServletRequest("GET", key); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); HandlerMethod result = this.mapping.getHandlerInternal(request); + assertThat(result.getMethod()).isEqualTo(method1); assertThat(request.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE)).isEqualTo(result); + assertThat(this.mapping.getMatches()).containsExactly("/foo"); } @Test @@ -99,7 +102,7 @@ public class HandlerMethodMappingTests { } @Test - public void ambiguousMatch() throws Exception { + public void ambiguousMatch() { this.mapping.registerMapping("/f?o", this.handler, this.method1); this.mapping.registerMapping("/fo?", this.handler, this.method2); @@ -127,7 +130,7 @@ public class HandlerMethodMappingTests { } @Test - public void registerMapping() throws Exception { + public void registerMapping() { String key1 = "/foo"; String key2 = "/foo*"; @@ -136,7 +139,7 @@ public class HandlerMethodMappingTests { // Direct URL lookup - List directUrlMatches = this.mapping.getMappingRegistry().getMappingsByUrl(key1); + List directUrlMatches = this.mapping.getMappingRegistry().getMappingsByDirectPath(key1); assertThat(directUrlMatches).isNotNull(); assertThat(directUrlMatches.size()).isEqualTo(1); assertThat(directUrlMatches.get(0)).isEqualTo(key1); @@ -170,7 +173,7 @@ public class HandlerMethodMappingTests { } @Test - public void registerMappingWithSameMethodAndTwoHandlerInstances() throws Exception { + public void registerMappingWithSameMethodAndTwoHandlerInstances() { String key1 = "foo"; String key2 = "bar"; @@ -186,7 +189,7 @@ public class HandlerMethodMappingTests { // Direct URL lookup - List directUrlMatches = this.mapping.getMappingRegistry().getMappingsByUrl(key1); + List directUrlMatches = this.mapping.getMappingRegistry().getMappingsByDirectPath(key1); assertThat(directUrlMatches).isNotNull(); assertThat(directUrlMatches.size()).isEqualTo(1); assertThat(directUrlMatches.get(0)).isEqualTo(key1); @@ -222,7 +225,7 @@ public class HandlerMethodMappingTests { this.mapping.unregisterMapping(key); assertThat(mapping.getHandlerInternal(new MockHttpServletRequest("GET", key))).isNull(); - assertThat(this.mapping.getMappingRegistry().getMappingsByUrl(key)).isNull(); + assertThat(this.mapping.getMappingRegistry().getMappingsByDirectPath(key)).isNull(); assertThat(this.mapping.getMappingRegistry().getHandlerMethodsByMappingName(this.method1.getName())).isNull(); assertThat(this.mapping.getMappingRegistry().getCorsConfiguration(handlerMethod)).isNull(); } @@ -253,26 +256,30 @@ public class HandlerMethodMappingTests { private PathMatcher pathMatcher = new AntPathMatcher(); + private final List matches = new ArrayList<>(); public MyHandlerMethodMapping() { setHandlerMethodMappingNamingStrategy(new SimpleMappingNamingStrategy()); } + public List getMatches() { + return this.matches; + } + @Override protected boolean isHandler(Class beanType) { return true; } @Override - protected String getMappingForMethod(Method method, Class handlerType) { - String methodName = method.getName(); - return methodName.startsWith("handler") ? methodName : null; + protected Set getDirectPaths(String mapping) { + return (pathMatcher.isPattern(mapping) ? Collections.emptySet() : Collections.singleton(mapping)); } @Override - @SuppressWarnings("deprecation") - protected Set getMappingPathPatterns(String key) { - return (this.pathMatcher.isPattern(key) ? Collections.emptySet() : Collections.singleton(key)); + protected String getMappingForMethod(Method method, Class handlerType) { + String methodName = method.getName(); + return methodName.startsWith("handler") ? methodName : null; } @Override @@ -285,7 +292,11 @@ public class HandlerMethodMappingTests { @Override protected String getMatchingMapping(String pattern, HttpServletRequest request) { String lookupPath = this.pathHelper.getLookupPathForRequest(request); - return this.pathMatcher.match(pattern, lookupPath) ? pattern : null; + String match = (this.pathMatcher.match(pattern, lookupPath) ? pattern : null); + if (match != null) { + this.matches.add(match); + } + return match; } @Override