io.aeron.ChannelUri Maven / Gradle / Ivy
Show all versions of aeron-client Show documentation
/*
* Copyright 2014-2024 Real Logic Limited.
*
* 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
*
* https://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 io.aeron;
import io.aeron.logbuffer.LogBufferDescriptor;
import org.agrona.AsciiEncoding;
import org.agrona.collections.ArrayUtil;
import org.agrona.collections.Object2ObjectHashMap;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiConsumer;
import static io.aeron.CommonContext.*;
import static io.aeron.logbuffer.FrameDescriptor.FRAME_ALIGNMENT;
import static java.util.Objects.requireNonNull;
/**
* Parser for Aeron channel URIs. The format is:
*
* aeron-uri = "aeron:" media [ "?" param *( "|" param ) ]
* media = *( "[^?:]" )
* param = key "=" value
* key = *( "[^=]" )
* value = *( "[^|]" )
*
*
* Multiple params with the same key are allowed, the last value specified takes precedence.
*
* @see ChannelUriStringBuilder
*/
public final class ChannelUri
{
private enum State
{
MEDIA, PARAMS_KEY, PARAMS_VALUE
}
/**
* URI Scheme for Aeron channels and destinations.
*/
public static final String AERON_SCHEME = "aeron";
/**
* Qualifier for spy subscriptions which spy on outgoing network destined traffic efficiently.
*/
public static final String SPY_QUALIFIER = "aeron-spy";
/**
* Invalid tag value returned when calling {@link #getTag(String)} and the channel is not tagged.
*/
public static final long INVALID_TAG = Aeron.NULL_VALUE;
private static final int CHANNEL_TAG_INDEX = 0;
private static final int ENTITY_TAG_INDEX = 1;
private static final String AERON_PREFIX = AERON_SCHEME + ":";
private String prefix;
private String media;
private final Object2ObjectHashMap params;
private final String[] tags;
/**
* Construct with the components provided to avoid parsing.
*
* @param prefix empty if no prefix is required otherwise expected to be 'aeron-spy'
* @param media for the channel which is typically "udp" or "ipc".
* @param params for the query string as key value pairs.
*/
private ChannelUri(final String prefix, final String media, final Object2ObjectHashMap params)
{
this.prefix = prefix;
this.media = media;
this.params = params;
this.tags = splitTags(params.get(TAGS_PARAM_NAME));
}
/**
* The prefix for the channel.
*
* @return the prefix for the channel.
*/
public String prefix()
{
return prefix;
}
/**
* Change the prefix from what has been parsed.
*
* @param prefix to replace the existing prefix.
* @return this for a fluent API.
*/
public ChannelUri prefix(final String prefix)
{
this.prefix = prefix;
return this;
}
/**
* The media over which the channel operates.
*
* @return the media over which the channel operates.
*/
public String media()
{
return media;
}
/**
* Set the media over which the channel operates.
*
* @param media to replace the parsed value.
* @return this for a fluent API.
*/
public ChannelUri media(final String media)
{
validateMedia(media);
this.media = media;
return this;
}
/**
* Is the channel {@link #media()} equal to {@link CommonContext#UDP_MEDIA}.
*
* @return true the channel {@link #media()} equals {@link CommonContext#UDP_MEDIA}.
*/
public boolean isUdp()
{
return UDP_MEDIA.equals(media);
}
/**
* Is the channel {@link #media()} equal to {@link CommonContext#IPC_MEDIA}.
*
* @return true the channel {@link #media()} equals {@link CommonContext#IPC_MEDIA}.
*/
public boolean isIpc()
{
return IPC_MEDIA.equals(media);
}
/**
* The scheme for the URI. Must be "aeron".
*
* @return the scheme for the URI.
*/
public String scheme()
{
return AERON_SCHEME;
}
/**
* Get a value for a given parameter key.
*
* @param key to lookup.
* @return the value if set for the key otherwise null.
*/
public String get(final String key)
{
return params.get(key);
}
/**
* Get the value for a given parameter key or the default value provided if the key does not exist.
*
* @param key to lookup.
* @param defaultValue to be returned if no key match is found.
* @return the value if set for the key otherwise the default value provided.
*/
public String get(final String key, final String defaultValue)
{
final String value = params.get(key);
if (null != value)
{
return value;
}
return defaultValue;
}
/**
* Put a key and value pair in the map of params.
*
* @param key of the param to be put.
* @param value of the param to be put.
* @return the existing value otherwise null.
*/
public String put(final String key, final String value)
{
return params.put(key, value);
}
/**
* Remove a key pair in the map of params.
*
* @param key of the param to be removed.
* @return the previous value of the param or null.
*/
public String remove(final String key)
{
return params.remove(key);
}
/**
* Does the URI contain a value for the given key.
*
* @param key to be lookup.
* @return true if the key has a value otherwise false.
*/
public boolean containsKey(final String key)
{
return params.containsKey(key);
}
/**
* Get the channel tag, if it exists, that refers to another channel.
*
* @return channel tag if it exists or null if not in this URI.
* @see CommonContext#TAGS_PARAM_NAME
* @see CommonContext#TAG_PREFIX
*/
public String channelTag()
{
return tags.length > CHANNEL_TAG_INDEX ? tags[CHANNEL_TAG_INDEX] : null;
}
/**
* Get the entity tag, if it exists, that refers to an entity such as subscription or publication.
*
* @return entity tag if it exists or null if not in this URI.
* @see CommonContext#TAGS_PARAM_NAME
* @see CommonContext#TAG_PREFIX
*/
public String entityTag()
{
return tags.length > ENTITY_TAG_INDEX ? tags[ENTITY_TAG_INDEX] : null;
}
/**
* {@inheritDoc}
*/
public boolean equals(final Object o)
{
if (this == o)
{
return true;
}
if (!(o instanceof ChannelUri))
{
return false;
}
final ChannelUri that = (ChannelUri)o;
return Objects.equals(prefix, that.prefix) &&
Objects.equals(media, that.media) &&
Objects.equals(params, that.params) &&
Arrays.equals(tags, that.tags);
}
/**
* {@inheritDoc}
*/
public int hashCode()
{
int result = 19;
result = 31 * result + Objects.hashCode(prefix);
result = 31 * result + Objects.hashCode(media);
result = 31 * result + Objects.hashCode(params);
result = 31 * result + Arrays.hashCode(tags);
return result;
}
/**
* Generate a String representation of the URI that is valid for an Aeron channel.
*
* @return a String representation of the URI that is valid for an Aeron channel.
*/
public String toString()
{
final StringBuilder sb;
if (prefix == null || prefix.isEmpty())
{
sb = new StringBuilder((params.size() * 20) + 10);
}
else
{
sb = new StringBuilder((params.size() * 20) + 20);
sb.append(prefix);
if (!prefix.endsWith(":"))
{
sb.append(':');
}
}
sb.append(AERON_PREFIX).append(media);
if (!params.isEmpty())
{
sb.append('?');
for (final Map.Entry entry : params.entrySet())
{
sb.append(entry.getKey()).append('=').append(entry.getValue()).append('|');
}
sb.setLength(sb.length() - 1);
}
return sb.toString();
}
/**
* Initialise a channel for restarting a publication at a given position.
*
* @param position at which the publication should be started.
* @param initialTermId what which the stream would start.
* @param termLength for the stream.
*/
public void initialPosition(final long position, final int initialTermId, final int termLength)
{
if (position < 0 || 0 != (position & (FRAME_ALIGNMENT - 1)))
{
throw new IllegalArgumentException("invalid position: " + position);
}
final int bitsToShift = LogBufferDescriptor.positionBitsToShift(termLength);
final int termId = LogBufferDescriptor.computeTermIdFromPosition(position, bitsToShift, initialTermId);
final int termOffset = (int)(position & (termLength - 1));
put(INITIAL_TERM_ID_PARAM_NAME, Integer.toString(initialTermId));
put(TERM_ID_PARAM_NAME, Integer.toString(termId));
put(TERM_OFFSET_PARAM_NAME, Integer.toString(termOffset));
put(TERM_LENGTH_PARAM_NAME, Integer.toString(termLength));
}
/**
* Parse a {@link CharSequence} which contains an Aeron URI.
*
* @param cs to be parsed.
* @return a new {@link ChannelUri} representing the URI string.
*/
@SuppressWarnings("MethodLength")
public static ChannelUri parse(final CharSequence cs)
{
int position = 0;
final String prefix;
if (startsWith(cs, 0, SPY_PREFIX))
{
prefix = SPY_QUALIFIER;
position = SPY_PREFIX.length();
}
else
{
prefix = "";
}
if (!startsWith(cs, position, AERON_PREFIX))
{
throw new IllegalArgumentException("Aeron URIs must start with 'aeron:', found: " + cs);
}
else
{
position += AERON_PREFIX.length();
}
final StringBuilder builder = new StringBuilder();
final Object2ObjectHashMap params = new Object2ObjectHashMap<>();
String media = null;
String key = null;
State state = State.MEDIA;
for (int i = position, length = cs.length(); i < length; i++)
{
final char c = cs.charAt(i);
switch (state)
{
case MEDIA:
switch (c)
{
case '?':
media = builder.toString();
builder.setLength(0);
state = State.PARAMS_KEY;
break;
case ':':
case '|':
case '=':
throw new IllegalArgumentException(
"encountered '" + c + "' within media definition at index " + i + " in " + cs);
default:
builder.append(c);
}
break;
case PARAMS_KEY:
if (c == '=')
{
if (0 == builder.length())
{
throw new IllegalStateException("empty key not allowed at index " + i + " in " + cs);
}
key = builder.toString();
builder.setLength(0);
state = State.PARAMS_VALUE;
}
else
{
if (c == '|')
{
throw new IllegalStateException("invalid end of key at index " + i + " in " + cs);
}
builder.append(c);
}
break;
case PARAMS_VALUE:
if (c == '|')
{
params.put(key, builder.toString());
builder.setLength(0);
state = State.PARAMS_KEY;
}
else
{
builder.append(c);
}
break;
default:
throw new IllegalStateException("unexpected state=" + state + " in " + cs);
}
}
switch (state)
{
case MEDIA:
media = builder.toString();
validateMedia(media);
break;
case PARAMS_VALUE:
params.put(key, builder.toString());
break;
default:
throw new IllegalStateException("no more input found, state=" + state + " in " + cs);
}
return new ChannelUri(prefix, media, params);
}
/**
* Add a sessionId to a given channel.
*
* @param channel to add sessionId to.
* @param sessionId to add to channel.
* @return new string that represents channel with sessionId added.
*/
public static String addSessionId(final String channel, final int sessionId)
{
final ChannelUri channelUri = ChannelUri.parse(channel);
channelUri.put(CommonContext.SESSION_ID_PARAM_NAME, Integer.toString(sessionId));
return channelUri.toString();
}
/**
* Is the param value tagged? (starts with the "tag:" prefix).
*
* @param paramValue to check if tagged.
* @return true if tagged or false if not.
* @see CommonContext#TAGS_PARAM_NAME
* @see CommonContext#TAG_PREFIX
*/
public static boolean isTagged(final String paramValue)
{
return startsWith(paramValue, 0, TAG_PREFIX);
}
/**
* Get the value of the tag from a given parameter value.
*
* @param paramValue to extract the tag value from.
* @return the value of the tag or {@link #INVALID_TAG} if not tagged.
* @see CommonContext#TAGS_PARAM_NAME
* @see CommonContext#TAG_PREFIX
*/
public static long getTag(final String paramValue)
{
return isTagged(paramValue) ?
AsciiEncoding.parseLongAscii(paramValue, 4, paramValue.length() - 4) : INVALID_TAG;
}
/**
* Create a channel URI for a destination, i.e. a channel that uses {@code media} and {@code interface} parameters
* of the original channel and adds specified {@code endpoint} to it. For example given the input channel is
* {@code aeron:udp?mtu=1440|ttl=0|endpoint=localhost:8090|term-length=128k|interface=eth0} and the endpoint is
* {@code 192.168.0.14} the output of this method will be {@code aeron:udp?endpoint=192.168.0.14|interface=eth0}.
*
* @param channel for which the destination is being added.
* @param endpoint for the target destination.
* @return new channel URI for a destination.
*/
public static String createDestinationUri(final String channel, final String endpoint)
{
final ChannelUri channelUri = ChannelUri.parse(channel);
final String uri = AERON_PREFIX + channelUri.media() + "?" + ENDPOINT_PARAM_NAME + "=" + endpoint;
final String networkInterface = channelUri.get(INTERFACE_PARAM_NAME);
if (null != networkInterface)
{
return uri + "|" + INTERFACE_PARAM_NAME + "=" + networkInterface;
}
return uri;
}
/**
* Uses the supplied endpoint to resolve any wildcard ports. If the existing endpoint has a value of "0" for then
* the port of this endpoint will be used instead. If the endpoint is not specified in this uri, then the whole
* supplied endpoint is used. If the endpoint exists and has a non-wildcard port, then the existing endpoint is
* retained.
*
* @param resolvedEndpoint The endpoint to supply a resolved endpoint port.
* @throws IllegalArgumentException if the supplied resolvedEndpoint does not have a port or the port is zero.
* @throws NullPointerException if the supplied resolvedEndpoint is null
*/
public void replaceEndpointWildcardPort(final String resolvedEndpoint)
{
final int portSeparatorIndex = requireNonNull(resolvedEndpoint, "resolvedEndpoint is null").lastIndexOf(':');
if (-1 == portSeparatorIndex)
{
throw new IllegalArgumentException("No port specified on resolvedEndpoint=" + resolvedEndpoint);
}
if (resolvedEndpoint.endsWith(":0"))
{
throw new IllegalArgumentException("Wildcard port specified on resolvedEndpoint=" + resolvedEndpoint);
}
final String existingEndpoint = get(ENDPOINT_PARAM_NAME);
if (null == existingEndpoint)
{
put(ENDPOINT_PARAM_NAME, resolvedEndpoint);
}
else if (existingEndpoint.endsWith(":0"))
{
final String endpoint = existingEndpoint.substring(0, existingEndpoint.length() - 2) +
resolvedEndpoint.substring(resolvedEndpoint.lastIndexOf(':'));
put(ENDPOINT_PARAM_NAME, endpoint);
}
}
/**
* Call consumer for each parameter defined in the URI.
*
* @param consumer to be invoked for each parameter.
*/
public void forEachParameter(final BiConsumer consumer)
{
params.forEach(consumer);
}
/**
* Determines if this channel has specified control-mode=response
.
*
* @return true if this channel has specified control-mode=response
.
*/
public boolean hasControlModeResponse()
{
return CONTROL_MODE_RESPONSE.equals(get(MDC_CONTROL_MODE_PARAM_NAME));
}
/**
* Determines if the supplied channel has specified control-mode=response
.
*
* @param channelUri to check if the control mode is response
* @return true if the supplied channel has specified control-mode=response
.
*/
public static boolean isControlModeResponse(final String channelUri)
{
return parse(channelUri).hasControlModeResponse();
}
private static void validateMedia(final String media)
{
if (IPC_MEDIA.equals(media) || UDP_MEDIA.equals(media))
{
return;
}
throw new IllegalArgumentException("unknown media: " + media);
}
private static boolean startsWith(final CharSequence input, final int position, final String prefix)
{
if ((input.length() - position) < prefix.length())
{
return false;
}
for (int i = 0; i < prefix.length(); i++)
{
if (input.charAt(position + i) != prefix.charAt(i))
{
return false;
}
}
return true;
}
private static String[] splitTags(final String tagsValue)
{
String[] tags = ArrayUtil.EMPTY_STRING_ARRAY;
if (null != tagsValue)
{
final int tagCount = countTags(tagsValue);
if (tagCount == 1)
{
tags = new String[]{ tagsValue };
}
else
{
int tagStartPosition = 0;
int tagIndex = 0;
tags = new String[tagCount];
for (int i = 0, length = tagsValue.length(); i < length; i++)
{
if (tagsValue.charAt(i) == ',')
{
tags[tagIndex++] = tagsValue.substring(tagStartPosition, i);
tagStartPosition = i + 1;
if (tagIndex >= (tagCount - 1))
{
tags[tagIndex] = tagsValue.substring(tagStartPosition, length);
}
}
}
}
}
return tags;
}
private static int countTags(final String tags)
{
int count = 1;
for (int i = 0, length = tags.length(); i < length; i++)
{
if (tags.charAt(i) == ',')
{
++count;
}
}
return count;
}
Map diff(final ChannelUri that)
{
final HashMap differingValues = new HashMap<>();
if (!Objects.equals(prefix, that.prefix))
{
differingValues.put("prefix", prefix + " != " + that.prefix);
}
if (!Objects.equals(media, that.media))
{
differingValues.put("media", media + " != " + that.media);
}
if (!Objects.equals(params, that.params))
{
params.forEach(
(key, value) ->
{
final String thatValue = that.params.get(key);
if (!Objects.equals(value, thatValue))
{
differingValues.put(key, value + " != " + thatValue);
}
});
}
if (!Arrays.equals(tags, that.tags))
{
differingValues.put(TAGS_PARAM_NAME, Arrays.toString(tags) + " != " + Arrays.toString(that.tags));
}
return differingValues;
}
}