Merge branch 'master' into session-refactor
This commit is contained in:
commit
fe5cc7a336
|
@ -276,12 +276,18 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest
|
|||
|
||||
private void process()
|
||||
{
|
||||
Connection connection = connectionPool.acquire();
|
||||
if (connection != null)
|
||||
process(connection);
|
||||
while (true)
|
||||
{
|
||||
Connection connection = connectionPool.acquire();
|
||||
if (connection == null)
|
||||
break;
|
||||
boolean proceed = process(connection);
|
||||
if (!proceed)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void process(final Connection connection)
|
||||
public boolean process(final Connection connection)
|
||||
{
|
||||
HttpClient client = getHttpClient();
|
||||
final HttpExchange exchange = getHttpExchanges().poll();
|
||||
|
@ -291,13 +297,13 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest
|
|||
{
|
||||
if (!connectionPool.release(connection))
|
||||
connection.close();
|
||||
|
||||
if (!client.isRunning())
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("{} is stopping", client);
|
||||
connection.close();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -316,6 +322,7 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest
|
|||
{
|
||||
send(connection, exchange);
|
||||
}
|
||||
return getHttpExchanges().peek() != null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -40,18 +40,18 @@ public class MultiplexConnectionPool extends AbstractConnectionPool
|
|||
private static final Logger LOG = Log.getLogger(MultiplexConnectionPool.class);
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private final int maxMultiplexed;
|
||||
private final Deque<Holder> idleConnections;
|
||||
private final Map<Connection, Holder> muxedConnections;
|
||||
private final Map<Connection, Holder> busyConnections;
|
||||
private int maxMultiplex;
|
||||
|
||||
public MultiplexConnectionPool(HttpDestination destination, int maxConnections, Callback requester, int maxMultiplexed)
|
||||
public MultiplexConnectionPool(HttpDestination destination, int maxConnections, Callback requester, int maxMultiplex)
|
||||
{
|
||||
super(destination, maxConnections, requester);
|
||||
this.maxMultiplexed = maxMultiplexed;
|
||||
this.idleConnections = new ArrayDeque<>(maxConnections);
|
||||
this.muxedConnections = new HashMap<>(maxConnections);
|
||||
this.busyConnections = new HashMap<>(maxConnections);
|
||||
this.maxMultiplex = maxMultiplex;
|
||||
}
|
||||
|
||||
protected void lock()
|
||||
|
@ -64,6 +64,32 @@ public class MultiplexConnectionPool extends AbstractConnectionPool
|
|||
lock.unlock();
|
||||
}
|
||||
|
||||
public int getMaxMultiplex()
|
||||
{
|
||||
lock();
|
||||
try
|
||||
{
|
||||
return maxMultiplex;
|
||||
}
|
||||
finally
|
||||
{
|
||||
unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void setMaxMultiplex(int maxMultiplex)
|
||||
{
|
||||
lock();
|
||||
try
|
||||
{
|
||||
this.maxMultiplex = maxMultiplex;
|
||||
}
|
||||
finally
|
||||
{
|
||||
unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive(Connection connection)
|
||||
{
|
||||
|
@ -120,7 +146,7 @@ public class MultiplexConnectionPool extends AbstractConnectionPool
|
|||
holder = muxedConnections.values().iterator().next();
|
||||
}
|
||||
|
||||
if (holder.count < maxMultiplexed)
|
||||
if (holder.count < maxMultiplex)
|
||||
{
|
||||
++holder.count;
|
||||
break;
|
||||
|
|
|
@ -30,4 +30,19 @@ public abstract class MultiplexHttpDestination extends HttpDestination
|
|||
return new MultiplexConnectionPool(this, client.getMaxConnectionsPerDestination(), this,
|
||||
client.getMaxRequestsQueuedPerDestination());
|
||||
}
|
||||
|
||||
public int getMaxRequestsPerConnection()
|
||||
{
|
||||
ConnectionPool connectionPool = getConnectionPool();
|
||||
if (connectionPool instanceof MultiplexConnectionPool)
|
||||
return ((MultiplexConnectionPool)connectionPool).getMaxMultiplex();
|
||||
return 1;
|
||||
}
|
||||
|
||||
public void setMaxRequestsPerConnection(int maxRequestsPerConnection)
|
||||
{
|
||||
ConnectionPool connectionPool = getConnectionPool();
|
||||
if (connectionPool instanceof MultiplexConnectionPool)
|
||||
((MultiplexConnectionPool)connectionPool).setMaxMultiplex(maxRequestsPerConnection);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.StringTokenizer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.eclipse.jetty.util.ArrayTernaryTrie;
|
||||
import org.eclipse.jetty.util.RegexSet;
|
||||
|
@ -592,9 +593,8 @@ public class PathMap<O> extends HashMap<String,O>
|
|||
}
|
||||
}
|
||||
|
||||
public static class PathSet extends AbstractSet<String>
|
||||
public static class PathSet extends AbstractSet<String> implements Predicate<String>
|
||||
{
|
||||
public static final BiFunction<PathSet,String,Boolean> MATCHER=(s,e)->{return s.containsMatch(e);};
|
||||
private final PathMap<Boolean> _map = new PathMap<>();
|
||||
|
||||
@Override
|
||||
|
@ -627,12 +627,15 @@ public class PathMap<O> extends HashMap<String,O>
|
|||
return _map.containsKey(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(String s)
|
||||
{
|
||||
return _map.containsMatch(s);
|
||||
}
|
||||
|
||||
public boolean containsMatch(String s)
|
||||
{
|
||||
return _map.containsMatch(s);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.pathmap;
|
||||
|
||||
import org.eclipse.jetty.util.annotation.ManagedAttribute;
|
||||
import org.eclipse.jetty.util.annotation.ManagedObject;
|
||||
|
||||
@ManagedObject("Mapped Resource")
|
||||
public class MappedResource<E> implements Comparable<MappedResource<E>>
|
||||
{
|
||||
private final PathSpec pathSpec;
|
||||
private final E resource;
|
||||
|
||||
public MappedResource(PathSpec pathSpec, E resource)
|
||||
{
|
||||
this.pathSpec = pathSpec;
|
||||
this.resource = resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparison is based solely on the pathSpec
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(MappedResource<E> other)
|
||||
{
|
||||
return this.pathSpec.compareTo(other.pathSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if (this == obj)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (obj == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
MappedResource<?> other = (MappedResource<?>)obj;
|
||||
if (pathSpec == null)
|
||||
{
|
||||
if (other.pathSpec != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!pathSpec.equals(other.pathSpec))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@ManagedAttribute(value = "path spec", readonly = true)
|
||||
public PathSpec getPathSpec()
|
||||
{
|
||||
return pathSpec;
|
||||
}
|
||||
|
||||
@ManagedAttribute(value = "resource", readonly = true)
|
||||
public E getResource()
|
||||
{
|
||||
return resource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = (prime * result) + ((pathSpec == null) ? 0 : pathSpec.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("MappedResource[pathSpec=%s,resource=%s]",pathSpec,resource);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.pathmap;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jetty.util.annotation.ManagedAttribute;
|
||||
import org.eclipse.jetty.util.annotation.ManagedObject;
|
||||
import org.eclipse.jetty.util.component.ContainerLifeCycle;
|
||||
import org.eclipse.jetty.util.component.Dumpable;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
/**
|
||||
* Path Mappings of PathSpec to Resource.
|
||||
* <p>
|
||||
* Sorted into search order upon entry into the Set
|
||||
*
|
||||
* @param <E> the type of mapping endpoint
|
||||
*/
|
||||
@ManagedObject("Path Mappings")
|
||||
public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
|
||||
{
|
||||
private static final Logger LOG = Log.getLogger(PathMappings.class);
|
||||
private List<MappedResource<E>> mappings = new ArrayList<MappedResource<E>>();
|
||||
private MappedResource<E> defaultResource = null;
|
||||
private MappedResource<E> rootResource = null;
|
||||
|
||||
@Override
|
||||
public String dump()
|
||||
{
|
||||
return ContainerLifeCycle.dump(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(Appendable out, String indent) throws IOException
|
||||
{
|
||||
ContainerLifeCycle.dump(out,indent,mappings);
|
||||
}
|
||||
|
||||
@ManagedAttribute(value = "mappings", readonly = true)
|
||||
public List<MappedResource<E>> getMappings()
|
||||
{
|
||||
return mappings;
|
||||
}
|
||||
|
||||
public void reset()
|
||||
{
|
||||
mappings.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of MappedResource matches for the specified path.
|
||||
*
|
||||
* @param path the path to return matches on
|
||||
* @return the list of mapped resource the path matches on
|
||||
*/
|
||||
public List<MappedResource<E>> getMatches(String path)
|
||||
{
|
||||
boolean matchRoot = "/".equals(path);
|
||||
|
||||
List<MappedResource<E>> ret = new ArrayList<>();
|
||||
int len = mappings.size();
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
MappedResource<E> mr = mappings.get(i);
|
||||
|
||||
switch (mr.getPathSpec().group)
|
||||
{
|
||||
case ROOT:
|
||||
if (matchRoot)
|
||||
ret.add(mr);
|
||||
break;
|
||||
case DEFAULT:
|
||||
if (matchRoot || mr.getPathSpec().matches(path))
|
||||
ret.add(mr);
|
||||
break;
|
||||
default:
|
||||
if (mr.getPathSpec().matches(path))
|
||||
ret.add(mr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public MappedResource<E> getMatch(String path)
|
||||
{
|
||||
if (path.equals("/") && rootResource != null)
|
||||
{
|
||||
return rootResource;
|
||||
}
|
||||
|
||||
int len = mappings.size();
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
MappedResource<E> mr = mappings.get(i);
|
||||
if (mr.getPathSpec().matches(path))
|
||||
{
|
||||
return mr;
|
||||
}
|
||||
}
|
||||
return defaultResource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<MappedResource<E>> iterator()
|
||||
{
|
||||
return mappings.iterator();
|
||||
}
|
||||
|
||||
@SuppressWarnings("incomplete-switch")
|
||||
public void put(PathSpec pathSpec, E resource)
|
||||
{
|
||||
MappedResource<E> entry = new MappedResource<>(pathSpec,resource);
|
||||
switch (pathSpec.group)
|
||||
{
|
||||
case DEFAULT:
|
||||
defaultResource = entry;
|
||||
break;
|
||||
case ROOT:
|
||||
rootResource = entry;
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO: add warning when replacing an existing pathspec?
|
||||
|
||||
mappings.add(entry);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Added {} to {}",entry,this);
|
||||
Collections.sort(mappings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("%s[size=%d]",this.getClass().getSimpleName(),mappings.size());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.pathmap;
|
||||
|
||||
/**
|
||||
* The base PathSpec, what all other path specs are based on
|
||||
*/
|
||||
public abstract class PathSpec implements Comparable<PathSpec>
|
||||
{
|
||||
protected String pathSpec;
|
||||
protected PathSpecGroup group;
|
||||
protected int pathDepth;
|
||||
protected int specLength;
|
||||
|
||||
@Override
|
||||
public int compareTo(PathSpec other)
|
||||
{
|
||||
// Grouping (increasing)
|
||||
int diff = this.group.ordinal() - other.group.ordinal();
|
||||
if (diff != 0)
|
||||
{
|
||||
return diff;
|
||||
}
|
||||
|
||||
// Spec Length (decreasing)
|
||||
diff = other.specLength - this.specLength;
|
||||
if (diff != 0)
|
||||
{
|
||||
return diff;
|
||||
}
|
||||
|
||||
// Path Spec Name (alphabetical)
|
||||
return this.pathSpec.compareTo(other.pathSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if (this == obj)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (obj == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
PathSpec other = (PathSpec)obj;
|
||||
if (pathSpec == null)
|
||||
{
|
||||
if (other.pathSpec != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!pathSpec.equals(other.pathSpec))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public PathSpecGroup getGroup()
|
||||
{
|
||||
return group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of path elements that this path spec declares.
|
||||
* <p>
|
||||
* This is used to determine longest match logic.
|
||||
*
|
||||
* @return the depth of the path segments that this spec declares
|
||||
*/
|
||||
public int getPathDepth()
|
||||
{
|
||||
return pathDepth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the portion of the path that is after the path spec.
|
||||
*
|
||||
* @param path
|
||||
* the path to match against
|
||||
* @return the path info portion of the string
|
||||
*/
|
||||
public abstract String getPathInfo(String path);
|
||||
|
||||
/**
|
||||
* Return the portion of the path that matches a path spec.
|
||||
*
|
||||
* @param path
|
||||
* the path to match against
|
||||
* @return the match, or null if no match at all
|
||||
*/
|
||||
public abstract String getPathMatch(String path);
|
||||
|
||||
/**
|
||||
* The as-provided path spec.
|
||||
*
|
||||
* @return the as-provided path spec
|
||||
*/
|
||||
public String getDeclaration()
|
||||
{
|
||||
return pathSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relative path.
|
||||
*
|
||||
* @param base
|
||||
* the base the path is relative to
|
||||
* @param path
|
||||
* the additional path
|
||||
* @return the base plus path with pathSpec portion removed
|
||||
*/
|
||||
public abstract String getRelativePath(String base, String path);
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = (prime * result) + ((pathSpec == null)?0:pathSpec.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test to see if the provided path matches this path spec
|
||||
*
|
||||
* @param path
|
||||
* the path to test
|
||||
* @return true if the path matches this path spec, false otherwise
|
||||
*/
|
||||
public abstract boolean matches(String path);
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder str = new StringBuilder();
|
||||
str.append(this.getClass().getSimpleName()).append("[\"");
|
||||
str.append(pathSpec);
|
||||
str.append("\",pathDepth=").append(pathDepth);
|
||||
str.append(",group=").append(group);
|
||||
str.append("]");
|
||||
return str.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.pathmap;
|
||||
|
||||
/**
|
||||
* Types of path spec groups.
|
||||
* <p>
|
||||
* This is used to facilitate proper pathspec search order.
|
||||
* <p>
|
||||
* Search Order:
|
||||
* <ol>
|
||||
* <li>{@link PathSpecGroup#ordinal()} [increasing]</li>
|
||||
* <li>{@link PathSpec#specLength} [decreasing]</li>
|
||||
* <li>{@link PathSpec#pathSpec} [natural sort order]</li>
|
||||
* </ol>
|
||||
*/
|
||||
public enum PathSpecGroup
|
||||
{
|
||||
// NOTE: Order of enums determines order of Groups.
|
||||
|
||||
/**
|
||||
* For exactly defined path specs, no glob.
|
||||
*/
|
||||
EXACT,
|
||||
/**
|
||||
* For path specs that have a hardcoded prefix and suffix with wildcard glob in the middle.
|
||||
*
|
||||
* <pre>
|
||||
* "^/downloads/[^/]*.zip$" - regex spec
|
||||
* "/a/{var}/c" - uri-template spec
|
||||
* </pre>
|
||||
*
|
||||
* Note: there is no known servlet spec variant of this kind of path spec
|
||||
*/
|
||||
MIDDLE_GLOB,
|
||||
/**
|
||||
* For path specs that have a hardcoded prefix and a trailing wildcard glob.
|
||||
* <p>
|
||||
*
|
||||
* <pre>
|
||||
* "/downloads/*" - servlet spec
|
||||
* "/api/*" - servlet spec
|
||||
* "^/rest/.*$" - regex spec
|
||||
* "/bookings/{guest-id}" - uri-template spec
|
||||
* "/rewards/{vip-level}" - uri-template spec
|
||||
* </pre>
|
||||
*/
|
||||
PREFIX_GLOB,
|
||||
/**
|
||||
* For path specs that have a wildcard glob with a hardcoded suffix
|
||||
*
|
||||
* <pre>
|
||||
* "*.do" - servlet spec
|
||||
* "*.css" - servlet spec
|
||||
* "^.*\.zip$" - regex spec
|
||||
* </pre>
|
||||
*
|
||||
* Note: there is no known uri-template spec variant of this kind of path spec
|
||||
*/
|
||||
SUFFIX_GLOB,
|
||||
/**
|
||||
* The root spec for accessing the Root behavior.
|
||||
*
|
||||
* <pre>
|
||||
* "" - servlet spec (Root Servlet)
|
||||
* null - servlet spec (Root Servlet)
|
||||
* </pre>
|
||||
*
|
||||
* Note: there is no known uri-template spec variant of this kind of path spec
|
||||
*/
|
||||
ROOT,
|
||||
/**
|
||||
* The default spec for accessing the Default path behavior.
|
||||
*
|
||||
* <pre>
|
||||
* "/" - servlet spec (Default Servlet)
|
||||
* "/" - uri-template spec (Root Context)
|
||||
* "^/$" - regex spec (Root Context)
|
||||
* </pre>
|
||||
*
|
||||
* Per Servlet Spec, pathInfo is always null for these specs.
|
||||
* If nothing above matches, then default will match.
|
||||
*/
|
||||
DEFAULT,
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.pathmap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* A Set of PathSpec strings.
|
||||
* <p>
|
||||
* Used by {@link org.eclipse.jetty.util.IncludeExclude} logic
|
||||
*/
|
||||
public class PathSpecSet implements Set<String>, Predicate<String>
|
||||
{
|
||||
private final Set<PathSpec> specs = new TreeSet<>();
|
||||
|
||||
@Override
|
||||
public boolean test(String s)
|
||||
{
|
||||
for (PathSpec spec : specs)
|
||||
{
|
||||
if (spec.matches(s))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty()
|
||||
{
|
||||
return specs.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<String> iterator()
|
||||
{
|
||||
return new Iterator<String>()
|
||||
{
|
||||
private Iterator<PathSpec> iter = specs.iterator();
|
||||
|
||||
@Override
|
||||
public boolean hasNext()
|
||||
{
|
||||
return iter.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String next()
|
||||
{
|
||||
PathSpec spec = iter.next();
|
||||
if (spec == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return spec.getDeclaration();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size()
|
||||
{
|
||||
return specs.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(Object o)
|
||||
{
|
||||
if (o instanceof PathSpec)
|
||||
{
|
||||
return specs.contains(o);
|
||||
}
|
||||
if (o instanceof String)
|
||||
{
|
||||
return specs.contains(toPathSpec((String)o));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private PathSpec asPathSpec(Object o)
|
||||
{
|
||||
if (o == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (o instanceof PathSpec)
|
||||
{
|
||||
return (PathSpec)o;
|
||||
}
|
||||
if (o instanceof String)
|
||||
{
|
||||
return toPathSpec((String)o);
|
||||
}
|
||||
return toPathSpec(o.toString());
|
||||
}
|
||||
|
||||
private PathSpec toPathSpec(String rawSpec)
|
||||
{
|
||||
if ((rawSpec == null) || (rawSpec.length() < 1))
|
||||
{
|
||||
throw new RuntimeException("Path Spec String must start with '^', '/', or '*.': got [" + rawSpec + "]");
|
||||
}
|
||||
if (rawSpec.charAt(0) == '^')
|
||||
{
|
||||
return new RegexPathSpec(rawSpec);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ServletPathSpec(rawSpec);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] toArray()
|
||||
{
|
||||
return toArray(new String[specs.size()]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T[] toArray(T[] a)
|
||||
{
|
||||
int i = 0;
|
||||
for (PathSpec spec : specs)
|
||||
{
|
||||
a[i++] = (T)spec.getDeclaration();
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean add(String e)
|
||||
{
|
||||
return specs.add(toPathSpec(e));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(Object o)
|
||||
{
|
||||
return specs.remove(asPathSpec(o));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsAll(Collection<?> coll)
|
||||
{
|
||||
for (Object o : coll)
|
||||
{
|
||||
if (!specs.contains(asPathSpec(o)))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(Collection<? extends String> coll)
|
||||
{
|
||||
boolean ret = false;
|
||||
|
||||
for (String s : coll)
|
||||
{
|
||||
ret |= add(s);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean retainAll(Collection<?> coll)
|
||||
{
|
||||
List<PathSpec> collSpecs = new ArrayList<>();
|
||||
for (Object o : coll)
|
||||
{
|
||||
collSpecs.add(asPathSpec(o));
|
||||
}
|
||||
return specs.retainAll(collSpecs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeAll(Collection<?> coll)
|
||||
{
|
||||
List<PathSpec> collSpecs = new ArrayList<>();
|
||||
for (Object o : coll)
|
||||
{
|
||||
collSpecs.add(asPathSpec(o));
|
||||
}
|
||||
return specs.removeAll(collSpecs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear()
|
||||
{
|
||||
specs.clear();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.pathmap;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class RegexPathSpec extends PathSpec
|
||||
{
|
||||
protected Pattern pattern;
|
||||
|
||||
protected RegexPathSpec()
|
||||
{
|
||||
super();
|
||||
}
|
||||
|
||||
public RegexPathSpec(String regex)
|
||||
{
|
||||
super.pathSpec = regex;
|
||||
boolean inGrouping = false;
|
||||
this.pathDepth = 0;
|
||||
this.specLength = pathSpec.length();
|
||||
// build up a simple signature we can use to identify the grouping
|
||||
StringBuilder signature = new StringBuilder();
|
||||
for (char c : pathSpec.toCharArray())
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '[':
|
||||
inGrouping = true;
|
||||
break;
|
||||
case ']':
|
||||
inGrouping = false;
|
||||
signature.append('g'); // glob
|
||||
break;
|
||||
case '*':
|
||||
signature.append('g'); // glob
|
||||
break;
|
||||
case '/':
|
||||
if (!inGrouping)
|
||||
{
|
||||
this.pathDepth++;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!inGrouping)
|
||||
{
|
||||
if (Character.isLetterOrDigit(c))
|
||||
{
|
||||
signature.append('l'); // literal (exact)
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.pattern = Pattern.compile(pathSpec);
|
||||
|
||||
// Figure out the grouping based on the signature
|
||||
String sig = signature.toString();
|
||||
|
||||
if (Pattern.matches("^l*$",sig))
|
||||
{
|
||||
this.group = PathSpecGroup.EXACT;
|
||||
}
|
||||
else if (Pattern.matches("^l*g+",sig))
|
||||
{
|
||||
this.group = PathSpecGroup.PREFIX_GLOB;
|
||||
}
|
||||
else if (Pattern.matches("^g+l+$",sig))
|
||||
{
|
||||
this.group = PathSpecGroup.SUFFIX_GLOB;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.group = PathSpecGroup.MIDDLE_GLOB;
|
||||
}
|
||||
}
|
||||
|
||||
public Matcher getMatcher(String path)
|
||||
{
|
||||
return this.pattern.matcher(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPathInfo(String path)
|
||||
{
|
||||
// Path Info only valid for PREFIX_GLOB types
|
||||
if (group == PathSpecGroup.PREFIX_GLOB)
|
||||
{
|
||||
Matcher matcher = getMatcher(path);
|
||||
if (matcher.matches())
|
||||
{
|
||||
if (matcher.groupCount() >= 1)
|
||||
{
|
||||
String pathInfo = matcher.group(1);
|
||||
if ("".equals(pathInfo))
|
||||
{
|
||||
return "/";
|
||||
}
|
||||
else
|
||||
{
|
||||
return pathInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPathMatch(String path)
|
||||
{
|
||||
Matcher matcher = getMatcher(path);
|
||||
if (matcher.matches())
|
||||
{
|
||||
if (matcher.groupCount() >= 1)
|
||||
{
|
||||
int idx = matcher.start(1);
|
||||
if (idx > 0)
|
||||
{
|
||||
if (path.charAt(idx - 1) == '/')
|
||||
{
|
||||
idx--;
|
||||
}
|
||||
return path.substring(0,idx);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Pattern getPattern()
|
||||
{
|
||||
return this.pattern;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRelativePath(String base, String path)
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(final String path)
|
||||
{
|
||||
int idx = path.indexOf('?');
|
||||
if (idx >= 0)
|
||||
{
|
||||
// match only non-query part
|
||||
return getMatcher(path.substring(0,idx)).matches();
|
||||
}
|
||||
else
|
||||
{
|
||||
// match entire path
|
||||
return getMatcher(path).matches();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,261 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.pathmap;
|
||||
|
||||
import org.eclipse.jetty.util.URIUtil;
|
||||
|
||||
public class ServletPathSpec extends PathSpec
|
||||
{
|
||||
public ServletPathSpec(String servletPathSpec)
|
||||
{
|
||||
super();
|
||||
assertValidServletPathSpec(servletPathSpec);
|
||||
|
||||
// The Root Path Spec
|
||||
if ((servletPathSpec == null) || (servletPathSpec.length() == 0))
|
||||
{
|
||||
super.pathSpec = "";
|
||||
super.pathDepth = -1; // force this to be at the end of the sort order
|
||||
this.specLength = 1;
|
||||
this.group = PathSpecGroup.ROOT;
|
||||
return;
|
||||
}
|
||||
|
||||
// The Default Path Spec
|
||||
if("/".equals(servletPathSpec))
|
||||
{
|
||||
super.pathSpec = "/";
|
||||
super.pathDepth = -1; // force this to be at the end of the sort order
|
||||
this.specLength = 1;
|
||||
this.group = PathSpecGroup.DEFAULT;
|
||||
return;
|
||||
}
|
||||
|
||||
this.specLength = servletPathSpec.length();
|
||||
super.pathDepth = 0;
|
||||
char lastChar = servletPathSpec.charAt(specLength - 1);
|
||||
// prefix based
|
||||
if ((servletPathSpec.charAt(0) == '/') && (specLength > 1) && (lastChar == '*'))
|
||||
{
|
||||
this.group = PathSpecGroup.PREFIX_GLOB;
|
||||
}
|
||||
// suffix based
|
||||
else if (servletPathSpec.charAt(0) == '*')
|
||||
{
|
||||
this.group = PathSpecGroup.SUFFIX_GLOB;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.group = PathSpecGroup.EXACT;
|
||||
}
|
||||
|
||||
for (int i = 0; i < specLength; i++)
|
||||
{
|
||||
int cp = servletPathSpec.codePointAt(i);
|
||||
if (cp < 128)
|
||||
{
|
||||
char c = (char)cp;
|
||||
switch (c)
|
||||
{
|
||||
case '/':
|
||||
super.pathDepth++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.pathSpec = servletPathSpec;
|
||||
}
|
||||
|
||||
private void assertValidServletPathSpec(String servletPathSpec)
|
||||
{
|
||||
if ((servletPathSpec == null) || servletPathSpec.equals(""))
|
||||
{
|
||||
return; // empty path spec
|
||||
}
|
||||
|
||||
int len = servletPathSpec.length();
|
||||
// path spec must either start with '/' or '*.'
|
||||
if (servletPathSpec.charAt(0) == '/')
|
||||
{
|
||||
// Prefix Based
|
||||
if (len == 1)
|
||||
{
|
||||
return; // simple '/' path spec
|
||||
}
|
||||
int idx = servletPathSpec.indexOf('*');
|
||||
if (idx < 0)
|
||||
{
|
||||
return; // no hit on glob '*'
|
||||
}
|
||||
// only allowed to have '*' at the end of the path spec
|
||||
if (idx != (len - 1))
|
||||
{
|
||||
throw new IllegalArgumentException("Servlet Spec 12.2 violation: glob '*' can only exist at end of prefix based matches: bad spec \""+ servletPathSpec +"\"");
|
||||
}
|
||||
}
|
||||
else if (servletPathSpec.startsWith("*."))
|
||||
{
|
||||
// Suffix Based
|
||||
int idx = servletPathSpec.indexOf('/');
|
||||
// cannot have path separator
|
||||
if (idx >= 0)
|
||||
{
|
||||
throw new IllegalArgumentException("Servlet Spec 12.2 violation: suffix based path spec cannot have path separators: bad spec \""+ servletPathSpec +"\"");
|
||||
}
|
||||
|
||||
idx = servletPathSpec.indexOf('*',2);
|
||||
// only allowed to have 1 glob '*', at the start of the path spec
|
||||
if (idx >= 1)
|
||||
{
|
||||
throw new IllegalArgumentException("Servlet Spec 12.2 violation: suffix based path spec cannot have multiple glob '*': bad spec \""+ servletPathSpec +"\"");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IllegalArgumentException("Servlet Spec 12.2 violation: path spec must start with \"/\" or \"*.\": bad spec \""+ servletPathSpec +"\"");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPathInfo(String path)
|
||||
{
|
||||
// Path Info only valid for PREFIX_GLOB types
|
||||
if (group == PathSpecGroup.PREFIX_GLOB)
|
||||
{
|
||||
if (path.length() == (specLength - 2))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return path.substring(specLength - 2);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPathMatch(String path)
|
||||
{
|
||||
switch (group)
|
||||
{
|
||||
case EXACT:
|
||||
if (pathSpec.equals(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
case PREFIX_GLOB:
|
||||
if (isWildcardMatch(path))
|
||||
{
|
||||
return path.substring(0,specLength - 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
case SUFFIX_GLOB:
|
||||
if (path.regionMatches(path.length() - (specLength - 1),pathSpec,1,specLength - 1))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
case DEFAULT:
|
||||
return path;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRelativePath(String base, String path)
|
||||
{
|
||||
String info = getPathInfo(path);
|
||||
if (info == null)
|
||||
{
|
||||
info = path;
|
||||
}
|
||||
|
||||
if (info.startsWith("./"))
|
||||
{
|
||||
info = info.substring(2);
|
||||
}
|
||||
if (base.endsWith(URIUtil.SLASH))
|
||||
{
|
||||
if (info.startsWith(URIUtil.SLASH))
|
||||
{
|
||||
path = base + info.substring(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
path = base + info;
|
||||
}
|
||||
}
|
||||
else if (info.startsWith(URIUtil.SLASH))
|
||||
{
|
||||
path = base + info;
|
||||
}
|
||||
else
|
||||
{
|
||||
path = base + URIUtil.SLASH + info;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private boolean isWildcardMatch(String path)
|
||||
{
|
||||
// For a spec of "/foo/*" match "/foo" , "/foo/..." but not "/foobar"
|
||||
int cpl = specLength - 2;
|
||||
if ((group == PathSpecGroup.PREFIX_GLOB) && (path.regionMatches(0,pathSpec,0,cpl)))
|
||||
{
|
||||
if ((path.length() == cpl) || ('/' == path.charAt(cpl)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String path)
|
||||
{
|
||||
switch (group)
|
||||
{
|
||||
case EXACT:
|
||||
return pathSpec.equals(path);
|
||||
case PREFIX_GLOB:
|
||||
return isWildcardMatch(path);
|
||||
case SUFFIX_GLOB:
|
||||
return path.regionMatches((path.length() - specLength) + 1,pathSpec,1,specLength - 1);
|
||||
case ROOT:
|
||||
// Only "/" matches
|
||||
return ("/".equals(path));
|
||||
case DEFAULT:
|
||||
// If we reached this point, then everything matches
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,341 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.pathmap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jetty.util.TypeUtil;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
/**
|
||||
* PathSpec for URI Template based declarations
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc6570">URI Templates (Level 1)</a>
|
||||
*/
|
||||
public class UriTemplatePathSpec extends RegexPathSpec
|
||||
{
|
||||
private static final Logger LOG = Log.getLogger(UriTemplatePathSpec.class);
|
||||
|
||||
private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{(.*)\\}");
|
||||
/** Reserved Symbols in URI Template variable */
|
||||
private static final String VARIABLE_RESERVED = ":/?#[]@" + // gen-delims
|
||||
"!$&'()*+,;="; // sub-delims
|
||||
/** Allowed Symbols in a URI Template variable */
|
||||
private static final String VARIABLE_SYMBOLS="-._";
|
||||
private static final Set<String> FORBIDDEN_SEGMENTS;
|
||||
|
||||
static
|
||||
{
|
||||
FORBIDDEN_SEGMENTS = new HashSet<>();
|
||||
FORBIDDEN_SEGMENTS.add("/./");
|
||||
FORBIDDEN_SEGMENTS.add("/../");
|
||||
FORBIDDEN_SEGMENTS.add("//");
|
||||
}
|
||||
|
||||
private String variables[];
|
||||
|
||||
public UriTemplatePathSpec(String rawSpec)
|
||||
{
|
||||
super();
|
||||
Objects.requireNonNull(rawSpec,"Path Param Spec cannot be null");
|
||||
|
||||
if ("".equals(rawSpec) || "/".equals(rawSpec))
|
||||
{
|
||||
super.pathSpec = "/";
|
||||
super.pattern = Pattern.compile("^/$");
|
||||
super.pathDepth = 1;
|
||||
this.specLength = 1;
|
||||
this.variables = new String[0];
|
||||
this.group = PathSpecGroup.EXACT;
|
||||
return;
|
||||
}
|
||||
|
||||
if (rawSpec.charAt(0) != '/')
|
||||
{
|
||||
// path specs must start with '/'
|
||||
StringBuilder err = new StringBuilder();
|
||||
err.append("Syntax Error: path spec \"");
|
||||
err.append(rawSpec);
|
||||
err.append("\" must start with '/'");
|
||||
throw new IllegalArgumentException(err.toString());
|
||||
}
|
||||
|
||||
for (String forbidden : FORBIDDEN_SEGMENTS)
|
||||
{
|
||||
if (rawSpec.contains(forbidden))
|
||||
{
|
||||
StringBuilder err = new StringBuilder();
|
||||
err.append("Syntax Error: segment ");
|
||||
err.append(forbidden);
|
||||
err.append(" is forbidden in path spec: ");
|
||||
err.append(rawSpec);
|
||||
throw new IllegalArgumentException(err.toString());
|
||||
}
|
||||
}
|
||||
|
||||
this.pathSpec = rawSpec;
|
||||
|
||||
StringBuilder regex = new StringBuilder();
|
||||
regex.append('^');
|
||||
|
||||
List<String> varNames = new ArrayList<>();
|
||||
// split up into path segments (ignoring the first slash that will always be empty)
|
||||
String segments[] = rawSpec.substring(1).split("/");
|
||||
char segmentSignature[] = new char[segments.length];
|
||||
this.pathDepth = segments.length;
|
||||
for (int i = 0; i < segments.length; i++)
|
||||
{
|
||||
String segment = segments[i];
|
||||
Matcher mat = VARIABLE_PATTERN.matcher(segment);
|
||||
|
||||
if (mat.matches())
|
||||
{
|
||||
// entire path segment is a variable.
|
||||
String variable = mat.group(1);
|
||||
if (varNames.contains(variable))
|
||||
{
|
||||
// duplicate variable names
|
||||
StringBuilder err = new StringBuilder();
|
||||
err.append("Syntax Error: variable ");
|
||||
err.append(variable);
|
||||
err.append(" is duplicated in path spec: ");
|
||||
err.append(rawSpec);
|
||||
throw new IllegalArgumentException(err.toString());
|
||||
}
|
||||
|
||||
assertIsValidVariableLiteral(variable);
|
||||
|
||||
segmentSignature[i] = 'v'; // variable
|
||||
// valid variable name
|
||||
varNames.add(variable);
|
||||
// build regex
|
||||
regex.append("/([^/]+)");
|
||||
}
|
||||
else if (mat.find(0))
|
||||
{
|
||||
// variable exists as partial segment
|
||||
StringBuilder err = new StringBuilder();
|
||||
err.append("Syntax Error: variable ");
|
||||
err.append(mat.group());
|
||||
err.append(" must exist as entire path segment: ");
|
||||
err.append(rawSpec);
|
||||
throw new IllegalArgumentException(err.toString());
|
||||
}
|
||||
else if ((segment.indexOf('{') >= 0) || (segment.indexOf('}') >= 0))
|
||||
{
|
||||
// variable is split with a path separator
|
||||
StringBuilder err = new StringBuilder();
|
||||
err.append("Syntax Error: invalid path segment /");
|
||||
err.append(segment);
|
||||
err.append("/ variable declaration incomplete: ");
|
||||
err.append(rawSpec);
|
||||
throw new IllegalArgumentException(err.toString());
|
||||
}
|
||||
else if (segment.indexOf('*') >= 0)
|
||||
{
|
||||
// glob segment
|
||||
StringBuilder err = new StringBuilder();
|
||||
err.append("Syntax Error: path segment /");
|
||||
err.append(segment);
|
||||
err.append("/ contains a wildcard symbol (not supported by this uri-template implementation): ");
|
||||
err.append(rawSpec);
|
||||
throw new IllegalArgumentException(err.toString());
|
||||
}
|
||||
else
|
||||
{
|
||||
// valid path segment
|
||||
segmentSignature[i] = 'e'; // exact
|
||||
// build regex
|
||||
regex.append('/');
|
||||
// escape regex special characters
|
||||
for (char c : segment.toCharArray())
|
||||
{
|
||||
if ((c == '.') || (c == '[') || (c == ']') || (c == '\\'))
|
||||
{
|
||||
regex.append('\\');
|
||||
}
|
||||
regex.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle trailing slash (which is not picked up during split)
|
||||
if(rawSpec.charAt(rawSpec.length()-1) == '/')
|
||||
{
|
||||
regex.append('/');
|
||||
}
|
||||
|
||||
regex.append('$');
|
||||
|
||||
this.pattern = Pattern.compile(regex.toString());
|
||||
|
||||
int varcount = varNames.size();
|
||||
this.variables = varNames.toArray(new String[varcount]);
|
||||
|
||||
// Convert signature to group
|
||||
String sig = String.valueOf(segmentSignature);
|
||||
|
||||
if (Pattern.matches("^e*$",sig))
|
||||
{
|
||||
this.group = PathSpecGroup.EXACT;
|
||||
}
|
||||
else if (Pattern.matches("^e*v+",sig))
|
||||
{
|
||||
this.group = PathSpecGroup.PREFIX_GLOB;
|
||||
}
|
||||
else if (Pattern.matches("^v+e+",sig))
|
||||
{
|
||||
this.group = PathSpecGroup.SUFFIX_GLOB;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.group = PathSpecGroup.MIDDLE_GLOB;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate variable literal name, per RFC6570, Section 2.1 Literals
|
||||
* @param variable
|
||||
* @param pathParamSpec
|
||||
*/
|
||||
private void assertIsValidVariableLiteral(String variable)
|
||||
{
|
||||
int len = variable.length();
|
||||
|
||||
int i = 0;
|
||||
int codepoint;
|
||||
boolean valid = (len > 0); // must not be zero length
|
||||
|
||||
while (valid && i < len)
|
||||
{
|
||||
codepoint = variable.codePointAt(i);
|
||||
i += Character.charCount(codepoint);
|
||||
|
||||
// basic letters, digits, or symbols
|
||||
if (isValidBasicLiteralCodepoint(codepoint))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// The ucschar and iprivate pieces
|
||||
if (Character.isSupplementaryCodePoint(codepoint))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// pct-encoded
|
||||
if (codepoint == '%')
|
||||
{
|
||||
if (i + 2 > len)
|
||||
{
|
||||
// invalid percent encoding, missing extra 2 chars
|
||||
valid = false;
|
||||
continue;
|
||||
}
|
||||
codepoint = TypeUtil.convertHexDigit(variable.codePointAt(i++)) << 4;
|
||||
codepoint |= TypeUtil.convertHexDigit(variable.codePointAt(i++));
|
||||
|
||||
// validate basic literal
|
||||
if (isValidBasicLiteralCodepoint(codepoint))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!valid)
|
||||
{
|
||||
// invalid variable name
|
||||
StringBuilder err = new StringBuilder();
|
||||
err.append("Syntax Error: variable {");
|
||||
err.append(variable);
|
||||
err.append("} an invalid variable name: ");
|
||||
err.append(pathSpec);
|
||||
throw new IllegalArgumentException(err.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidBasicLiteralCodepoint(int codepoint)
|
||||
{
|
||||
// basic letters or digits
|
||||
if((codepoint >= 'a' && codepoint <= 'z') ||
|
||||
(codepoint >= 'A' && codepoint <= 'Z') ||
|
||||
(codepoint >= '0' && codepoint <= '9'))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// basic allowed symbols
|
||||
if(VARIABLE_SYMBOLS.indexOf(codepoint) >= 0)
|
||||
{
|
||||
return true; // valid simple value
|
||||
}
|
||||
|
||||
// basic reserved symbols
|
||||
if(VARIABLE_RESERVED.indexOf(codepoint) >= 0)
|
||||
{
|
||||
LOG.warn("Detected URI Template reserved symbol [{}] in path spec \"{}\"",(char)codepoint,pathSpec);
|
||||
return false; // valid simple value
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Map<String, String> getPathParams(String path)
|
||||
{
|
||||
Matcher matcher = getMatcher(path);
|
||||
if (matcher.matches())
|
||||
{
|
||||
if (group == PathSpecGroup.EXACT)
|
||||
{
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, String> ret = new HashMap<>();
|
||||
int groupCount = matcher.groupCount();
|
||||
for (int i = 1; i <= groupCount; i++)
|
||||
{
|
||||
ret.put(this.variables[i - 1],matcher.group(i));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getVariableCount()
|
||||
{
|
||||
return variables.length;
|
||||
}
|
||||
|
||||
public String[] getVariables()
|
||||
{
|
||||
return this.variables;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,281 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.pathmap;
|
||||
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class PathMappingsTest
|
||||
{
|
||||
private void assertMatch(PathMappings<String> pathmap, String path, String expectedValue)
|
||||
{
|
||||
String msg = String.format(".getMatch(\"%s\")",path);
|
||||
MappedResource<String> match = pathmap.getMatch(path);
|
||||
Assert.assertThat(msg,match,notNullValue());
|
||||
String actualMatch = match.getResource();
|
||||
Assert.assertEquals(msg,expectedValue,actualMatch);
|
||||
}
|
||||
|
||||
public void dumpMappings(PathMappings<String> p)
|
||||
{
|
||||
for (MappedResource<String> res : p)
|
||||
{
|
||||
System.out.printf(" %s%n",res);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the match order rules with a mixed Servlet and regex path specs
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Exact match</li>
|
||||
* <li>Longest prefix match</li>
|
||||
* <li>Longest suffix match</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Test
|
||||
public void testMixedMatchOrder()
|
||||
{
|
||||
PathMappings<String> p = new PathMappings<>();
|
||||
|
||||
p.put(new ServletPathSpec("/"),"default");
|
||||
p.put(new ServletPathSpec("/animal/bird/*"),"birds");
|
||||
p.put(new ServletPathSpec("/animal/fish/*"),"fishes");
|
||||
p.put(new ServletPathSpec("/animal/*"),"animals");
|
||||
p.put(new RegexPathSpec("^/animal/.*/chat$"),"animalChat");
|
||||
p.put(new RegexPathSpec("^/animal/.*/cam$"),"animalCam");
|
||||
p.put(new RegexPathSpec("^/entrance/cam$"),"entranceCam");
|
||||
|
||||
// dumpMappings(p);
|
||||
|
||||
assertMatch(p,"/animal/bird/eagle","birds");
|
||||
assertMatch(p,"/animal/fish/bass/sea","fishes");
|
||||
assertMatch(p,"/animal/peccary/javalina/evolution","animals");
|
||||
assertMatch(p,"/","default");
|
||||
assertMatch(p,"/animal/bird/eagle/chat","animalChat");
|
||||
assertMatch(p,"/animal/bird/penguin/chat","animalChat");
|
||||
assertMatch(p,"/animal/fish/trout/cam","animalCam");
|
||||
assertMatch(p,"/entrance/cam","entranceCam");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the match order rules imposed by the Servlet API (default vs any)
|
||||
*/
|
||||
@Test
|
||||
public void testServletMatchDefault()
|
||||
{
|
||||
PathMappings<String> p = new PathMappings<>();
|
||||
|
||||
p.put(new ServletPathSpec("/"),"default");
|
||||
p.put(new ServletPathSpec("/*"),"any");
|
||||
|
||||
assertMatch(p,"/abs/path","any");
|
||||
assertMatch(p,"/abs/path/xxx","any");
|
||||
assertMatch(p,"/animal/bird/eagle/bald","any");
|
||||
assertMatch(p,"/","any");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the match order rules with a mixed Servlet and URI Template path specs
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Exact match</li>
|
||||
* <li>Longest prefix match</li>
|
||||
* <li>Longest suffix match</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Test
|
||||
public void testMixedMatchUriOrder()
|
||||
{
|
||||
PathMappings<String> p = new PathMappings<>();
|
||||
|
||||
p.put(new ServletPathSpec("/"),"default");
|
||||
p.put(new ServletPathSpec("/animal/bird/*"),"birds");
|
||||
p.put(new ServletPathSpec("/animal/fish/*"),"fishes");
|
||||
p.put(new ServletPathSpec("/animal/*"),"animals");
|
||||
p.put(new UriTemplatePathSpec("/animal/{type}/{name}/chat"),"animalChat");
|
||||
p.put(new UriTemplatePathSpec("/animal/{type}/{name}/cam"),"animalCam");
|
||||
p.put(new UriTemplatePathSpec("/entrance/cam"),"entranceCam");
|
||||
|
||||
// dumpMappings(p);
|
||||
|
||||
assertMatch(p,"/animal/bird/eagle","birds");
|
||||
assertMatch(p,"/animal/fish/bass/sea","fishes");
|
||||
assertMatch(p,"/animal/peccary/javalina/evolution","animals");
|
||||
assertMatch(p,"/","default");
|
||||
assertMatch(p,"/animal/bird/eagle/chat","animalChat");
|
||||
assertMatch(p,"/animal/bird/penguin/chat","animalChat");
|
||||
assertMatch(p,"/animal/fish/trout/cam","animalCam");
|
||||
assertMatch(p,"/entrance/cam","entranceCam");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the match order rules for URI Template based specs
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Exact match</li>
|
||||
* <li>Longest prefix match</li>
|
||||
* <li>Longest suffix match</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Test
|
||||
public void testUriTemplateMatchOrder()
|
||||
{
|
||||
PathMappings<String> p = new PathMappings<>();
|
||||
|
||||
p.put(new UriTemplatePathSpec("/a/{var}/c"),"endpointA");
|
||||
p.put(new UriTemplatePathSpec("/a/b/c"),"endpointB");
|
||||
p.put(new UriTemplatePathSpec("/a/{var1}/{var2}"),"endpointC");
|
||||
p.put(new UriTemplatePathSpec("/{var1}/d"),"endpointD");
|
||||
p.put(new UriTemplatePathSpec("/b/{var2}"),"endpointE");
|
||||
|
||||
// dumpMappings(p);
|
||||
|
||||
assertMatch(p,"/a/b/c","endpointB");
|
||||
assertMatch(p,"/a/d/c","endpointA");
|
||||
assertMatch(p,"/a/x/y","endpointC");
|
||||
|
||||
assertMatch(p,"/b/d","endpointE");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPathMap() throws Exception
|
||||
{
|
||||
PathMappings<String> p = new PathMappings<>();
|
||||
|
||||
p.put(new ServletPathSpec("/abs/path"), "1");
|
||||
p.put(new ServletPathSpec("/abs/path/longer"), "2");
|
||||
p.put(new ServletPathSpec("/animal/bird/*"), "3");
|
||||
p.put(new ServletPathSpec("/animal/fish/*"), "4");
|
||||
p.put(new ServletPathSpec("/animal/*"), "5");
|
||||
p.put(new ServletPathSpec("*.tar.gz"), "6");
|
||||
p.put(new ServletPathSpec("*.gz"), "7");
|
||||
p.put(new ServletPathSpec("/"), "8");
|
||||
// p.put(new ServletPathSpec("/XXX:/YYY"), "9"); // special syntax from Jetty 3.1.x
|
||||
p.put(new ServletPathSpec(""), "10");
|
||||
p.put(new ServletPathSpec("/\u20ACuro/*"), "11");
|
||||
|
||||
assertEquals("pathMatch exact", "/Foo/bar", new ServletPathSpec("/Foo/bar").getPathMatch("/Foo/bar"));
|
||||
assertEquals("pathMatch prefix", "/Foo", new ServletPathSpec("/Foo/*").getPathMatch("/Foo/bar"));
|
||||
assertEquals("pathMatch prefix", "/Foo", new ServletPathSpec("/Foo/*").getPathMatch("/Foo/"));
|
||||
assertEquals("pathMatch prefix", "/Foo", new ServletPathSpec("/Foo/*").getPathMatch("/Foo"));
|
||||
assertEquals("pathMatch suffix", "/Foo/bar.ext", new ServletPathSpec("*.ext").getPathMatch("/Foo/bar.ext"));
|
||||
assertEquals("pathMatch default", "/Foo/bar.ext", new ServletPathSpec("/").getPathMatch("/Foo/bar.ext"));
|
||||
|
||||
assertEquals("pathInfo exact", null, new ServletPathSpec("/Foo/bar").getPathInfo("/Foo/bar"));
|
||||
assertEquals("pathInfo prefix", "/bar", new ServletPathSpec("/Foo/*").getPathInfo("/Foo/bar"));
|
||||
assertEquals("pathInfo prefix", "/*", new ServletPathSpec("/Foo/*").getPathInfo("/Foo/*"));
|
||||
assertEquals("pathInfo prefix", "/", new ServletPathSpec("/Foo/*").getPathInfo("/Foo/"));
|
||||
assertEquals("pathInfo prefix", null, new ServletPathSpec("/Foo/*").getPathInfo("/Foo"));
|
||||
assertEquals("pathInfo suffix", null, new ServletPathSpec("*.ext").getPathInfo("/Foo/bar.ext"));
|
||||
assertEquals("pathInfo default", null, new ServletPathSpec("/").getPathInfo("/Foo/bar.ext"));
|
||||
|
||||
p.put(new ServletPathSpec("/*"), "0");
|
||||
|
||||
// assertEquals("Get absolute path", "1", p.get("/abs/path"));
|
||||
assertEquals("Match absolute path", "/abs/path", p.getMatch("/abs/path").getPathSpec().pathSpec);
|
||||
assertEquals("Match absolute path", "1", p.getMatch("/abs/path").getResource());
|
||||
assertEquals("Mismatch absolute path", "0", p.getMatch("/abs/path/xxx").getResource());
|
||||
assertEquals("Mismatch absolute path", "0", p.getMatch("/abs/pith").getResource());
|
||||
assertEquals("Match longer absolute path", "2", p.getMatch("/abs/path/longer").getResource());
|
||||
assertEquals("Not exact absolute path", "0", p.getMatch("/abs/path/").getResource());
|
||||
assertEquals("Not exact absolute path", "0", p.getMatch("/abs/path/xxx").getResource());
|
||||
|
||||
assertEquals("Match longest prefix", "3", p.getMatch("/animal/bird/eagle/bald").getResource());
|
||||
assertEquals("Match longest prefix", "4", p.getMatch("/animal/fish/shark/grey").getResource());
|
||||
assertEquals("Match longest prefix", "5", p.getMatch("/animal/insect/bug").getResource());
|
||||
assertEquals("mismatch exact prefix", "5", p.getMatch("/animal").getResource());
|
||||
assertEquals("mismatch exact prefix", "5", p.getMatch("/animal/").getResource());
|
||||
|
||||
assertEquals("Match longest suffix", "0", p.getMatch("/suffix/path.tar.gz").getResource());
|
||||
assertEquals("Match longest suffix", "0", p.getMatch("/suffix/path.gz").getResource());
|
||||
assertEquals("prefix rather than suffix", "5", p.getMatch("/animal/path.gz").getResource());
|
||||
|
||||
assertEquals("default", "0", p.getMatch("/Other/path").getResource());
|
||||
|
||||
assertEquals("pathMatch /*", "", new ServletPathSpec("/*").getPathMatch("/xxx/zzz"));
|
||||
assertEquals("pathInfo /*", "/xxx/zzz", new ServletPathSpec("/*").getPathInfo("/xxx/zzz"));
|
||||
|
||||
assertTrue("match /", new ServletPathSpec("/").matches("/anything"));
|
||||
assertTrue("match /*", new ServletPathSpec("/*").matches("/anything"));
|
||||
assertTrue("match /foo", new ServletPathSpec("/foo").matches("/foo"));
|
||||
assertTrue("!match /foo", !new ServletPathSpec("/foo").matches("/bar"));
|
||||
assertTrue("match /foo/*", new ServletPathSpec("/foo/*").matches("/foo"));
|
||||
assertTrue("match /foo/*", new ServletPathSpec("/foo/*").matches("/foo/"));
|
||||
assertTrue("match /foo/*", new ServletPathSpec("/foo/*").matches("/foo/anything"));
|
||||
assertTrue("!match /foo/*", !new ServletPathSpec("/foo/*").matches("/bar"));
|
||||
assertTrue("!match /foo/*", !new ServletPathSpec("/foo/*").matches("/bar/"));
|
||||
assertTrue("!match /foo/*", !new ServletPathSpec("/foo/*").matches("/bar/anything"));
|
||||
assertTrue("match *.foo", new ServletPathSpec("*.foo").matches("anything.foo"));
|
||||
assertTrue("!match *.foo", !new ServletPathSpec("*.foo").matches("anything.bar"));
|
||||
|
||||
assertEquals("match / with ''", "10", p.getMatch("/").getResource());
|
||||
|
||||
assertTrue("match \"\"", new ServletPathSpec("").matches("/"));
|
||||
}
|
||||
|
||||
/**
|
||||
* See JIRA issue: JETTY-88.
|
||||
* @throws Exception failed test
|
||||
*/
|
||||
@Test
|
||||
public void testPathMappingsOnlyMatchOnDirectoryNames() throws Exception
|
||||
{
|
||||
ServletPathSpec spec = new ServletPathSpec("/xyz/*");
|
||||
|
||||
PathSpecAssert.assertMatch(spec, "/xyz");
|
||||
PathSpecAssert.assertMatch(spec, "/xyz/");
|
||||
PathSpecAssert.assertMatch(spec, "/xyz/123");
|
||||
PathSpecAssert.assertMatch(spec, "/xyz/123/");
|
||||
PathSpecAssert.assertMatch(spec, "/xyz/123.txt");
|
||||
PathSpecAssert.assertNotMatch(spec, "/xyz123");
|
||||
PathSpecAssert.assertNotMatch(spec, "/xyz123;jessionid=99");
|
||||
PathSpecAssert.assertNotMatch(spec, "/xyz123/");
|
||||
PathSpecAssert.assertNotMatch(spec, "/xyz123/456");
|
||||
PathSpecAssert.assertNotMatch(spec, "/xyz.123");
|
||||
PathSpecAssert.assertNotMatch(spec, "/xyz;123"); // as if the ; was encoded and part of the path
|
||||
PathSpecAssert.assertNotMatch(spec, "/xyz?123"); // as if the ? was encoded and part of the path
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrecidenceVsOrdering() throws Exception
|
||||
{
|
||||
PathMappings<String> p = new PathMappings<>();
|
||||
p.put(new ServletPathSpec("/dump/gzip/*"),"prefix");
|
||||
p.put(new ServletPathSpec("*.txt"),"suffix");
|
||||
|
||||
assertEquals(null,p.getMatch("/foo/bar"));
|
||||
assertEquals("prefix",p.getMatch("/dump/gzip/something").getResource());
|
||||
assertEquals("suffix",p.getMatch("/foo/something.txt").getResource());
|
||||
assertEquals("prefix",p.getMatch("/dump/gzip/something.txt").getResource());
|
||||
|
||||
p = new PathMappings<>();
|
||||
p.put(new ServletPathSpec("*.txt"),"suffix");
|
||||
p.put(new ServletPathSpec("/dump/gzip/*"),"prefix");
|
||||
|
||||
assertEquals(null,p.getMatch("/foo/bar"));
|
||||
assertEquals("prefix",p.getMatch("/dump/gzip/something").getResource());
|
||||
assertEquals("suffix",p.getMatch("/foo/something.txt").getResource());
|
||||
assertEquals("prefix",p.getMatch("/dump/gzip/something.txt").getResource());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.pathmap;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
public class PathSpecAssert
|
||||
{
|
||||
public static void assertMatch(PathSpec spec, String path)
|
||||
{
|
||||
boolean match = spec.matches(path);
|
||||
assertThat(spec.getClass().getSimpleName() + " '" + spec + "' should match path '" + path + "'", match, is(true));
|
||||
}
|
||||
|
||||
public static void assertNotMatch(PathSpec spec, String path)
|
||||
{
|
||||
boolean match = spec.matches(path);
|
||||
assertThat(spec.getClass().getSimpleName() + " '" + spec + "' should not match path '" + path + "'", match, is(false));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.pathmap;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class RegexPathSpecTest
|
||||
{
|
||||
public static void assertMatches(PathSpec spec, String path)
|
||||
{
|
||||
String msg = String.format("Spec(\"%s\").matches(\"%s\")",spec.getDeclaration(),path);
|
||||
assertThat(msg,spec.matches(path),is(true));
|
||||
}
|
||||
|
||||
public static void assertNotMatches(PathSpec spec, String path)
|
||||
{
|
||||
String msg = String.format("!Spec(\"%s\").matches(\"%s\")",spec.getDeclaration(),path);
|
||||
assertThat(msg,spec.matches(path),is(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExactSpec()
|
||||
{
|
||||
RegexPathSpec spec = new RegexPathSpec("^/a$");
|
||||
assertEquals("Spec.pathSpec","^/a$",spec.getDeclaration());
|
||||
assertEquals("Spec.pattern","^/a$",spec.getPattern().pattern());
|
||||
assertEquals("Spec.pathDepth",1,spec.getPathDepth());
|
||||
assertEquals("Spec.group",PathSpecGroup.EXACT,spec.group);
|
||||
|
||||
assertMatches(spec,"/a");
|
||||
|
||||
assertNotMatches(spec,"/aa");
|
||||
assertNotMatches(spec,"/a/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMiddleSpec()
|
||||
{
|
||||
RegexPathSpec spec = new RegexPathSpec("^/rest/([^/]*)/list$");
|
||||
assertEquals("Spec.pathSpec","^/rest/([^/]*)/list$",spec.getDeclaration());
|
||||
assertEquals("Spec.pattern","^/rest/([^/]*)/list$",spec.getPattern().pattern());
|
||||
assertEquals("Spec.pathDepth",3,spec.getPathDepth());
|
||||
assertEquals("Spec.group",PathSpecGroup.MIDDLE_GLOB,spec.group);
|
||||
|
||||
assertMatches(spec,"/rest/api/list");
|
||||
assertMatches(spec,"/rest/1.0/list");
|
||||
assertMatches(spec,"/rest/2.0/list");
|
||||
assertMatches(spec,"/rest/accounts/list");
|
||||
|
||||
assertNotMatches(spec,"/a");
|
||||
assertNotMatches(spec,"/aa");
|
||||
assertNotMatches(spec,"/aa/bb");
|
||||
assertNotMatches(spec,"/rest/admin/delete");
|
||||
assertNotMatches(spec,"/rest/list");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMiddleSpecNoGrouping()
|
||||
{
|
||||
RegexPathSpec spec = new RegexPathSpec("^/rest/[^/]+/list$");
|
||||
assertEquals("Spec.pathSpec","^/rest/[^/]+/list$",spec.getDeclaration());
|
||||
assertEquals("Spec.pattern","^/rest/[^/]+/list$",spec.getPattern().pattern());
|
||||
assertEquals("Spec.pathDepth",3,spec.getPathDepth());
|
||||
assertEquals("Spec.group",PathSpecGroup.MIDDLE_GLOB,spec.group);
|
||||
|
||||
assertMatches(spec,"/rest/api/list");
|
||||
assertMatches(spec,"/rest/1.0/list");
|
||||
assertMatches(spec,"/rest/2.0/list");
|
||||
assertMatches(spec,"/rest/accounts/list");
|
||||
|
||||
assertNotMatches(spec,"/a");
|
||||
assertNotMatches(spec,"/aa");
|
||||
assertNotMatches(spec,"/aa/bb");
|
||||
assertNotMatches(spec,"/rest/admin/delete");
|
||||
assertNotMatches(spec,"/rest/list");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrefixSpec()
|
||||
{
|
||||
RegexPathSpec spec = new RegexPathSpec("^/a/(.*)$");
|
||||
assertEquals("Spec.pathSpec","^/a/(.*)$",spec.getDeclaration());
|
||||
assertEquals("Spec.pattern","^/a/(.*)$",spec.getPattern().pattern());
|
||||
assertEquals("Spec.pathDepth",2,spec.getPathDepth());
|
||||
assertEquals("Spec.group",PathSpecGroup.PREFIX_GLOB,spec.group);
|
||||
|
||||
assertMatches(spec,"/a/");
|
||||
assertMatches(spec,"/a/b");
|
||||
assertMatches(spec,"/a/b/c/d/e");
|
||||
|
||||
assertNotMatches(spec,"/a");
|
||||
assertNotMatches(spec,"/aa");
|
||||
assertNotMatches(spec,"/aa/bb");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuffixSpec()
|
||||
{
|
||||
RegexPathSpec spec = new RegexPathSpec("^(.*).do$");
|
||||
assertEquals("Spec.pathSpec","^(.*).do$",spec.getDeclaration());
|
||||
assertEquals("Spec.pattern","^(.*).do$",spec.getPattern().pattern());
|
||||
assertEquals("Spec.pathDepth",0,spec.getPathDepth());
|
||||
assertEquals("Spec.group",PathSpecGroup.SUFFIX_GLOB,spec.group);
|
||||
|
||||
assertMatches(spec,"/a.do");
|
||||
assertMatches(spec,"/a/b/c.do");
|
||||
assertMatches(spec,"/abcde.do");
|
||||
assertMatches(spec,"/abc/efg.do");
|
||||
|
||||
assertNotMatches(spec,"/a");
|
||||
assertNotMatches(spec,"/aa");
|
||||
assertNotMatches(spec,"/aa/bb");
|
||||
assertNotMatches(spec,"/aa/bb.do/more");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.pathmap;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameter;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
/**
|
||||
* Tests of {@link PathMappings#getMatches(String)}
|
||||
*/
|
||||
@RunWith(Parameterized.class)
|
||||
public class ServletPathSpecMatchListTest
|
||||
{
|
||||
@Parameters(name="{0} = {1}")
|
||||
public static List<String[]> testCases()
|
||||
{
|
||||
String data[][] = new String[][] {
|
||||
// From old PathMapTest
|
||||
{ "All matches", "/animal/bird/path.tar.gz", "[/animal/bird/*=birds, /animal/*=animals, *.tar.gz=tarball, *.gz=gzipped, /=default]"},
|
||||
{ "Dir matches", "/animal/fish/", "[/animal/fish/*=fishes, /animal/*=animals, /=default]"},
|
||||
{ "Dir matches", "/animal/fish", "[/animal/fish/*=fishes, /animal/*=animals, /=default]"},
|
||||
{ "Root matches", "/", "[=root, /=default]"},
|
||||
{ "Dir matches", "", "[/=default]"}
|
||||
};
|
||||
|
||||
return Arrays.asList(data);
|
||||
}
|
||||
|
||||
private static PathMappings<String> mappings;
|
||||
|
||||
static {
|
||||
mappings = new PathMappings<>();
|
||||
|
||||
// From old PathMapTest
|
||||
mappings.put(new ServletPathSpec("/abs/path"),"abspath"); // 1
|
||||
mappings.put(new ServletPathSpec("/abs/path/longer"),"longpath"); // 2
|
||||
mappings.put(new ServletPathSpec("/animal/bird/*"),"birds"); // 3
|
||||
mappings.put(new ServletPathSpec("/animal/fish/*"),"fishes"); // 4
|
||||
mappings.put(new ServletPathSpec("/animal/*"),"animals"); // 5
|
||||
mappings.put(new ServletPathSpec("*.tar.gz"),"tarball"); // 6
|
||||
mappings.put(new ServletPathSpec("*.gz"),"gzipped"); // 7
|
||||
mappings.put(new ServletPathSpec("/"),"default"); // 8
|
||||
// 9 was the old Jetty ":" spec delimited case (no longer valid)
|
||||
mappings.put(new ServletPathSpec(""),"root"); // 10
|
||||
mappings.put(new ServletPathSpec("/\u20ACuro/*"),"money"); // 11
|
||||
}
|
||||
|
||||
@Parameter(0)
|
||||
public String message;
|
||||
|
||||
@Parameter(1)
|
||||
public String inputPath;
|
||||
|
||||
@Parameter(2)
|
||||
public String expectedListing;
|
||||
|
||||
@Test
|
||||
public void testGetMatches()
|
||||
{
|
||||
List<MappedResource<String>> matches = mappings.getMatches(inputPath);
|
||||
|
||||
StringBuilder actual = new StringBuilder();
|
||||
actual.append('[');
|
||||
boolean delim = false;
|
||||
for (MappedResource<String> res : matches)
|
||||
{
|
||||
if (delim)
|
||||
actual.append(", ");
|
||||
actual.append(res.getPathSpec().pathSpec).append('=').append(res.getResource());
|
||||
delim = true;
|
||||
}
|
||||
actual.append(']');
|
||||
|
||||
assertThat(message + " on [" + inputPath + "]",actual.toString(),is(expectedListing));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.pathmap;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameter;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
/**
|
||||
* Tests of {@link PathMappings#getMatch(String)}, with a focus on correct mapping selection order
|
||||
*/
|
||||
@RunWith(Parameterized.class)
|
||||
public class ServletPathSpecOrderTest
|
||||
{
|
||||
@Parameters(name="{0} = {1}")
|
||||
public static List<String[]> testCases()
|
||||
{
|
||||
String data[][] = new String[][] {
|
||||
// From old PathMapTest
|
||||
{"/abs/path","abspath"},
|
||||
{"/abs/path/xxx","default"},
|
||||
{"/abs/pith","default"},
|
||||
{"/abs/path/longer","longpath"},
|
||||
{"/abs/path/","default"},
|
||||
{"/abs/path/foo","default"},
|
||||
{"/animal/bird/eagle/bald","birds"},
|
||||
{"/animal/fish/shark/hammerhead","fishes"},
|
||||
{"/animal/insect/ladybug","animals"},
|
||||
{"/animal","animals"},
|
||||
{"/animal/","animals"},
|
||||
{"/animal/other","animals"},
|
||||
{"/animal/*","animals"},
|
||||
{"/downloads/distribution.tar.gz","tarball"},
|
||||
{"/downloads/script.gz","gzipped"},
|
||||
{"/animal/arhive.gz","animals"},
|
||||
{"/Other/path","default"},
|
||||
{"/\u20ACuro/path","money"},
|
||||
{"/","root"},
|
||||
|
||||
// Extra tests
|
||||
{"/downloads/readme.txt","default"},
|
||||
{"/downloads/logs.tgz","default"},
|
||||
{"/main.css","default"}
|
||||
};
|
||||
|
||||
return Arrays.asList(data);
|
||||
}
|
||||
|
||||
private static PathMappings<String> mappings;
|
||||
|
||||
static {
|
||||
mappings = new PathMappings<>();
|
||||
|
||||
// From old PathMapTest
|
||||
mappings.put(new ServletPathSpec("/abs/path"),"abspath"); // 1
|
||||
mappings.put(new ServletPathSpec("/abs/path/longer"),"longpath"); // 2
|
||||
mappings.put(new ServletPathSpec("/animal/bird/*"),"birds"); // 3
|
||||
mappings.put(new ServletPathSpec("/animal/fish/*"),"fishes"); // 4
|
||||
mappings.put(new ServletPathSpec("/animal/*"),"animals"); // 5
|
||||
mappings.put(new ServletPathSpec("*.tar.gz"),"tarball"); // 6
|
||||
mappings.put(new ServletPathSpec("*.gz"),"gzipped"); // 7
|
||||
mappings.put(new ServletPathSpec("/"),"default"); // 8
|
||||
// 9 was the old Jetty ":" spec delimited case (no longer valid)
|
||||
mappings.put(new ServletPathSpec(""),"root"); // 10
|
||||
mappings.put(new ServletPathSpec("/\u20ACuro/*"),"money"); // 11
|
||||
}
|
||||
|
||||
@Parameter(0)
|
||||
public String inputPath;
|
||||
|
||||
@Parameter(1)
|
||||
public String expectedResource;
|
||||
|
||||
@Test
|
||||
public void testMatch()
|
||||
{
|
||||
assertThat("Match on ["+ inputPath+ "]", mappings.getMatch(inputPath).getResource(), is(expectedResource));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.pathmap;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class ServletPathSpecTest
|
||||
{
|
||||
private void assertBadServletPathSpec(String pathSpec)
|
||||
{
|
||||
try
|
||||
{
|
||||
new ServletPathSpec(pathSpec);
|
||||
fail("Expected IllegalArgumentException for a bad servlet pathspec on: " + pathSpec);
|
||||
}
|
||||
catch (IllegalArgumentException e)
|
||||
{
|
||||
// expected path
|
||||
System.out.println(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertMatches(ServletPathSpec spec, String path)
|
||||
{
|
||||
String msg = String.format("Spec(\"%s\").matches(\"%s\")",spec.getDeclaration(),path);
|
||||
assertThat(msg,spec.matches(path),is(true));
|
||||
}
|
||||
|
||||
private void assertNotMatches(ServletPathSpec spec, String path)
|
||||
{
|
||||
String msg = String.format("!Spec(\"%s\").matches(\"%s\")",spec.getDeclaration(),path);
|
||||
assertThat(msg,spec.matches(path),is(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadServletPathSpecA()
|
||||
{
|
||||
assertBadServletPathSpec("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadServletPathSpecB()
|
||||
{
|
||||
assertBadServletPathSpec("/foo/*.do");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadServletPathSpecC()
|
||||
{
|
||||
assertBadServletPathSpec("foo/*.do");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadServletPathSpecD()
|
||||
{
|
||||
assertBadServletPathSpec("foo/*.*do");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadServletPathSpecE()
|
||||
{
|
||||
assertBadServletPathSpec("*do");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultPathSpec()
|
||||
{
|
||||
ServletPathSpec spec = new ServletPathSpec("/");
|
||||
assertEquals("Spec.pathSpec","/",spec.getDeclaration());
|
||||
assertEquals("Spec.pathDepth",-1,spec.getPathDepth());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExactPathSpec()
|
||||
{
|
||||
ServletPathSpec spec = new ServletPathSpec("/abs/path");
|
||||
assertEquals("Spec.pathSpec","/abs/path",spec.getDeclaration());
|
||||
assertEquals("Spec.pathDepth",2,spec.getPathDepth());
|
||||
|
||||
assertMatches(spec,"/abs/path");
|
||||
|
||||
assertNotMatches(spec,"/abs/path/");
|
||||
assertNotMatches(spec,"/abs/path/more");
|
||||
assertNotMatches(spec,"/foo");
|
||||
assertNotMatches(spec,"/foo/abs/path");
|
||||
assertNotMatches(spec,"/foo/abs/path/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetPathInfo()
|
||||
{
|
||||
assertEquals("pathInfo exact",null,new ServletPathSpec("/Foo/bar").getPathInfo("/Foo/bar"));
|
||||
assertEquals("pathInfo prefix","/bar",new ServletPathSpec("/Foo/*").getPathInfo("/Foo/bar"));
|
||||
assertEquals("pathInfo prefix","/*",new ServletPathSpec("/Foo/*").getPathInfo("/Foo/*"));
|
||||
assertEquals("pathInfo prefix","/",new ServletPathSpec("/Foo/*").getPathInfo("/Foo/"));
|
||||
assertEquals("pathInfo prefix",null,new ServletPathSpec("/Foo/*").getPathInfo("/Foo"));
|
||||
assertEquals("pathInfo suffix",null,new ServletPathSpec("*.ext").getPathInfo("/Foo/bar.ext"));
|
||||
assertEquals("pathInfo default",null,new ServletPathSpec("/").getPathInfo("/Foo/bar.ext"));
|
||||
|
||||
assertEquals("pathInfo default","/xxx/zzz",new ServletPathSpec("/*").getPathInfo("/xxx/zzz"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullPathSpec()
|
||||
{
|
||||
ServletPathSpec spec = new ServletPathSpec(null);
|
||||
assertEquals("Spec.pathSpec","",spec.getDeclaration());
|
||||
assertEquals("Spec.pathDepth",-1,spec.getPathDepth());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRootPathSpec()
|
||||
{
|
||||
ServletPathSpec spec = new ServletPathSpec("");
|
||||
assertEquals("Spec.pathSpec","",spec.getDeclaration());
|
||||
assertEquals("Spec.pathDepth",-1,spec.getPathDepth());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPathMatch()
|
||||
{
|
||||
assertEquals("pathMatch exact","/Foo/bar",new ServletPathSpec("/Foo/bar").getPathMatch("/Foo/bar"));
|
||||
assertEquals("pathMatch prefix","/Foo",new ServletPathSpec("/Foo/*").getPathMatch("/Foo/bar"));
|
||||
assertEquals("pathMatch prefix","/Foo",new ServletPathSpec("/Foo/*").getPathMatch("/Foo/"));
|
||||
assertEquals("pathMatch prefix","/Foo",new ServletPathSpec("/Foo/*").getPathMatch("/Foo"));
|
||||
assertEquals("pathMatch suffix","/Foo/bar.ext",new ServletPathSpec("*.ext").getPathMatch("/Foo/bar.ext"));
|
||||
assertEquals("pathMatch default","/Foo/bar.ext",new ServletPathSpec("/").getPathMatch("/Foo/bar.ext"));
|
||||
|
||||
assertEquals("pathMatch default","",new ServletPathSpec("/*").getPathMatch("/xxx/zzz"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrefixPathSpec()
|
||||
{
|
||||
ServletPathSpec spec = new ServletPathSpec("/downloads/*");
|
||||
assertEquals("Spec.pathSpec","/downloads/*",spec.getDeclaration());
|
||||
assertEquals("Spec.pathDepth",2,spec.getPathDepth());
|
||||
|
||||
assertMatches(spec,"/downloads/logo.jpg");
|
||||
assertMatches(spec,"/downloads/distribution.tar.gz");
|
||||
assertMatches(spec,"/downloads/distribution.tgz");
|
||||
assertMatches(spec,"/downloads/distribution.zip");
|
||||
|
||||
assertMatches(spec,"/downloads");
|
||||
|
||||
assertEquals("Spec.pathInfo","/",spec.getPathInfo("/downloads/"));
|
||||
assertEquals("Spec.pathInfo","/distribution.zip",spec.getPathInfo("/downloads/distribution.zip"));
|
||||
assertEquals("Spec.pathInfo","/dist/9.0/distribution.tar.gz",spec.getPathInfo("/downloads/dist/9.0/distribution.tar.gz"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuffixPathSpec()
|
||||
{
|
||||
ServletPathSpec spec = new ServletPathSpec("*.gz");
|
||||
assertEquals("Spec.pathSpec","*.gz",spec.getDeclaration());
|
||||
assertEquals("Spec.pathDepth",0,spec.getPathDepth());
|
||||
|
||||
assertMatches(spec,"/downloads/distribution.tar.gz");
|
||||
assertMatches(spec,"/downloads/jetty.log.gz");
|
||||
|
||||
assertNotMatches(spec,"/downloads/distribution.zip");
|
||||
assertNotMatches(spec,"/downloads/distribution.tgz");
|
||||
assertNotMatches(spec,"/abs/path");
|
||||
|
||||
assertEquals("Spec.pathInfo",null,spec.getPathInfo("/downloads/distribution.tar.gz"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.pathmap;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
/**
|
||||
* Tests for bad path specs on ServerEndpoint Path Param / URI Template
|
||||
*/
|
||||
@RunWith(Parameterized.class)
|
||||
public class UriTemplatePathSpecBadSpecsTest
|
||||
{
|
||||
private static void bad(List<String[]> data, String str)
|
||||
{
|
||||
data.add(new String[]
|
||||
{ str });
|
||||
}
|
||||
|
||||
@Parameters
|
||||
public static Collection<String[]> data()
|
||||
{
|
||||
List<String[]> data = new ArrayList<>();
|
||||
bad(data,"/a/b{var}"); // bad syntax - variable does not encompass whole path segment
|
||||
bad(data,"a/{var}"); // bad syntax - no start slash
|
||||
bad(data,"/a/{var/b}"); // path segment separator in variable name
|
||||
bad(data,"/{var}/*"); // bad syntax - no globs allowed
|
||||
bad(data,"/{var}.do"); // bad syntax - variable does not encompass whole path segment
|
||||
bad(data,"/a/{var*}"); // use of glob character not allowed in variable name
|
||||
bad(data,"/a/{}"); // bad syntax - no variable name
|
||||
// MIGHT BE ALLOWED bad(data,"/a/{---}"); // no alpha in variable name
|
||||
bad(data,"{var}"); // bad syntax - no start slash
|
||||
bad(data,"/a/{my special variable}"); // bad syntax - space in variable name
|
||||
bad(data,"/a/{var}/{var}"); // variable name duplicate
|
||||
// MIGHT BE ALLOWED bad(data,"/a/{var}/{Var}/{vAR}"); // variable name duplicated (diff case)
|
||||
bad(data,"/a/../../../{var}"); // path navigation not allowed
|
||||
bad(data,"/a/./{var}"); // path navigation not allowed
|
||||
bad(data,"/a//{var}"); // bad syntax - double path slash (no path segment)
|
||||
return data;
|
||||
}
|
||||
|
||||
private String pathSpec;
|
||||
|
||||
public UriTemplatePathSpecBadSpecsTest(String pathSpec)
|
||||
{
|
||||
this.pathSpec = pathSpec;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadPathSpec()
|
||||
{
|
||||
try
|
||||
{
|
||||
new UriTemplatePathSpec(this.pathSpec);
|
||||
fail("Expected IllegalArgumentException for a bad PathParam pathspec on: " + pathSpec);
|
||||
}
|
||||
catch (IllegalArgumentException e)
|
||||
{
|
||||
// expected path
|
||||
System.out.println(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,284 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.pathmap;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Tests for URI Template Path Specs
|
||||
*/
|
||||
public class UriTemplatePathSpecTest
|
||||
{
|
||||
private void assertDetectedVars(UriTemplatePathSpec spec, String... expectedVars)
|
||||
{
|
||||
String prefix = String.format("Spec(\"%s\")",spec.getDeclaration());
|
||||
assertEquals(prefix + ".variableCount",expectedVars.length,spec.getVariableCount());
|
||||
assertEquals(prefix + ".variable.length",expectedVars.length,spec.getVariables().length);
|
||||
for (int i = 0; i < expectedVars.length; i++)
|
||||
{
|
||||
assertEquals(String.format("%s.variable[%d]",prefix,i),expectedVars[i],spec.getVariables()[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertMatches(PathSpec spec, String path)
|
||||
{
|
||||
String msg = String.format("Spec(\"%s\").matches(\"%s\")",spec.getDeclaration(),path);
|
||||
assertThat(msg,spec.matches(path),is(true));
|
||||
}
|
||||
|
||||
private void assertNotMatches(PathSpec spec, String path)
|
||||
{
|
||||
String msg = String.format("!Spec(\"%s\").matches(\"%s\")",spec.getDeclaration(),path);
|
||||
assertThat(msg,spec.matches(path),is(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultPathSpec()
|
||||
{
|
||||
UriTemplatePathSpec spec = new UriTemplatePathSpec("/");
|
||||
assertEquals("Spec.pathSpec","/",spec.getDeclaration());
|
||||
assertEquals("Spec.pattern","^/$",spec.getPattern().pattern());
|
||||
assertEquals("Spec.pathDepth",1,spec.getPathDepth());
|
||||
assertEquals("Spec.group",PathSpecGroup.EXACT,spec.getGroup());
|
||||
|
||||
assertEquals("Spec.variableCount",0,spec.getVariableCount());
|
||||
assertEquals("Spec.variable.length",0,spec.getVariables().length);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExactOnePathSpec()
|
||||
{
|
||||
UriTemplatePathSpec spec = new UriTemplatePathSpec("/a");
|
||||
assertEquals("Spec.pathSpec","/a",spec.getDeclaration());
|
||||
assertEquals("Spec.pattern","^/a$",spec.getPattern().pattern());
|
||||
assertEquals("Spec.pathDepth",1,spec.getPathDepth());
|
||||
assertEquals("Spec.group",PathSpecGroup.EXACT,spec.getGroup());
|
||||
|
||||
assertMatches(spec,"/a");
|
||||
assertMatches(spec,"/a?type=other");
|
||||
assertNotMatches(spec,"/a/b");
|
||||
assertNotMatches(spec,"/a/");
|
||||
|
||||
assertEquals("Spec.variableCount",0,spec.getVariableCount());
|
||||
assertEquals("Spec.variable.length",0,spec.getVariables().length);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExactPathSpec_TestWebapp()
|
||||
{
|
||||
UriTemplatePathSpec spec = new UriTemplatePathSpec("/deep.thought/");
|
||||
assertEquals("Spec.pathSpec","/deep.thought/",spec.getDeclaration());
|
||||
assertEquals("Spec.pattern","^/deep\\.thought/$",spec.getPattern().pattern());
|
||||
assertEquals("Spec.pathDepth",1,spec.getPathDepth());
|
||||
assertEquals("Spec.group",PathSpecGroup.EXACT,spec.getGroup());
|
||||
|
||||
assertMatches(spec,"/deep.thought/");
|
||||
assertNotMatches(spec,"/deep.thought");
|
||||
|
||||
assertEquals("Spec.variableCount",0,spec.getVariableCount());
|
||||
assertEquals("Spec.variable.length",0,spec.getVariables().length);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExactTwoPathSpec()
|
||||
{
|
||||
UriTemplatePathSpec spec = new UriTemplatePathSpec("/a/b");
|
||||
assertEquals("Spec.pathSpec","/a/b",spec.getDeclaration());
|
||||
assertEquals("Spec.pattern","^/a/b$",spec.getPattern().pattern());
|
||||
assertEquals("Spec.pathDepth",2,spec.getPathDepth());
|
||||
assertEquals("Spec.group",PathSpecGroup.EXACT,spec.getGroup());
|
||||
|
||||
assertEquals("Spec.variableCount",0,spec.getVariableCount());
|
||||
assertEquals("Spec.variable.length",0,spec.getVariables().length);
|
||||
|
||||
assertMatches(spec,"/a/b");
|
||||
|
||||
assertNotMatches(spec,"/a/b/");
|
||||
assertNotMatches(spec,"/a/");
|
||||
assertNotMatches(spec,"/a/bb");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMiddleVarPathSpec()
|
||||
{
|
||||
UriTemplatePathSpec spec = new UriTemplatePathSpec("/a/{var}/c");
|
||||
assertEquals("Spec.pathSpec","/a/{var}/c",spec.getDeclaration());
|
||||
assertEquals("Spec.pattern","^/a/([^/]+)/c$",spec.getPattern().pattern());
|
||||
assertEquals("Spec.pathDepth",3,spec.getPathDepth());
|
||||
assertEquals("Spec.group",PathSpecGroup.MIDDLE_GLOB,spec.getGroup());
|
||||
|
||||
assertDetectedVars(spec,"var");
|
||||
|
||||
assertMatches(spec,"/a/b/c");
|
||||
assertMatches(spec,"/a/zz/c");
|
||||
assertMatches(spec,"/a/hello+world/c");
|
||||
assertNotMatches(spec,"/a/bc");
|
||||
assertNotMatches(spec,"/a/b/");
|
||||
assertNotMatches(spec,"/a/b");
|
||||
|
||||
Map<String, String> mapped = spec.getPathParams("/a/b/c");
|
||||
assertThat("Spec.pathParams",mapped,notNullValue());
|
||||
assertThat("Spec.pathParams.size",mapped.size(),is(1));
|
||||
assertEquals("Spec.pathParams[var]","b",mapped.get("var"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOneVarPathSpec()
|
||||
{
|
||||
UriTemplatePathSpec spec = new UriTemplatePathSpec("/a/{foo}");
|
||||
assertEquals("Spec.pathSpec","/a/{foo}",spec.getDeclaration());
|
||||
assertEquals("Spec.pattern","^/a/([^/]+)$",spec.getPattern().pattern());
|
||||
assertEquals("Spec.pathDepth",2,spec.getPathDepth());
|
||||
assertEquals("Spec.group",PathSpecGroup.PREFIX_GLOB,spec.getGroup());
|
||||
|
||||
assertDetectedVars(spec,"foo");
|
||||
|
||||
assertMatches(spec,"/a/b");
|
||||
assertNotMatches(spec,"/a/");
|
||||
assertNotMatches(spec,"/a");
|
||||
|
||||
Map<String, String> mapped = spec.getPathParams("/a/b");
|
||||
assertThat("Spec.pathParams",mapped,notNullValue());
|
||||
assertThat("Spec.pathParams.size",mapped.size(),is(1));
|
||||
assertEquals("Spec.pathParams[foo]","b",mapped.get("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOneVarSuffixPathSpec()
|
||||
{
|
||||
UriTemplatePathSpec spec = new UriTemplatePathSpec("/{var}/b/c");
|
||||
assertEquals("Spec.pathSpec","/{var}/b/c",spec.getDeclaration());
|
||||
assertEquals("Spec.pattern","^/([^/]+)/b/c$",spec.getPattern().pattern());
|
||||
assertEquals("Spec.pathDepth",3,spec.getPathDepth());
|
||||
assertEquals("Spec.group",PathSpecGroup.SUFFIX_GLOB,spec.getGroup());
|
||||
|
||||
assertDetectedVars(spec,"var");
|
||||
|
||||
assertMatches(spec,"/a/b/c");
|
||||
assertMatches(spec,"/az/b/c");
|
||||
assertMatches(spec,"/hello+world/b/c");
|
||||
assertNotMatches(spec,"/a/bc");
|
||||
assertNotMatches(spec,"/a/b/");
|
||||
assertNotMatches(spec,"/a/b");
|
||||
|
||||
Map<String, String> mapped = spec.getPathParams("/a/b/c");
|
||||
assertThat("Spec.pathParams",mapped,notNullValue());
|
||||
assertThat("Spec.pathParams.size",mapped.size(),is(1));
|
||||
assertEquals("Spec.pathParams[var]","a",mapped.get("var"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTwoVarComplexInnerPathSpec()
|
||||
{
|
||||
UriTemplatePathSpec spec = new UriTemplatePathSpec("/a/{var1}/c/{var2}/e");
|
||||
assertEquals("Spec.pathSpec","/a/{var1}/c/{var2}/e",spec.getDeclaration());
|
||||
assertEquals("Spec.pattern","^/a/([^/]+)/c/([^/]+)/e$",spec.getPattern().pattern());
|
||||
assertEquals("Spec.pathDepth",5,spec.getPathDepth());
|
||||
assertEquals("Spec.group",PathSpecGroup.MIDDLE_GLOB,spec.getGroup());
|
||||
|
||||
assertDetectedVars(spec,"var1","var2");
|
||||
|
||||
assertMatches(spec,"/a/b/c/d/e");
|
||||
assertNotMatches(spec,"/a/bc/d/e");
|
||||
assertNotMatches(spec,"/a/b/d/e");
|
||||
assertNotMatches(spec,"/a/b//d/e");
|
||||
|
||||
Map<String, String> mapped = spec.getPathParams("/a/b/c/d/e");
|
||||
assertThat("Spec.pathParams",mapped,notNullValue());
|
||||
assertThat("Spec.pathParams.size",mapped.size(),is(2));
|
||||
assertEquals("Spec.pathParams[var1]","b",mapped.get("var1"));
|
||||
assertEquals("Spec.pathParams[var2]","d",mapped.get("var2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTwoVarComplexOuterPathSpec()
|
||||
{
|
||||
UriTemplatePathSpec spec = new UriTemplatePathSpec("/{var1}/b/{var2}/{var3}");
|
||||
assertEquals("Spec.pathSpec","/{var1}/b/{var2}/{var3}",spec.getDeclaration());
|
||||
assertEquals("Spec.pattern","^/([^/]+)/b/([^/]+)/([^/]+)$",spec.getPattern().pattern());
|
||||
assertEquals("Spec.pathDepth",4,spec.getPathDepth());
|
||||
assertEquals("Spec.group",PathSpecGroup.MIDDLE_GLOB,spec.getGroup());
|
||||
|
||||
assertDetectedVars(spec,"var1","var2","var3");
|
||||
|
||||
assertMatches(spec,"/a/b/c/d");
|
||||
assertNotMatches(spec,"/a/bc/d/e");
|
||||
assertNotMatches(spec,"/a/c/d/e");
|
||||
assertNotMatches(spec,"/a//d/e");
|
||||
|
||||
Map<String, String> mapped = spec.getPathParams("/a/b/c/d");
|
||||
assertThat("Spec.pathParams",mapped,notNullValue());
|
||||
assertThat("Spec.pathParams.size",mapped.size(),is(3));
|
||||
assertEquals("Spec.pathParams[var1]","a",mapped.get("var1"));
|
||||
assertEquals("Spec.pathParams[var2]","c",mapped.get("var2"));
|
||||
assertEquals("Spec.pathParams[var3]","d",mapped.get("var3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTwoVarPrefixPathSpec()
|
||||
{
|
||||
UriTemplatePathSpec spec = new UriTemplatePathSpec("/a/{var1}/{var2}");
|
||||
assertEquals("Spec.pathSpec","/a/{var1}/{var2}",spec.getDeclaration());
|
||||
assertEquals("Spec.pattern","^/a/([^/]+)/([^/]+)$",spec.getPattern().pattern());
|
||||
assertEquals("Spec.pathDepth",3,spec.getPathDepth());
|
||||
assertEquals("Spec.group",PathSpecGroup.PREFIX_GLOB,spec.getGroup());
|
||||
|
||||
assertDetectedVars(spec,"var1","var2");
|
||||
|
||||
assertMatches(spec,"/a/b/c");
|
||||
assertNotMatches(spec,"/a/bc");
|
||||
assertNotMatches(spec,"/a/b/");
|
||||
assertNotMatches(spec,"/a/b");
|
||||
|
||||
Map<String, String> mapped = spec.getPathParams("/a/b/c");
|
||||
assertThat("Spec.pathParams",mapped,notNullValue());
|
||||
assertThat("Spec.pathParams.size",mapped.size(),is(2));
|
||||
assertEquals("Spec.pathParams[var1]","b",mapped.get("var1"));
|
||||
assertEquals("Spec.pathParams[var2]","c",mapped.get("var2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVarOnlyPathSpec()
|
||||
{
|
||||
UriTemplatePathSpec spec = new UriTemplatePathSpec("/{var1}");
|
||||
assertEquals("Spec.pathSpec","/{var1}",spec.getDeclaration());
|
||||
assertEquals("Spec.pattern","^/([^/]+)$",spec.getPattern().pattern());
|
||||
assertEquals("Spec.pathDepth",1,spec.getPathDepth());
|
||||
assertEquals("Spec.group",PathSpecGroup.PREFIX_GLOB,spec.getGroup());
|
||||
|
||||
assertDetectedVars(spec,"var1");
|
||||
|
||||
assertMatches(spec,"/a");
|
||||
assertNotMatches(spec,"/");
|
||||
assertNotMatches(spec,"/a/b");
|
||||
assertNotMatches(spec,"/a/b/c");
|
||||
|
||||
Map<String, String> mapped = spec.getPathParams("/a");
|
||||
assertThat("Spec.pathParams",mapped,notNullValue());
|
||||
assertThat("Spec.pathParams.size",mapped.size(),is(1));
|
||||
assertEquals("Spec.pathParams[var1]","a",mapped.get("var1"));
|
||||
}
|
||||
}
|
|
@ -39,6 +39,7 @@ import org.eclipse.jetty.http.HttpScheme;
|
|||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http2.ErrorCode;
|
||||
import org.eclipse.jetty.http2.api.Session;
|
||||
import org.eclipse.jetty.http2.api.Stream;
|
||||
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
|
||||
|
@ -585,6 +586,48 @@ public class HTTP2Test extends AbstractTest
|
|||
Assert.assertTrue(completeLatch.await(5, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCleanGoAwayDoesNotTriggerFailureNotification() throws Exception
|
||||
{
|
||||
start(new ServerSessionListener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
|
||||
{
|
||||
MetaData.Response metaData = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, new HttpFields());
|
||||
HeadersFrame response = new HeadersFrame(stream.getId(), metaData, null, true);
|
||||
stream.headers(response, Callback.NOOP);
|
||||
// Close cleanly.
|
||||
stream.getSession().close(ErrorCode.NO_ERROR.code, null, Callback.NOOP);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
CountDownLatch closeLatch = new CountDownLatch(1);
|
||||
CountDownLatch failureLatch = new CountDownLatch(1);
|
||||
Session session = newClient(new Session.Listener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public void onClose(Session session, GoAwayFrame frame)
|
||||
{
|
||||
closeLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Session session, Throwable failure)
|
||||
{
|
||||
failureLatch.countDown();
|
||||
}
|
||||
});
|
||||
MetaData.Request metaData = newRequest("GET", new HttpFields());
|
||||
HeadersFrame request = new HeadersFrame(metaData, null, true);
|
||||
session.newStream(request, new Promise.Adapter<>(), new Stream.Listener.Adapter());
|
||||
|
||||
// Make sure onClose() is called.
|
||||
Assert.assertTrue(closeLatch.await(5, TimeUnit.SECONDS));
|
||||
Assert.assertFalse(failureLatch.await(1, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
private static void sleep(long time)
|
||||
{
|
||||
try
|
||||
|
|
|
@ -230,7 +230,7 @@ public class HTTP2Flusher extends IteratingCallback
|
|||
if (actives.isEmpty())
|
||||
{
|
||||
if (isClosed())
|
||||
abort(new ClosedChannelException());
|
||||
fail(new ClosedChannelException(), true);
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Flushed {}", session);
|
||||
|
@ -289,9 +289,6 @@ public class HTTP2Flusher extends IteratingCallback
|
|||
@Override
|
||||
protected void onCompleteFailure(Throwable x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Failed", x);
|
||||
|
||||
lease.recycle();
|
||||
|
||||
// Transfer active items to avoid reentrancy.
|
||||
|
@ -306,10 +303,10 @@ public class HTTP2Flusher extends IteratingCallback
|
|||
entry.failed(x);
|
||||
}
|
||||
|
||||
abort(x);
|
||||
fail(x, isClosed());
|
||||
}
|
||||
|
||||
private void abort(Throwable x)
|
||||
private void fail(Throwable x, boolean closed)
|
||||
{
|
||||
Queue<Entry> queued;
|
||||
synchronized (this)
|
||||
|
@ -319,12 +316,13 @@ public class HTTP2Flusher extends IteratingCallback
|
|||
}
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Aborting, queued={}", queued.size());
|
||||
LOG.debug("{}, queued={}", closed ? "Closing" : "Failing", queued.size());
|
||||
|
||||
for (Entry entry : queued)
|
||||
closed(entry, x);
|
||||
entry.failed(x);
|
||||
|
||||
session.abort(x);
|
||||
if (!closed)
|
||||
session.abort(x);
|
||||
}
|
||||
|
||||
private void closed(Entry entry, Throwable failure)
|
||||
|
|
|
@ -989,8 +989,16 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
|
|||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("%s@%x{queueSize=%d,sendWindow=%s,recvWindow=%s,streams=%d,%s}", getClass().getSimpleName(),
|
||||
hashCode(), flusher.getQueueSize(), sendWindow, recvWindow, streams.size(), closed);
|
||||
return String.format("%s@%x{l:%s <-> r:%s,queueSize=%d,sendWindow=%s,recvWindow=%s,streams=%d,%s}",
|
||||
getClass().getSimpleName(),
|
||||
hashCode(),
|
||||
getEndPoint().getLocalAddress(),
|
||||
getEndPoint().getRemoteAddress(),
|
||||
flusher.getQueueSize(),
|
||||
sendWindow,
|
||||
recvWindow,
|
||||
streams.size(),
|
||||
closed);
|
||||
}
|
||||
|
||||
private class ControlEntry extends HTTP2Flusher.Entry
|
||||
|
|
|
@ -61,6 +61,23 @@
|
|||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.toolchain</groupId>
|
||||
<artifactId>jetty-test-helper</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.http2</groupId>
|
||||
<artifactId>http2-server</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
|
|
|
@ -32,6 +32,8 @@ import org.eclipse.jetty.http.HttpScheme;
|
|||
import org.eclipse.jetty.http2.api.Session;
|
||||
import org.eclipse.jetty.http2.client.HTTP2Client;
|
||||
import org.eclipse.jetty.http2.client.HTTP2ClientConnectionFactory;
|
||||
import org.eclipse.jetty.http2.frames.GoAwayFrame;
|
||||
import org.eclipse.jetty.http2.frames.SettingsFrame;
|
||||
import org.eclipse.jetty.io.ClientConnectionFactory;
|
||||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.io.ssl.SslClientConnectionFactory;
|
||||
|
@ -105,39 +107,17 @@ public class HttpClientTransportOverHTTP2 extends ContainerLifeCycle implements
|
|||
{
|
||||
client.setConnectTimeout(httpClient.getConnectTimeout());
|
||||
|
||||
final HttpDestination destination = (HttpDestination)context.get(HTTP_DESTINATION_CONTEXT_KEY);
|
||||
HttpDestinationOverHTTP2 destination = (HttpDestinationOverHTTP2)context.get(HTTP_DESTINATION_CONTEXT_KEY);
|
||||
@SuppressWarnings("unchecked")
|
||||
final Promise<Connection> connection = (Promise<Connection>)context.get(HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
|
||||
Promise<Connection> connectionPromise = (Promise<Connection>)context.get(HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
|
||||
|
||||
Session.Listener listener = new Session.Listener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public void onFailure(Session session, Throwable failure)
|
||||
{
|
||||
destination.abort(failure);
|
||||
}
|
||||
};
|
||||
|
||||
final Promise<Session> promise = new Promise<Session>()
|
||||
{
|
||||
@Override
|
||||
public void succeeded(Session session)
|
||||
{
|
||||
connection.succeeded(newHttpConnection(destination, session));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(Throwable failure)
|
||||
{
|
||||
connection.failed(failure);
|
||||
}
|
||||
};
|
||||
SessionListenerPromise listenerPromise = new SessionListenerPromise(destination, connectionPromise);
|
||||
|
||||
SslContextFactory sslContextFactory = null;
|
||||
if (HttpScheme.HTTPS.is(destination.getScheme()))
|
||||
sslContextFactory = httpClient.getSslContextFactory();
|
||||
|
||||
client.connect(sslContextFactory, address, listener, promise, context);
|
||||
client.connect(sslContextFactory, address, listenerPromise, listenerPromise, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -154,4 +134,50 @@ public class HttpClientTransportOverHTTP2 extends ContainerLifeCycle implements
|
|||
{
|
||||
return new HttpConnectionOverHTTP2(destination, session);
|
||||
}
|
||||
|
||||
private class SessionListenerPromise extends Session.Listener.Adapter implements Promise<Session>
|
||||
{
|
||||
private final HttpDestinationOverHTTP2 destination;
|
||||
private final Promise<Connection> promise;
|
||||
private HttpConnectionOverHTTP2 connection;
|
||||
|
||||
public SessionListenerPromise(HttpDestinationOverHTTP2 destination, Promise<Connection> promise)
|
||||
{
|
||||
this.destination = destination;
|
||||
this.promise = promise;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void succeeded(Session session)
|
||||
{
|
||||
connection = newHttpConnection(destination, session);
|
||||
promise.succeeded(connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(Throwable failure)
|
||||
{
|
||||
promise.failed(failure);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSettings(Session session, SettingsFrame frame)
|
||||
{
|
||||
Map<Integer, Integer> settings = frame.getSettings();
|
||||
if (settings.containsKey(SettingsFrame.MAX_CONCURRENT_STREAMS))
|
||||
destination.setMaxRequestsPerConnection(settings.get(SettingsFrame.MAX_CONCURRENT_STREAMS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose(Session session, GoAwayFrame frame)
|
||||
{
|
||||
connection.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Session session, Throwable failure)
|
||||
{
|
||||
connection.close(failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,16 +57,22 @@ public class HttpConnectionOverHTTP2 extends HttpConnection
|
|||
protected void release(HttpChannel channel)
|
||||
{
|
||||
channels.remove(channel);
|
||||
getHttpDestination().release(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
close(new AsynchronousCloseException());
|
||||
}
|
||||
|
||||
protected void close(Throwable failure)
|
||||
{
|
||||
// First close then abort, to be sure that the connection cannot be reused
|
||||
// from an onFailure() handler or by blocking code waiting for completion.
|
||||
getHttpDestination().close(this);
|
||||
session.close(ErrorCode.NO_ERROR.code, null, Callback.NOOP);
|
||||
abort(new AsynchronousCloseException());
|
||||
abort(failure);
|
||||
}
|
||||
|
||||
private void abort(Throwable failure)
|
||||
|
@ -79,4 +85,13 @@ public class HttpConnectionOverHTTP2 extends HttpConnection
|
|||
}
|
||||
channels.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("%s@%h[%s]",
|
||||
getClass().getSimpleName(),
|
||||
this,
|
||||
session);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http2.client.http;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.http2.client.HTTP2Client;
|
||||
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.toolchain.test.TestTracker;
|
||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
import org.junit.After;
|
||||
import org.junit.Rule;
|
||||
|
||||
public class AbstractTest
|
||||
{
|
||||
@Rule
|
||||
public TestTracker tracker = new TestTracker();
|
||||
protected Server server;
|
||||
protected ServerConnector connector;
|
||||
protected HttpClient client;
|
||||
|
||||
protected void start(int maxConcurrentStreams, Handler handler) throws Exception
|
||||
{
|
||||
QueuedThreadPool serverExecutor = new QueuedThreadPool();
|
||||
serverExecutor.setName("server");
|
||||
server = new Server(serverExecutor);
|
||||
|
||||
HTTP2ServerConnectionFactory http2 = new HTTP2ServerConnectionFactory(new HttpConfiguration());
|
||||
http2.setMaxConcurrentStreams(maxConcurrentStreams);
|
||||
connector = new ServerConnector(server, 1, 1, http2);
|
||||
server.addConnector(connector);
|
||||
|
||||
server.setHandler(handler);
|
||||
server.start();
|
||||
|
||||
client = new HttpClient(new HttpClientTransportOverHTTP2(new HTTP2Client()), null);
|
||||
QueuedThreadPool clientExecutor = new QueuedThreadPool();
|
||||
clientExecutor.setName("client");
|
||||
client.setExecutor(clientExecutor);
|
||||
client.start();
|
||||
}
|
||||
|
||||
@After
|
||||
public void dispose() throws Exception
|
||||
{
|
||||
if (client != null)
|
||||
client.stop();
|
||||
if (server != null)
|
||||
server.stop();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http2.client.http;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.handler.AbstractHandler;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class MaxConcurrentStreamsTest extends AbstractTest
|
||||
{
|
||||
@Test
|
||||
public void testOneConcurrentStream() throws Exception
|
||||
{
|
||||
long sleep = 1000;
|
||||
start(1, new AbstractHandler()
|
||||
{
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
{
|
||||
baseRequest.setHandled(true);
|
||||
// Sleep a bit to allow the second request to be queued.
|
||||
sleep(sleep);
|
||||
}
|
||||
});
|
||||
client.setMaxConnectionsPerDestination(1);
|
||||
|
||||
// Prime the connection so that the maxConcurrentStream setting arrives to the client.
|
||||
client.newRequest("localhost", connector.getLocalPort()).path("/prime").send();
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(2);
|
||||
|
||||
// First request is sent immediately.
|
||||
client.newRequest("localhost", connector.getLocalPort())
|
||||
.path("/first")
|
||||
.send(result ->
|
||||
{
|
||||
if (result.isSucceeded())
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
// Second request is queued.
|
||||
client.newRequest("localhost", connector.getLocalPort())
|
||||
.path("/second")
|
||||
.send(result ->
|
||||
{
|
||||
if (result.isSucceeded())
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
// When the first request returns, the second must be sent.
|
||||
Assert.assertTrue(latch.await(5 * sleep, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTwoConcurrentStreamsThirdWaits() throws Exception
|
||||
{
|
||||
int maxStreams = 2;
|
||||
long sleep = 1000;
|
||||
start(maxStreams, new AbstractHandler()
|
||||
{
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
{
|
||||
baseRequest.setHandled(true);
|
||||
sleep(sleep);
|
||||
}
|
||||
});
|
||||
client.setMaxConnectionsPerDestination(1);
|
||||
|
||||
// Prime the connection so that the maxConcurrentStream setting arrives to the client.
|
||||
client.newRequest("localhost", connector.getLocalPort()).path("/prime").send();
|
||||
|
||||
// Send requests up to the max allowed.
|
||||
for (int i = 0; i < maxStreams; ++i)
|
||||
{
|
||||
client.newRequest("localhost", connector.getLocalPort())
|
||||
.path("/" + i)
|
||||
.send(null);
|
||||
}
|
||||
|
||||
// Send the request in excess.
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
String path = "/excess";
|
||||
client.newRequest("localhost", connector.getLocalPort())
|
||||
.path(path)
|
||||
.send(result ->
|
||||
{
|
||||
if (result.getResponse().getStatus() == HttpStatus.OK_200)
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
// The last exchange should remain in the queue.
|
||||
HttpDestinationOverHTTP2 destination = (HttpDestinationOverHTTP2)client.getDestination("http", "localhost", connector.getLocalPort());
|
||||
Assert.assertEquals(1, destination.getHttpExchanges().size());
|
||||
Assert.assertEquals(path, destination.getHttpExchanges().peek().getRequest().getPath());
|
||||
|
||||
Assert.assertTrue(latch.await(5 * sleep, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAbortedWhileQueued() throws Exception
|
||||
{
|
||||
long sleep = 1000;
|
||||
start(1, new AbstractHandler()
|
||||
{
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
{
|
||||
baseRequest.setHandled(true);
|
||||
sleep(sleep);
|
||||
}
|
||||
});
|
||||
client.setMaxConnectionsPerDestination(1);
|
||||
|
||||
// Prime the connection so that the maxConcurrentStream setting arrives to the client.
|
||||
client.newRequest("localhost", connector.getLocalPort()).path("/prime").send();
|
||||
|
||||
// Send a request that is aborted while queued.
|
||||
client.newRequest("localhost", connector.getLocalPort())
|
||||
.path("/aborted")
|
||||
.onRequestQueued(request -> request.abort(new Exception()))
|
||||
.send(null);
|
||||
|
||||
// Must be able to send another request.
|
||||
ContentResponse response = client.newRequest("localhost", connector.getLocalPort()).path("/check").send();
|
||||
|
||||
Assert.assertEquals(HttpStatus.OK_200, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleRequestsQueuedOnConnect() throws Exception
|
||||
{
|
||||
int maxConcurrent = 10;
|
||||
long sleep = 500;
|
||||
start(maxConcurrent, new AbstractHandler()
|
||||
{
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
{
|
||||
baseRequest.setHandled(true);
|
||||
sleep(sleep);
|
||||
}
|
||||
});
|
||||
client.setMaxConnectionsPerDestination(1);
|
||||
|
||||
// The first request will open the connection, the others will be queued.
|
||||
CountDownLatch latch = new CountDownLatch(maxConcurrent);
|
||||
for (int i = 0; i < maxConcurrent; ++i)
|
||||
{
|
||||
client.newRequest("localhost", connector.getLocalPort())
|
||||
.path("/" + i)
|
||||
.send(result -> latch.countDown());
|
||||
}
|
||||
|
||||
// The requests should be processed in parallel, not sequentially.
|
||||
Assert.assertTrue(latch.await(maxConcurrent * sleep / 2, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
|
||||
private void sleep(long time)
|
||||
{
|
||||
try
|
||||
{
|
||||
Thread.sleep(time);
|
||||
}
|
||||
catch (InterruptedException x)
|
||||
{
|
||||
throw new RuntimeException(x);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -36,7 +36,7 @@ import org.eclipse.jetty.http.HttpHeader;
|
|||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http.MimeTypes;
|
||||
import org.eclipse.jetty.http.PathMap;
|
||||
import org.eclipse.jetty.http.pathmap.PathSpecSet;
|
||||
import org.eclipse.jetty.server.HttpOutput;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.handler.HandlerWrapper;
|
||||
|
@ -72,9 +72,9 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
|
|||
// non-static, as other GzipHandler instances may have different configurations
|
||||
private final ThreadLocal<Deflater> _deflater = new ThreadLocal<Deflater>();
|
||||
|
||||
private final IncludeExclude<String> _agentPatterns=new IncludeExclude<>(RegexSet.class,RegexSet.MATCHER);
|
||||
private final IncludeExclude<String> _agentPatterns=new IncludeExclude<>(RegexSet.class);
|
||||
private final IncludeExclude<String> _methods = new IncludeExclude<>();
|
||||
private final IncludeExclude<String> _paths = new IncludeExclude<>(PathMap.PathSet.class,PathMap.PathSet.MATCHER);
|
||||
private final IncludeExclude<String> _paths = new IncludeExclude<String>(PathSpecSet.class);
|
||||
private final IncludeExclude<String> _mimeTypes = new IncludeExclude<>();
|
||||
|
||||
private HttpField _vary;
|
||||
|
@ -144,9 +144,27 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
|
|||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* Add path to excluded paths list.
|
||||
* <p>
|
||||
* There are 2 syntaxes supported, Servlet <code>url-pattern</code> based, and
|
||||
* Regex based. This means that the initial characters on the path spec
|
||||
* line are very strict, and determine the behavior of the path matching.
|
||||
* <ul>
|
||||
* <li>If the spec starts with <code>'^'</code> the spec is assumed to be
|
||||
* a regex based path spec and will match with normal Java regex rules.</li>
|
||||
* <li>If the spec starts with <code>'/'</code> then spec is assumed to be
|
||||
* a Servlet url-pattern rules path spec for either an exact match
|
||||
* or prefix based match.</li>
|
||||
* <li>If the spec starts with <code>'*.'</code> then spec is assumed to be
|
||||
* a Servlet url-pattern rules path spec for a suffix based match.</li>
|
||||
* <li>All other syntaxes are unsupported</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Note: inclusion takes precedence over exclude.
|
||||
*
|
||||
* @param pathspecs Path specs (as per servlet spec) to exclude. If a
|
||||
* ServletContext is available, the paths are relative to the context path,
|
||||
* otherwise they are absolute.
|
||||
* otherwise they are absolute.<br>
|
||||
* For backward compatibility the pathspecs may be comma separated strings, but this
|
||||
* will not be supported in future versions.
|
||||
*/
|
||||
|
@ -191,12 +209,27 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
|
|||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* Add path specs to include. Inclusion takes precedence over exclusion.
|
||||
* Add path specs to include.
|
||||
* <p>
|
||||
* There are 2 syntaxes supported, Servlet <code>url-pattern</code> based, and
|
||||
* Regex based. This means that the initial characters on the path spec
|
||||
* line are very strict, and determine the behavior of the path matching.
|
||||
* <ul>
|
||||
* <li>If the spec starts with <code>'^'</code> the spec is assumed to be
|
||||
* a regex based path spec and will match with normal Java regex rules.</li>
|
||||
* <li>If the spec starts with <code>'/'</code> then spec is assumed to be
|
||||
* a Servlet url-pattern rules path spec for either an exact match
|
||||
* or prefix based match.</li>
|
||||
* <li>If the spec starts with <code>'*.'</code> then spec is assumed to be
|
||||
* a Servlet url-pattern rules path spec for a suffix based match.</li>
|
||||
* <li>All other syntaxes are unsupported</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Note: inclusion takes precedence over exclude.
|
||||
*
|
||||
* @param pathspecs Path specs (as per servlet spec) to include. If a
|
||||
* ServletContext is available, the paths are relative to the context path,
|
||||
* otherwise they are absolute
|
||||
* For backward compatibility the pathspecs may be comma separated strings, but this
|
||||
* will not be supported in future versions.
|
||||
*/
|
||||
public void addIncludedPaths(String... pathspecs)
|
||||
{
|
||||
|
@ -334,9 +367,9 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
|
|||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* Get the minimum reponse size.
|
||||
* Get the minimum response size.
|
||||
*
|
||||
* @return minimum reponse size
|
||||
* @return minimum response size
|
||||
*/
|
||||
public int getMinGzipSize()
|
||||
{
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.server.handler.gzip;
|
||||
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class GzipHandlerTest
|
||||
{
|
||||
@Test
|
||||
public void testAddGetPaths()
|
||||
{
|
||||
GzipHandler gzip = new GzipHandler();
|
||||
gzip.addIncludedPaths("/foo");
|
||||
gzip.addIncludedPaths("^/bar.*$");
|
||||
|
||||
String[] includedPaths = gzip.getIncludedPaths();
|
||||
assertThat("Included Paths.size", includedPaths.length, is(2));
|
||||
assertThat("Included Paths", Arrays.asList(includedPaths), contains("/foo","^/bar.*$"));
|
||||
}
|
||||
}
|
|
@ -51,6 +51,7 @@ import org.eclipse.jetty.http.HttpHeader;
|
|||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.MimeTypes;
|
||||
import org.eclipse.jetty.http.PathMap.MappedEntry;
|
||||
import org.eclipse.jetty.http.pathmap.MappedResource;
|
||||
import org.eclipse.jetty.http.PreEncodedHttpField;
|
||||
import org.eclipse.jetty.http.ResourceHttpContent;
|
||||
import org.eclipse.jetty.io.WriterOutputStream;
|
||||
|
@ -672,9 +673,9 @@ public class DefaultServlet extends HttpServlet implements ResourceFactory
|
|||
|
||||
if ((_welcomeServlets || _welcomeExactServlets) && welcome_servlet==null)
|
||||
{
|
||||
MappedEntry<?> entry=_servletHandler.getHolderEntry(welcome_in_context);
|
||||
if (entry!=null && entry.getValue()!=_defaultHolder &&
|
||||
(_welcomeServlets || (_welcomeExactServlets && entry.getKey().equals(welcome_in_context))))
|
||||
MappedResource<ServletHolder> entry=_servletHandler.getHolderEntry(welcome_in_context);
|
||||
if (entry!=null && entry.getResource()!=_defaultHolder &&
|
||||
(_welcomeServlets || (_welcomeExactServlets && entry.getPathSpec().getDeclaration().equals(welcome_in_context))))
|
||||
welcome_servlet=welcome_in_context;
|
||||
|
||||
}
|
||||
|
|
|
@ -32,6 +32,8 @@ import javax.servlet.http.HttpServletRequest;
|
|||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.http.PathMap.MappedEntry;
|
||||
import org.eclipse.jetty.http.pathmap.MappedResource;
|
||||
import org.eclipse.jetty.server.Dispatcher;
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
|
@ -71,7 +73,7 @@ public class Invoker extends HttpServlet
|
|||
|
||||
private ContextHandler _contextHandler;
|
||||
private ServletHandler _servletHandler;
|
||||
private Map.Entry<String, ServletHolder> _invokerEntry;
|
||||
private MappedResource<ServletHolder> _invokerEntry;
|
||||
private Map<String, String> _parameters;
|
||||
private boolean _nonContextServlets;
|
||||
private boolean _verbose;
|
||||
|
@ -170,12 +172,12 @@ public class Invoker extends HttpServlet
|
|||
|
||||
// Check for existing mapping (avoid threaded race).
|
||||
String path=URIUtil.addPaths(servlet_path,servlet);
|
||||
Map.Entry<String, ServletHolder> entry = _servletHandler.getHolderEntry(path);
|
||||
MappedResource<ServletHolder> entry = _servletHandler.getHolderEntry(path);
|
||||
|
||||
if (entry!=null && !entry.equals(_invokerEntry))
|
||||
{
|
||||
// Use the holder
|
||||
holder=(ServletHolder)entry.getValue();
|
||||
holder=(ServletHolder)entry.getResource();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -49,7 +49,10 @@ import javax.servlet.http.HttpServlet;
|
|||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.http.PathMap;
|
||||
import org.eclipse.jetty.http.pathmap.MappedResource;
|
||||
import org.eclipse.jetty.http.pathmap.PathMappings;
|
||||
import org.eclipse.jetty.http.pathmap.PathSpec;
|
||||
import org.eclipse.jetty.http.pathmap.ServletPathSpec;
|
||||
import org.eclipse.jetty.security.IdentityService;
|
||||
import org.eclipse.jetty.security.SecurityHandler;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
|
@ -111,7 +114,8 @@ public class ServletHandler extends ScopedHandler
|
|||
private MultiMap<FilterMapping> _filterNameMappings;
|
||||
|
||||
private final Map<String,ServletHolder> _servletNameMap=new HashMap<>();
|
||||
private PathMap<ServletHolder> _servletPathMap;
|
||||
// private PathMap<ServletHolder> _servletPathMap;
|
||||
private PathMappings<ServletHolder> _servletPathMap;
|
||||
|
||||
private ListenerHolder[] _listeners=new ListenerHolder[0];
|
||||
|
||||
|
@ -358,7 +362,7 @@ public class ServletHandler extends ScopedHandler
|
|||
* @param pathInContext Path within _context.
|
||||
* @return PathMap Entries pathspec to ServletHolder
|
||||
*/
|
||||
public PathMap.MappedEntry<ServletHolder> getHolderEntry(String pathInContext)
|
||||
public MappedResource<ServletHolder> getHolderEntry(String pathInContext)
|
||||
{
|
||||
if (_servletPathMap==null)
|
||||
return null;
|
||||
|
@ -436,14 +440,14 @@ public class ServletHandler extends ScopedHandler
|
|||
if (target.startsWith("/"))
|
||||
{
|
||||
// Look for the servlet by path
|
||||
PathMap.MappedEntry<ServletHolder> entry=getHolderEntry(target);
|
||||
MappedResource<ServletHolder> entry=getHolderEntry(target);
|
||||
if (entry!=null)
|
||||
{
|
||||
servlet_holder=entry.getValue();
|
||||
PathSpec pathSpec = entry.getPathSpec();
|
||||
servlet_holder=entry.getResource();
|
||||
|
||||
String servlet_path_spec= entry.getKey();
|
||||
String servlet_path=entry.getMapped()!=null?entry.getMapped():PathMap.pathMatch(servlet_path_spec,target);
|
||||
String path_info=PathMap.pathInfo(servlet_path_spec,target);
|
||||
String servlet_path=pathSpec.getPathMatch(target);
|
||||
String path_info=pathSpec.getPathInfo(target);
|
||||
|
||||
if (DispatcherType.INCLUDE.equals(type))
|
||||
{
|
||||
|
@ -1307,7 +1311,7 @@ public class ServletHandler extends ScopedHandler
|
|||
}
|
||||
else
|
||||
{
|
||||
PathMap<ServletHolder> pm = new PathMap<>();
|
||||
PathMappings<ServletHolder> pm = new PathMappings<>();
|
||||
Map<String,ServletMapping> servletPathMappings = new HashMap<>();
|
||||
|
||||
//create a map of paths to set of ServletMappings that define that mapping
|
||||
|
@ -1370,7 +1374,7 @@ public class ServletHandler extends ScopedHandler
|
|||
if (LOG.isDebugEnabled()) LOG.debug("Chose path={} mapped to servlet={} from default={}", pathSpec, finalMapping.getServletName(), finalMapping.isDefault());
|
||||
|
||||
servletPathMappings.put(pathSpec, finalMapping);
|
||||
pm.put(pathSpec,_servletNameMap.get(finalMapping.getServletName()));
|
||||
pm.put(new ServletPathSpec(pathSpec),_servletNameMap.get(finalMapping.getServletName()));
|
||||
}
|
||||
|
||||
_servletPathMap=pm;
|
||||
|
|
|
@ -20,7 +20,7 @@ package org.eclipse.jetty.util;
|
|||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
|
||||
/** Utility class to maintain a set of inclusions and exclusions.
|
||||
|
@ -36,28 +36,54 @@ public class IncludeExclude<ITEM>
|
|||
{
|
||||
private final Set<ITEM> _includes;
|
||||
private final Set<ITEM> _excludes;
|
||||
private final BiFunction<Set<ITEM>,ITEM, Boolean> _matcher;
|
||||
private final Predicate<ITEM> _includePredicate;
|
||||
private final Predicate<ITEM> _excludePredicate;
|
||||
|
||||
private static class SetContainsPredicate<ITEM> implements Predicate<ITEM>
|
||||
{
|
||||
private final Set<ITEM> set;
|
||||
|
||||
public SetContainsPredicate(Set<ITEM> set)
|
||||
{
|
||||
this.set = set;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(ITEM item)
|
||||
{
|
||||
return set.contains(item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default constructor over {@link HashSet}
|
||||
*/
|
||||
public IncludeExclude()
|
||||
{
|
||||
this(HashSet.class,null);
|
||||
this(HashSet.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an IncludeExclude
|
||||
* @param setClass The type of {@link Set} to using internally
|
||||
* @param matcher A function to test if a passed ITEM is matched by the passed SET, or null to use {@link Set#contains(Object)}
|
||||
* @param predicate A predicate function to test if a passed ITEM is matched by the passed SET}
|
||||
*/
|
||||
public <SET extends Set<ITEM>> IncludeExclude(Class<SET> setClass, BiFunction<SET,ITEM, Boolean> matcher)
|
||||
public <SET extends Set<ITEM>> IncludeExclude(Class<SET> setClass)
|
||||
{
|
||||
try
|
||||
{
|
||||
_includes = setClass.newInstance();
|
||||
_excludes = setClass.newInstance();
|
||||
_matcher = (BiFunction<Set<ITEM>,ITEM, Boolean>)matcher;
|
||||
if(_includes instanceof Predicate) {
|
||||
_includePredicate = (Predicate<ITEM>)_includes;
|
||||
} else {
|
||||
_includePredicate = new SetContainsPredicate<>(_includes);
|
||||
}
|
||||
if(_excludes instanceof Predicate) {
|
||||
_excludePredicate = (Predicate<ITEM>)_excludes;
|
||||
} else {
|
||||
_excludePredicate = new SetContainsPredicate<>(_excludes);
|
||||
}
|
||||
}
|
||||
catch (InstantiationException | IllegalAccessException e)
|
||||
{
|
||||
|
@ -65,6 +91,22 @@ public class IncludeExclude<ITEM>
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an IncludeExclude
|
||||
*
|
||||
* @param includeSet the Set of items that represent the included space
|
||||
* @param includePredicate the Predicate for included item testing (null for simple {@link Set#contains(Object)} test)
|
||||
* @param excludeSet the Set of items that represent the excluded space
|
||||
* @param excludePredicate the Predicate for excluded item testing (null for simple {@link Set#contains(Object)} test)
|
||||
*/
|
||||
public <SET extends Set<ITEM>> IncludeExclude(Set<ITEM> includeSet, Predicate<ITEM> includePredicate, Set<ITEM> excludeSet, Predicate<ITEM> excludePredicate)
|
||||
{
|
||||
_includes = includeSet;
|
||||
_includePredicate = includePredicate;
|
||||
_excludes = excludeSet;
|
||||
_excludePredicate = excludePredicate;
|
||||
}
|
||||
|
||||
public void include(ITEM element)
|
||||
{
|
||||
_includes.add(element);
|
||||
|
@ -89,15 +131,9 @@ public class IncludeExclude<ITEM>
|
|||
|
||||
public boolean matches(ITEM e)
|
||||
{
|
||||
if (_matcher==null)
|
||||
{
|
||||
if (_includes.size()>0 && !_includes.contains(e))
|
||||
return false;
|
||||
return !_excludes.contains(e);
|
||||
}
|
||||
if (_includes.size()>0 && !_matcher.apply(_includes,e))
|
||||
if (!_includes.isEmpty() && !_includePredicate.test(e))
|
||||
return false;
|
||||
return !_matcher.apply(_excludes,e);
|
||||
return !_excludePredicate.test(e);
|
||||
}
|
||||
|
||||
public int size()
|
||||
|
@ -124,6 +160,6 @@ public class IncludeExclude<ITEM>
|
|||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("%s@%x{i=%s,e=%s,m=%s}",this.getClass().getSimpleName(),hashCode(),_includes,_excludes,_matcher);
|
||||
return String.format("%s@%x{i=%s,ip=%s,e=%s,ep=%s}",this.getClass().getSimpleName(),hashCode(),_includes,_includePredicate,_excludes,_excludePredicate);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import java.util.HashSet;
|
|||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
|
@ -32,9 +33,8 @@ import java.util.regex.Pattern;
|
|||
* <p>
|
||||
* Provides the efficient {@link #matches(String)} method to check for a match against all the combined Regex's
|
||||
*/
|
||||
public class RegexSet extends AbstractSet<String>
|
||||
public class RegexSet extends AbstractSet<String> implements Predicate<String>
|
||||
{
|
||||
public static final BiFunction<RegexSet,String,Boolean> MATCHER=(rs,p)->{return rs.matches(p);};
|
||||
private final Set<String> _patterns=new HashSet<String>();
|
||||
private final Set<String> _unmodifiable=Collections.unmodifiableSet(_patterns);
|
||||
private Pattern _pattern;
|
||||
|
@ -99,6 +99,12 @@ public class RegexSet extends AbstractSet<String>
|
|||
_pattern = Pattern.compile(builder.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(String s)
|
||||
{
|
||||
return _pattern!=null && _pattern.matcher(s).matches();
|
||||
}
|
||||
|
||||
public boolean matches(String s)
|
||||
{
|
||||
return _pattern!=null && _pattern.matcher(s).matches();
|
||||
|
|
|
@ -20,7 +20,9 @@ package org.eclipse.jetty.util;
|
|||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
public class IncludeExcludeTest
|
||||
{
|
||||
|
@ -29,8 +31,8 @@ public class IncludeExcludeTest
|
|||
{
|
||||
IncludeExclude<String> ie = new IncludeExclude<>();
|
||||
|
||||
assertEquals(0,ie.size());
|
||||
assertEquals(true,ie.matches("foo"));
|
||||
assertThat("Empty IncludeExclude", ie.size(), is(0));
|
||||
assertThat("Matches 'foo'",ie.matches("foo"),is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -40,7 +42,7 @@ public class IncludeExcludeTest
|
|||
ie.include("foo");
|
||||
ie.include("bar");
|
||||
|
||||
assertEquals(2,ie.size());
|
||||
assertThat("IncludeExclude.size", ie.size(), is(2));
|
||||
assertEquals(false,ie.matches(""));
|
||||
assertEquals(true,ie.matches("foo"));
|
||||
assertEquals(true,ie.matches("bar"));
|
||||
|
@ -86,7 +88,7 @@ public class IncludeExcludeTest
|
|||
@Test
|
||||
public void testEmptyRegex()
|
||||
{
|
||||
IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class,RegexSet.MATCHER);
|
||||
IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class);
|
||||
|
||||
assertEquals(0,ie.size());
|
||||
assertEquals(true,ie.matches("foo"));
|
||||
|
@ -95,7 +97,7 @@ public class IncludeExcludeTest
|
|||
@Test
|
||||
public void testIncludeRegex()
|
||||
{
|
||||
IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class,RegexSet.MATCHER);
|
||||
IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class);
|
||||
ie.include("f..");
|
||||
ie.include("b((ar)|(oo))");
|
||||
|
||||
|
@ -112,7 +114,7 @@ public class IncludeExcludeTest
|
|||
@Test
|
||||
public void testExcludeRegex()
|
||||
{
|
||||
IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class,RegexSet.MATCHER);
|
||||
IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class);
|
||||
ie.exclude("f..");
|
||||
ie.exclude("b((ar)|(oo))");
|
||||
|
||||
|
@ -130,7 +132,7 @@ public class IncludeExcludeTest
|
|||
@Test
|
||||
public void testIncludeExcludeRegex()
|
||||
{
|
||||
IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class,RegexSet.MATCHER);
|
||||
IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class);
|
||||
ie.include(".*[aeiou].*");
|
||||
ie.include("[AEIOU].*");
|
||||
ie.exclude("f..");
|
||||
|
@ -146,8 +148,5 @@ public class IncludeExcludeTest
|
|||
|
||||
assertEquals(true,ie.matches("foobar"));
|
||||
assertEquals(true,ie.matches("Ant"));
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ import javax.servlet.ServletException;
|
|||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.handler.AbstractHandler;
|
||||
import org.junit.Assert;
|
||||
|
@ -93,4 +95,28 @@ public class HttpClientIdleTimeoutTest extends AbstractTest
|
|||
|
||||
Assert.assertTrue(latch.await(2 * idleTimeout, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testServerIdleTimeout() throws Exception
|
||||
{
|
||||
start(new AbstractHandler()
|
||||
{
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
{
|
||||
baseRequest.setHandled(true);
|
||||
}
|
||||
});
|
||||
connector.setIdleTimeout(idleTimeout);
|
||||
|
||||
ContentResponse response1 = client.newRequest(newURI()).send();
|
||||
Assert.assertEquals(HttpStatus.OK_200, response1.getStatus());
|
||||
|
||||
// Let the server idle timeout.
|
||||
Thread.sleep(2 * idleTimeout);
|
||||
|
||||
// Make sure we can make another request successfully.
|
||||
ContentResponse response2 = client.newRequest(newURI()).send();
|
||||
Assert.assertEquals(HttpStatus.OK_200, response2.getStatus());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue