org.cometd.bayeux.ChannelId Maven / Gradle / Ivy
/*
* Copyright (c) 2008-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.cometd.bayeux;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Reification of a {@link Channel#getId() channel id} with methods to test properties
* and compare with other {@link ChannelId}s.
* A {@link ChannelId} breaks the channel id into path segments so that, for example,
* {@code /foo/bar} breaks into {@code ["foo","bar"]}.
* {@link ChannelId} can be wild, when they end with one or two wild characters {@code "*"};
* a {@link ChannelId} is shallow wild if it ends with one wild character (for example {@code /foo/bar/*})
* and deep wild if it ends with two wild characters (for example {@code /foo/bar/**}).
* {@link ChannelId} can be a template, when a segment contains variable names surrounded by
* braces, for example {@code /foo/{var_name}}. Variable names can only be made of characters
* defined by the {@link Pattern \w} regular expression character class.
*/
public class ChannelId
{
public static final String WILD = "*";
public static final String DEEPWILD = "**";
private static final Pattern VAR = Pattern.compile("\\{(\\w+)\\}");
private final String _id;
private String[] _segments;
private int _wild;
private List _wilds;
private String _parent;
private List _vars;
/**
* Constructs a new {@code ChannelId} with the given id
*
* @param id the channel id in string form
*/
public ChannelId(String id)
{
if (id == null || id.length() == 0 || id.charAt(0) != '/' || "/".equals(id))
throw new IllegalArgumentException("Invalid channel id: " + id);
id = id.trim();
if (id.charAt(id.length() - 1) == '/')
id = id.substring(0, id.length() - 1);
_id = id;
}
private void resolve()
{
synchronized (this)
{
if (_segments != null)
return;
resolve(_id);
}
}
private void resolve(String name)
{
String[] segments = name.substring(1).split("/");
if (segments.length < 1)
throw new IllegalArgumentException("Invalid channel id: " + this);
for (int i = 1, size = segments.length; i <= size; ++i)
{
String segment = segments[i - 1];
if (i < size && (WILD.equals(segment) || DEEPWILD.equals(segment)))
throw new IllegalArgumentException("Invalid channel id: " + this);
Matcher matcher = VAR.matcher(segment);
if (matcher.matches())
{
if (_vars == null)
_vars = new ArrayList<>();
_vars.add(matcher.group(1));
}
if (i == size)
_wild = DEEPWILD.equals(segment) ? 2 : WILD.equals(segment) ? 1 : 0;
}
if (_vars == null)
_vars = Collections.emptyList();
else
_vars = Collections.unmodifiableList(_vars);
if (_wild > 0)
{
if (!_vars.isEmpty())
throw new IllegalArgumentException("Invalid channel id: " + this);
_wilds = Collections.emptyList();
}
else
{
boolean addShallow = true;
List wilds = new ArrayList<>(segments.length + 1);
StringBuilder b = new StringBuilder(name.length()).append("/");
for (int i = 1, size = segments.length; i <= size; ++i)
{
String segment = segments[i - 1];
if (segment.trim().length() == 0)
throw new IllegalArgumentException("Invalid channel id: " + this);
wilds.add(0, b + "**");
if (segment.matches(VAR.pattern()))
{
addShallow = i == size;
break;
}
if (i < size)
b.append(segment).append('/');
}
if (addShallow)
wilds.add(0, b + "*");
_wilds = Collections.unmodifiableList(wilds);
}
_parent = segments.length == 1 ? null : name.substring(0, name.length() - segments[segments.length - 1].length() - 1);
_segments = segments;
}
/**
* @return whether this {@code ChannelId} is either {@link #isShallowWild() shallow wild}
* or {@link #isDeepWild() deep wild}
*/
public boolean isWild()
{
resolve();
return _wild > 0;
}
/**
* Shallow wild {@code ChannelId}s end with a single wild character {@code "*"}
* and {@link #matches(ChannelId) match} non wild channels with
* the same {@link #depth() depth}.
* Example: {@code /foo/*} matches {@code /foo/bar}, but not {@code /foo/bar/baz}.
*
* @return whether this {@code ChannelId} is a shallow wild channel id
*/
public boolean isShallowWild()
{
return isWild() && !isDeepWild();
}
/**
* Deep wild {@code ChannelId}s end with a double wild character "**"
* and {@link #matches(ChannelId) match} non wild channels with
* the same or greater {@link #depth() depth}.
* Example: {@code /foo/**} matches {@code /foo/bar} and {@code /foo/bar/baz}.
*
* @return whether this {@code ChannelId} is a deep wild channel id
*/
public boolean isDeepWild()
{
resolve();
return _wild > 1;
}
/**
* A {@code ChannelId} is a meta {@code ChannelId} if it starts with {@code "/meta/"}.
*
* @return whether the first segment is "meta"
*/
public boolean isMeta()
{
return isMeta(_id);
}
/**
* A {@code ChannelId} is a service {@code ChannelId} if it starts with {@code "/service/"}.
*
* @return whether the first segment is "service"
*/
public boolean isService()
{
return isService(_id);
}
/**
* @return whether this {@code ChannelId} is neither {@link #isMeta() meta} nor {@link #isService() service}
*/
public boolean isBroadcast()
{
return isBroadcast(_id);
}
/**
* @return whether this {@code ChannelId} is a template, that is it contains segments
* that identify a variable name between braces, such as {@code /foo/{var_name}}.
*
* @see #bind(ChannelId)
* @see #getParameters()
*/
public boolean isTemplate()
{
resolve();
return !_vars.isEmpty();
}
/**
* @return the list of variable names if this {@link ChannelId} is a template,
* otherwise an empty list.
* @see #isTemplate()
*/
public List getParameters()
{
resolve();
return _vars;
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (!(obj instanceof ChannelId))
return false;
ChannelId that = (ChannelId)obj;
return _id.equals(that._id);
}
@Override
public int hashCode()
{
return _id.hashCode();
}
/**
* Tests whether this {@code ChannelId} matches the given {@code ChannelId}.
* If the given {@code ChannelId} is {@link #isWild() wild},
* then it matches only if it is equal to this {@code ChannelId}.
* If this {@code ChannelId} is non-wild,
* then it matches only if it is equal to the given {@code ChannelId}.
* Otherwise, this {@code ChannelId} is either shallow or deep wild, and
* matches {@code ChannelId}s with the same number of equal segments (if it is
* shallow wild), or {@code ChannelId}s with the same or a greater number of
* equal segments (if it is deep wild).
*
* @param channelId the channelId to match
* @return true if this {@code ChannelId} matches the given {@code ChannelId}
*/
public boolean matches(ChannelId channelId)
{
resolve();
if (channelId.isWild())
return equals(channelId);
switch (_wild)
{
case 0:
{
return equals(channelId);
}
case 1:
{
if (channelId._segments.length != _segments.length)
return false;
for (int i = _segments.length - 1; i-- > 0; )
if (!_segments[i].equals(channelId._segments[i]))
return false;
return true;
}
case 2:
{
if (channelId._segments.length < _segments.length)
return false;
for (int i = _segments.length - 1; i-- > 0; )
if (!_segments[i].equals(channelId._segments[i]))
return false;
return true;
}
default:
{
throw new IllegalStateException();
}
}
}
/**
* If this {@code ChannelId} is a template, and the given {@code target} {@code ChannelId}
* is non-wild and non-template, and the two have the same {@link #depth()}, then binds
* the variable(s) defined in this template with the values of the segments defined by
* the target {@code ChannelId}.
* For example:
*
* // template and target match.
* Map<String, String> bindings = new ChannelId("/a/{var1}/c/{var2}").bind(new ChannelId("/a/foo/c/bar"));
* bindings: {"var1": "foo", "var2": "bar"}
*
* // template has 2 segments, target has only 1 segment.
* bindings = new ChannelId("/a/{var1}").bind(new ChannelId("/a"))
* bindings = {}
*
* // template has 2 segments, target too many segments.
* bindings = new ChannelId("/a/{var1}").bind(new ChannelId("/a/b/c"))
* bindings = {}
*
* // same number of segments, but no match on non-variable segments.
* bindings = new ChannelId("/a/{var1}").bind(new ChannelId("/b/c"))
* bindings = {}
*
* The returned map may not preserve the order of variables present in the template {@code ChannelId}.
*
* @param target the non-wild, non-template {@code ChannelId} to bind
* @return a map withe the bindings, or an empty map if no binding was possible
* @see #isTemplate()
*/
public Map bind(ChannelId target)
{
if (!isTemplate() || target.isTemplate() || target.isWild() || depth() != target.depth())
return Collections.emptyMap();
Map result = new LinkedHashMap<>();
for (int i = 0; i < _segments.length; ++i)
{
String thisSegment = getSegment(i);
String thatSegment = target.getSegment(i);
Matcher matcher = VAR.matcher(thisSegment);
if (matcher.matches())
{
result.put(matcher.group(1), thatSegment);
}
else
{
if (!thisSegment.equals(thatSegment))
return Collections.emptyMap();
}
}
return result;
}
@Override
public String toString()
{
return _id;
}
/**
* @return how many segments this {@code ChannelId} is made of
* @see #getSegment(int)
*/
public int depth()
{
resolve();
return _segments.length;
}
/**
* @param id the channel to test
* @return whether this {@code ChannelId} is an ancestor of the given {@code ChannelId}
* @see #isParentOf(ChannelId)
*/
public boolean isAncestorOf(ChannelId id)
{
resolve();
if (isWild() || depth() >= id.depth())
return false;
for (int i = _segments.length; i-- > 0; )
{
if (!_segments[i].equals(id._segments[i]))
return false;
}
return true;
}
/**
* @param id the channel to test
* @return whether this {@code ChannelId} is the parent of the given {@code ChannelId}
* @see #isAncestorOf(ChannelId)
*/
public boolean isParentOf(ChannelId id)
{
resolve();
if (isWild() || depth() != id.depth() - 1)
return false;
for (int i = _segments.length; i-- > 0; )
{
if (!_segments[i].equals(id._segments[i]))
return false;
}
return true;
}
/**
* @return the channel string parent of this {@code ChannelId},
* or null if this {@code ChannelId} has only one segment
* @see #isParentOf(ChannelId)
*/
public String getParent()
{
resolve();
return _parent;
}
/**
* @param i the segment index
* @return the i-nth segment of this channel, or null if no such segment exist
* @see #depth()
*/
public String getSegment(int i)
{
resolve();
if (i >= _segments.length)
return null;
return _segments[i];
}
/**
* @return The list of wilds channels that match this channel, or
* the empty list if this channel is already wild.
*/
public List getWilds()
{
resolve();
return _wilds;
}
/**
* Returns the regular part of this {@link ChannelId}: the part
* of the channel id from the beginning until the first occurrence
* of a parameter or a wild character.
* Examples:
*
* ChannelId.regularPart Examples
*
*
* Channel
* Regular Part
*
*
*
*
* /foo
* /foo
*
*
* /foo/*
* /foo
*
*
* /foo/bar/**
* /foo/bar
*
*
* /foo/{p}
* /foo
*
*
* /foo/bar/{p}
* /foo/bar
*
*
* /*
* null
*
*
* /**
* null
*
*
* /{p}
* null
*
*
*
*
* @return the regular part of this channel
*/
public String getRegularPart()
{
resolve();
if (isWild())
return getParent();
if (!isTemplate())
return _id;
int regular = depth() - getParameters().size();
if (regular <= 0)
return null;
String result = "";
for (int i = 0; i < regular; ++i)
result += "/" + getSegment(i);
return result;
}
/**
* Helper method to test if the string form of a {@code ChannelId}
* represents a {@link #isMeta() meta} {@code ChannelId}.
*
* @param channelId the channel id to test
* @return whether the given channel id is a meta channel id
*/
public static boolean isMeta(String channelId)
{
return channelId != null && channelId.startsWith("/meta/");
}
/**
* Helper method to test if the string form of a {@code ChannelId}
* represents a {@link #isService() service} {@code ChannelId}.
*
* @param channelId the channel id to test
* @return whether the given channel id is a service channel id
*/
public static boolean isService(String channelId)
{
return channelId != null && channelId.startsWith("/service/");
}
/**
* Helper method to test if the string form of a {@code ChannelId}
* represents a {@link #isBroadcast() broadcast} {@code ChannelId}.
*
* @param channelId the channel id to test
* @return whether the given channel id is a broadcast channel id
*/
public static boolean isBroadcast(String channelId)
{
return !isMeta(channelId) && !isService(channelId);
}
}