diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/MappedResource.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/MappedResource.java index 04b4bf8fb50..7ea9e6b2c57 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/MappedResource.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/MappedResource.java @@ -13,11 +13,13 @@ package org.eclipse.jetty.http.pathmap; +import java.util.Map; + import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; @ManagedObject("Mapped Resource") -public class MappedResource implements Comparable> +public class MappedResource implements Comparable>, Map.Entry { private final PathSpec pathSpec; private final E resource; @@ -90,6 +92,24 @@ public class MappedResource implements Comparable> return true; } + @Override + public PathSpec getKey() + { + return getPathSpec(); + } + + @Override + public E getValue() + { + return getResource(); + } + + @Override + public E setValue(E value) + { + throw new UnsupportedOperationException(); + } + @ManagedAttribute(value = "path spec", readonly = true) public PathSpec getPathSpec() { diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/PathMappings.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/PathMappings.java index ea7f0c45166..e97f9273782 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/PathMappings.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/PathMappings.java @@ -14,9 +14,9 @@ package org.eclipse.jetty.http.pathmap; import java.io.IOException; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -41,10 +41,11 @@ import org.slf4j.LoggerFactory; * @param the type of mapping endpoint */ @ManagedObject("Path Mappings") -public class PathMappings implements Iterable>, Dumpable +public class PathMappings extends AbstractMap implements Iterable>, Dumpable { private static final Logger LOG = LoggerFactory.getLogger(PathMappings.class); - private final Set> _mappings = new TreeSet<>(Comparator.comparing(MappedResource::getPathSpec)); + + private final Set> _mappings = new TreeSet<>(Map.Entry.comparingByKey()); /** * When _orderIsSignificant is true, the order of the MappedResources is significant and a match needs to be iteratively @@ -67,6 +68,14 @@ public class PathMappings implements Iterable>, Dumpable private MappedResource _servletRoot; private MappedResource _servletDefault; + @Override + public Set> entrySet() + { + @SuppressWarnings("unchecked") + Set> entries = (Set>)(Set>)_mappings; + return entries; + } + @Override public String dump() { @@ -108,9 +117,9 @@ public class PathMappings implements Iterable>, Dumpable return _mappings.stream(); } - public void removeIf(Predicate> predicate) + public boolean removeIf(Predicate> predicate) { - _mappings.removeIf(predicate); + return _mappings.removeIf(predicate); } /** @@ -352,130 +361,145 @@ public class PathMappings implements Iterable>, Dumpable return _mappings.iterator(); } - public E get(PathSpec spec) + @Override + public E get(Object key) { - return _mappings.stream() - .filter(mappedResource -> mappedResource.getPathSpec().equals(spec)) - .map(MappedResource::getResource) - .findFirst() - .orElse(null); + return key instanceof PathSpec pathSpec ? get(pathSpec) : null; } - public boolean put(String pathSpecString, E resource) + public E get(PathSpec pathSpec) + { + if (pathSpec == null) + return null; + + for (MappedResource mr : _mappings) + { + if (pathSpec.equals(mr.getKey())) + return mr.getValue(); + } + return null; + } + + public E put(String pathSpecString, E resource) { return put(PathSpec.from(pathSpecString), resource); } - public boolean put(PathSpec pathSpec, E resource) + @Override + public E put(PathSpec pathSpec, E resource) { + E old = remove(pathSpec); MappedResource entry = new MappedResource<>(pathSpec, resource); - boolean added = _mappings.add(entry); + _mappings.add(entry); if (LOG.isDebugEnabled()) - LOG.debug("{} {} to {}", added ? "Added" : "Ignored", entry, this); + LOG.debug("Added {} replacing {} to {}", entry, old, this); - if (added) + switch (pathSpec.getGroup()) { - switch (pathSpec.getGroup()) - { - case EXACT: - if (pathSpec instanceof ServletPathSpec) - { - String exact = pathSpec.getDeclaration(); - if (exact != null) - _exactMap.put(exact, entry); - } - else - { - // This is not a Servlet mapping, turn off optimization on Exact - // TODO: see if we can optimize all Regex / UriTemplate versions here too. - // Note: Example exact in Regex that can cause problems `^/a\Q/b\E/` (which is only ever matching `/a/b/`) - // Note: UriTemplate can handle exact easily enough - _optimizedExact = false; - _orderIsSignificant = true; - } - break; - case PREFIX_GLOB: - if (pathSpec instanceof ServletPathSpec) - { - String prefix = pathSpec.getPrefix(); - if (prefix != null) - _prefixMap.put(prefix, entry); - } - else - { - // This is not a Servlet mapping, turn off optimization on Prefix - // TODO: see if we can optimize all Regex / UriTemplate versions here too. - // Note: Example Prefix in Regex that can cause problems `^/a/b+` or `^/a/bb*` ('b' one or more times) - // Note: Example Prefix in UriTemplate that might cause problems `/a/{b}/{c}` - _optimizedPrefix = false; - _orderIsSignificant = true; - } - break; - case SUFFIX_GLOB: - if (pathSpec instanceof ServletPathSpec) - { - String suffix = pathSpec.getSuffix(); - if (suffix != null) - _suffixMap.put(suffix, entry); - } - else - { - // This is not a Servlet mapping, turn off optimization on Suffix - // TODO: see if we can optimize all Regex / UriTemplate versions here too. - // Note: Example suffix in Regex that can cause problems `^.*/path/name.ext` or `^/a/.*(ending)` - // Note: Example suffix in UriTemplate that can cause problems `/{a}/name.ext` - _optimizedSuffix = false; - _orderIsSignificant = true; - } - break; - case ROOT: - if (pathSpec instanceof ServletPathSpec) - { - if (_servletRoot == null) - _servletRoot = entry; - } - else - { - _orderIsSignificant = true; - } - break; - case DEFAULT: - if (pathSpec instanceof ServletPathSpec) - { - if (_servletDefault == null) - _servletDefault = entry; - } - else - { - _orderIsSignificant = true; - } - break; - default: - } + case EXACT: + if (pathSpec instanceof ServletPathSpec) + { + String exact = pathSpec.getDeclaration(); + if (exact != null) + _exactMap.put(exact, entry); + } + else + { + // This is not a Servlet mapping, turn off optimization on Exact + // TODO: see if we can optimize all Regex / UriTemplate versions here too. + // Note: Example exact in Regex that can cause problems `^/a\Q/b\E/` (which is only ever matching `/a/b/`) + // Note: UriTemplate can handle exact easily enough + _optimizedExact = false; + _orderIsSignificant = true; + } + break; + case PREFIX_GLOB: + if (pathSpec instanceof ServletPathSpec) + { + String prefix = pathSpec.getPrefix(); + if (prefix != null) + _prefixMap.put(prefix, entry); + } + else + { + // This is not a Servlet mapping, turn off optimization on Prefix + // TODO: see if we can optimize all Regex / UriTemplate versions here too. + // Note: Example Prefix in Regex that can cause problems `^/a/b+` or `^/a/bb*` ('b' one or more times) + // Note: Example Prefix in UriTemplate that might cause problems `/a/{b}/{c}` + _optimizedPrefix = false; + _orderIsSignificant = true; + } + break; + case SUFFIX_GLOB: + if (pathSpec instanceof ServletPathSpec) + { + String suffix = pathSpec.getSuffix(); + if (suffix != null) + _suffixMap.put(suffix, entry); + } + else + { + // This is not a Servlet mapping, turn off optimization on Suffix + // TODO: see if we can optimize all Regex / UriTemplate versions here too. + // Note: Example suffix in Regex that can cause problems `^.*/path/name.ext` or `^/a/.*(ending)` + // Note: Example suffix in UriTemplate that can cause problems `/{a}/name.ext` + _optimizedSuffix = false; + _orderIsSignificant = true; + } + break; + case ROOT: + if (pathSpec instanceof ServletPathSpec) + { + if (_servletRoot == null) + _servletRoot = entry; + } + else + { + _orderIsSignificant = true; + } + break; + case DEFAULT: + if (pathSpec instanceof ServletPathSpec) + { + if (_servletDefault == null) + _servletDefault = entry; + } + else + { + _orderIsSignificant = true; + } + break; + default: } - return added; + return old; } - @SuppressWarnings("incomplete-switch") - public boolean remove(PathSpec pathSpec) + @Override + public E remove(Object key) + { + return key instanceof PathSpec pathSpec ? remove(pathSpec) : null; + } + + public E remove(PathSpec pathSpec) { Iterator> iter = _mappings.iterator(); - boolean removed = false; + E removed = null; while (iter.hasNext()) { - if (iter.next().getPathSpec().equals(pathSpec)) + MappedResource entry = iter.next(); + if (entry.getPathSpec().equals(pathSpec)) { - removed = true; + removed = entry.getResource(); iter.remove(); break; } } if (LOG.isDebugEnabled()) - LOG.debug("{} {} to {}", removed ? "Removed" : "Ignored", pathSpec, this); + LOG.debug("Removed {} at {} from {}", removed, pathSpec, this); - if (removed) + if (removed != null) { switch (pathSpec.getGroup()) { diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/PathSpecSet.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/PathSpecSet.java index bd6b51ff38c..b379f5a8d05 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/PathSpecSet.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/PathSpecSet.java @@ -56,13 +56,13 @@ public class PathSpecSet extends AbstractSet implements Predicate p = new PathMappings<>(); - assertThat(p.put(new UriTemplatePathSpec("/a/{var1}/c"), "resourceA"), is(true)); - assertThat(p.put(new UriTemplatePathSpec("/a/{var2}/c"), "resourceAA"), is(false)); - assertThat(p.put(new UriTemplatePathSpec("/a/b/c"), "resourceB"), is(true)); - assertThat(p.put(new UriTemplatePathSpec("/a/b/c"), "resourceBB"), is(false)); - assertThat(p.put(new ServletPathSpec("/a/b/c"), "resourceBB"), is(true)); - assertThat(p.put(new RegexPathSpec("/a/b/c"), "resourceBB"), is(true)); + assertThat(p.put(new UriTemplatePathSpec("/a/{var2}/c"), "resourceA"), nullValue()); + assertThat(p.put(new UriTemplatePathSpec("/a/{var1}/c"), "resourceAA"), is("resourceA")); + assertThat(p.put(new UriTemplatePathSpec("/a/b/c"), "resourceB"), nullValue()); + assertThat(p.put(new UriTemplatePathSpec("/a/b/c"), "resourceBB"), is("resourceB")); + assertThat(p.put(new ServletPathSpec("/a/b/c"), "resourceBB"), nullValue()); + assertThat(p.put(new RegexPathSpec("/a/b/c"), "resourceBB"), nullValue()); - assertThat(p.put(new ServletPathSpec("/*"), "resourceC"), is(true)); - assertThat(p.put(new RegexPathSpec("/(.*)"), "resourceCC"), is(true)); + assertThat(p.put(new ServletPathSpec("/*"), "resourceC"), nullValue()); + assertThat(p.put(new RegexPathSpec("/(.*)"), "resourceCC"), nullValue()); } @Test @@ -373,27 +375,27 @@ public class PathMappingsTest PathMappings p = new PathMappings<>(); p.put(new UriTemplatePathSpec("/a/{var1}/c"), "resourceA"); - assertThat(p.remove(new UriTemplatePathSpec("/a/{var1}/c")), is(true)); + assertThat(p.remove(new UriTemplatePathSpec("/a/{var1}/c")), is("resourceA")); p.put(new UriTemplatePathSpec("/a/{var1}/c"), "resourceA"); - assertThat(p.remove(new UriTemplatePathSpec("/a/b/c")), is(false)); - assertThat(p.remove(new UriTemplatePathSpec("/a/{b}/c")), is(true)); - assertThat(p.remove(new UriTemplatePathSpec("/a/{b}/c")), is(false)); + assertThat(p.remove(new UriTemplatePathSpec("/a/b/c")), nullValue()); + assertThat(p.remove(new UriTemplatePathSpec("/a/{b}/c")), is("resourceA")); + assertThat(p.remove(new UriTemplatePathSpec("/a/{b}/c")), nullValue()); p.put(new UriTemplatePathSpec("/{var1}/b/c"), "resourceA"); - assertThat(p.remove(new UriTemplatePathSpec("/a/b/c")), is(false)); - assertThat(p.remove(new UriTemplatePathSpec("/{a}/b/c")), is(true)); - assertThat(p.remove(new UriTemplatePathSpec("/{a}/b/c")), is(false)); + assertThat(p.remove(new UriTemplatePathSpec("/a/b/c")), nullValue()); + assertThat(p.remove(new UriTemplatePathSpec("/{a}/b/c")), is("resourceA")); + assertThat(p.remove(new UriTemplatePathSpec("/{a}/b/c")), nullValue()); p.put(new UriTemplatePathSpec("/a/b/{var1}"), "resourceA"); - assertThat(p.remove(new UriTemplatePathSpec("/a/b/c")), is(false)); - assertThat(p.remove(new UriTemplatePathSpec("/a/b/{c}")), is(true)); - assertThat(p.remove(new UriTemplatePathSpec("/a/b/{c}")), is(false)); + assertThat(p.remove(new UriTemplatePathSpec("/a/b/c")), nullValue()); + assertThat(p.remove(new UriTemplatePathSpec("/a/b/{c}")), is("resourceA")); + assertThat(p.remove(new UriTemplatePathSpec("/a/b/{c}")), nullValue()); p.put(new UriTemplatePathSpec("/{var1}/{var2}/{var3}"), "resourceA"); - assertThat(p.remove(new UriTemplatePathSpec("/a/b/c")), is(false)); - assertThat(p.remove(new UriTemplatePathSpec("/{a}/{b}/{c}")), is(true)); - assertThat(p.remove(new UriTemplatePathSpec("/{a}/{b}/{c}")), is(false)); + assertThat(p.remove(new UriTemplatePathSpec("/a/b/c")), nullValue()); + assertThat(p.remove(new UriTemplatePathSpec("/{a}/{b}/{c}")), is("resourceA")); + assertThat(p.remove(new UriTemplatePathSpec("/{a}/{b}/{c}")), nullValue()); } @Test @@ -402,24 +404,24 @@ public class PathMappingsTest PathMappings p = new PathMappings<>(); p.put(new RegexPathSpec("/a/(.*)/c"), "resourceA"); - assertThat(p.remove(new RegexPathSpec("/a/b/c")), is(false)); - assertThat(p.remove(new RegexPathSpec("/a/(.*)/c")), is(true)); - assertThat(p.remove(new RegexPathSpec("/a/(.*)/c")), is(false)); + assertThat(p.remove(new RegexPathSpec("/a/b/c")), nullValue()); + assertThat(p.remove(new RegexPathSpec("/a/(.*)/c")), is("resourceA")); + assertThat(p.remove(new RegexPathSpec("/a/(.*)/c")), nullValue()); p.put(new RegexPathSpec("/(.*)/b/c"), "resourceA"); - assertThat(p.remove(new RegexPathSpec("/a/b/c")), is(false)); - assertThat(p.remove(new RegexPathSpec("/(.*)/b/c")), is(true)); - assertThat(p.remove(new RegexPathSpec("/(.*)/b/c")), is(false)); + assertThat(p.remove(new RegexPathSpec("/a/b/c")), nullValue()); + assertThat(p.remove(new RegexPathSpec("/(.*)/b/c")), is("resourceA")); + assertThat(p.remove(new RegexPathSpec("/(.*)/b/c")), nullValue()); p.put(new RegexPathSpec("/a/b/(.*)"), "resourceA"); - assertThat(p.remove(new RegexPathSpec("/a/b/c")), is(false)); - assertThat(p.remove(new RegexPathSpec("/a/b/(.*)")), is(true)); - assertThat(p.remove(new RegexPathSpec("/a/b/(.*)")), is(false)); + assertThat(p.remove(new RegexPathSpec("/a/b/c")), nullValue()); + assertThat(p.remove(new RegexPathSpec("/a/b/(.*)")), is("resourceA")); + assertThat(p.remove(new RegexPathSpec("/a/b/(.*)")), nullValue()); p.put(new RegexPathSpec("/a/b/c"), "resourceA"); - assertThat(p.remove(new RegexPathSpec("/a/b/d")), is(false)); - assertThat(p.remove(new RegexPathSpec("/a/b/c")), is(true)); - assertThat(p.remove(new RegexPathSpec("/a/b/c")), is(false)); + assertThat(p.remove(new RegexPathSpec("/a/b/d")), nullValue()); + assertThat(p.remove(new RegexPathSpec("/a/b/c")), is("resourceA")); + assertThat(p.remove(new RegexPathSpec("/a/b/c")), nullValue()); } @Test @@ -428,34 +430,34 @@ public class PathMappingsTest PathMappings p = new PathMappings<>(); p.put(new ServletPathSpec("/a/*"), "resourceA"); - assertThat(p.remove(new ServletPathSpec("/a/b")), is(false)); - assertThat(p.remove(new ServletPathSpec("/a/*")), is(true)); - assertThat(p.remove(new ServletPathSpec("/a/*")), is(false)); + assertThat(p.remove(new ServletPathSpec("/a/b")), nullValue()); + assertThat(p.remove(new ServletPathSpec("/a/*")), is("resourceA")); + assertThat(p.remove(new ServletPathSpec("/a/*")), nullValue()); p.put(new ServletPathSpec("/a/b/*"), "resourceA"); - assertThat(p.remove(new ServletPathSpec("/a/b/c")), is(false)); - assertThat(p.remove(new ServletPathSpec("/a/b/*")), is(true)); - assertThat(p.remove(new ServletPathSpec("/a/b/*")), is(false)); + assertThat(p.remove(new ServletPathSpec("/a/b/c")), nullValue()); + assertThat(p.remove(new ServletPathSpec("/a/b/*")), is("resourceA")); + assertThat(p.remove(new ServletPathSpec("/a/b/*")), nullValue()); p.put(new ServletPathSpec("*.do"), "resourceA"); - assertThat(p.remove(new ServletPathSpec("*.gz")), is(false)); - assertThat(p.remove(new ServletPathSpec("*.do")), is(true)); - assertThat(p.remove(new ServletPathSpec("*.do")), is(false)); + assertThat(p.remove(new ServletPathSpec("*.gz")), nullValue()); + assertThat(p.remove(new ServletPathSpec("*.do")), is("resourceA")); + assertThat(p.remove(new ServletPathSpec("*.do")), nullValue()); p.put(new ServletPathSpec("/"), "resourceA"); - assertThat(p.remove(new ServletPathSpec("/a")), is(false)); - assertThat(p.remove(new ServletPathSpec("/")), is(true)); - assertThat(p.remove(new ServletPathSpec("/")), is(false)); + assertThat(p.remove(new ServletPathSpec("/a")), nullValue()); + assertThat(p.remove(new ServletPathSpec("/")), is("resourceA")); + assertThat(p.remove(new ServletPathSpec("/")), nullValue()); p.put(new ServletPathSpec(""), "resourceA"); - assertThat(p.remove(new ServletPathSpec("/")), is(false)); - assertThat(p.remove(new ServletPathSpec("")), is(true)); - assertThat(p.remove(new ServletPathSpec("")), is(false)); + assertThat(p.remove(new ServletPathSpec("/")), nullValue()); + assertThat(p.remove(new ServletPathSpec("")), is("resourceA")); + assertThat(p.remove(new ServletPathSpec("")), nullValue()); p.put(new ServletPathSpec("/a/b/c"), "resourceA"); - assertThat(p.remove(new ServletPathSpec("/a/b/d")), is(false)); - assertThat(p.remove(new ServletPathSpec("/a/b/c")), is(true)); - assertThat(p.remove(new ServletPathSpec("/a/b/c")), is(false)); + assertThat(p.remove(new ServletPathSpec("/a/b/d")), nullValue()); + assertThat(p.remove(new ServletPathSpec("/a/b/c")), is("resourceA")); + assertThat(p.remove(new ServletPathSpec("/a/b/c")), nullValue()); } @Test @@ -510,6 +512,37 @@ public class PathMappingsTest assertThat(p.getMatched("/foo.do").getResource(), equalTo("resourceS")); assertThat(p.getMatched("/a/foo.do").getResource(), equalTo("resourceP")); assertThat(p.getMatched("/b/foo.do").getResource(), equalTo("resourceS")); - } + + @Test + public void testAsMap() + { + PathMappings p = new PathMappings<>(); + p.put(new ServletPathSpec(""), "resourceR"); + p.put(new ServletPathSpec("/"), "resourceD"); + p.put(new ServletPathSpec("/exact"), "REPLACED"); + p.put(new ServletPathSpec("/exact"), "resourceE"); + p.put(new ServletPathSpec("/a/*"), "resourceP"); + p.put(new ServletPathSpec("*.do"), "resourceS"); + + @SuppressWarnings("redundant") + Map map = p; + + assertThat(map.keySet(), containsInAnyOrder( + new ServletPathSpec(""), + new ServletPathSpec("/"), + new ServletPathSpec("/exact"), + new ServletPathSpec("/a/*"), + new ServletPathSpec("*.do") + )); + + assertThat(map.values(), containsInAnyOrder( + "resourceR", + "resourceD", + "resourceE", + "resourceP", + "resourceS" + )); + } + } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/PathMappingsHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/PathMappingsHandler.java index 9bf81190435..8304503bcf7 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/PathMappingsHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/PathMappingsHandler.java @@ -73,22 +73,17 @@ public class PathMappingsHandler extends Handler.AbstractContainer throw new IllegalStateException("Cannot add mapping: " + this); // check that self isn't present - if (handler == this || handler instanceof Handler.Container container && container.getDescendants().contains(this)) + if (handler == this) throw new IllegalStateException("Unable to addHandler of self: " + handler); - // check existing mappings - for (MappedResource entry : mappings) - { - Handler entryHandler = entry.getResource(); - - if (entryHandler == this || - entryHandler == handler || - (entryHandler instanceof Handler.Container container && container.getDescendants().contains(this))) - throw new IllegalStateException("addMapping loop detected: " + handler); - } + // check for loops + if (handler instanceof Handler.Container container && container.getDescendants().contains(this)) + throw new IllegalStateException("loop detected: " + handler); + // add new mapping and remove any old + Handler old = mappings.get(pathSpec); mappings.put(pathSpec, handler); - addBean(handler); + updateBean(old, handler); } @Override diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/WebSocketMappings.java b/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/WebSocketMappings.java index 6bd20c35498..7c71c20d60f 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/WebSocketMappings.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/WebSocketMappings.java @@ -205,7 +205,7 @@ public class WebSocketMappings implements Dumpable, LifeCycle.Listener public boolean removeMapping(PathSpec pathSpec) { - return mappings.remove(pathSpec); + return mappings.remove(pathSpec) != null; } /**