All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cometd.bayeux.ChannelId Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2008 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.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
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 = "**"; // Escape closing brace to support Android. private static final Pattern VAR = Pattern.compile("\\{(\\w+)\\}"); private final ReentrantLock lock = new ReentrantLock(); private final String _id; private volatile String[] _segments; private int _wild; private List _wilds; private List _all; 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 || !Bayeux.Validator.isValidChannelId(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() { if (_segments == null) { lock.lock(); try { if (_segments == null) { resolve(_id); } } finally { lock.unlock(); } } } 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 = List.of(); } else { _vars = List.copyOf(_vars); } if (_wild > 0) { if (!_vars.isEmpty()) { throw new IllegalArgumentException("Invalid channel id: " + this); } } _wilds = List.copyOf(resolveWilds(segments)); List all = new ArrayList<>(); all.add(_id); all.addAll(_wilds); _all = List.copyOf(all); _parent = segments.length == 1 ? null : name.substring(0, name.length() - segments[segments.length - 1].length() - 1); _segments = segments; } private List resolveWilds(String[] segments) { boolean addShallow = _wild == 0; int length = _wild > 1 ? segments.length - 1 : segments.length; List wilds = new ArrayList<>(length + 1); StringBuilder b = new StringBuilder("/"); for (int i = 1; i <= length; ++i) { String segment = segments[i - 1]; if (segment.trim().length() == 0) { throw new IllegalArgumentException("Invalid channel id: " + this); } wilds.add(0, b + "**"); if (VAR.matcher(segment).matches()) { addShallow = i == length; break; } if (i < length) { b.append(segment).append('/'); } } if (addShallow) { wilds.add(0, b + "*"); } return wilds; } /** *

Returns the normalized channel id string.

*

Normalization involves trimming white spaces and removing trailing slashes.

* * @return the normalized channel id string */ public String getId() { return _id; } /** * @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 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 that) { return _id.equals(that._id); } return false; } @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("Invalid wild value " + _wild + " for " + this); } } } /** *

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:

*
{@code
     * // template and target match.
     * Map 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 with 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 Map.of(); } 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 Map.of(); } } } return result; } @Override public String toString() { return getId(); } /** * @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. * @deprecated use {@link #getWildIds()} instead */ @Deprecated public List getWilds() { resolve(); return isWild() ? List.of() : _wilds; } /** *

Returns the list of wild channels that match this channel.

*

For channel {@code /foo/bar/baz}, returns {@code [/foo/bar/*, /foo/bar/**, /foo/**, /**]}.

*

For channel {@code /foo/bar/*}, returns {@code [/foo/bar/**, /foo/**, /**]}.

*

For channel {@code /foo/bar/**}, returns {@code [/foo/**, /**]}.

*

For channel {@code /*}, returns {@code [/**]}.

*

For channel {@code /**}, returns {@code []}, an empty list.

* * @return The list of wilds channels that match this channel. */ public List getWildIds() { resolve(); return _wilds; } /** * @return a list with this channel id and its wild channel ids. * @see #getId() * @see #getWildIds() */ public List getAllIds() { resolve(); return _all; } /** *

Returns the regular part of this 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
ChannelRegular Part
/foo/foo
/foo/*/foo
/foo/bar/**/foo/bar
/foo/{p}/foo
/foo/bar/{p}/foo/bar
/*{@code null}
/**{@code null}
/{p}{@code 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; } StringBuilder result = new StringBuilder(); for (int i = 0; i < regular; ++i) { result.append("/").append(getSegment(i)); } return result.toString(); } /** *

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); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy