/*
* Copyright 2016 higherfrequencytrading.com
*
* 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 net.openhft.chronicle.network;
import net.openhft.chronicle.core.Jvm;
import net.openhft.chronicle.core.io.Closeable;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListMap;
import static net.openhft.chronicle.core.io.Closeable.closeQuietly;
/**
* The TCPRegistry allows you to either provide a true host and port for example "localhost:8080" or
* if you would rather let the application allocate you a free port at random, you can just provide
* a text reference to the port, for example "host.port", you can provide any text you want, it will
* always be taken as a reference, that is unless its correctly formed like
* "<hostname>:<port>”, then it will use the exact host and port you provide. The reason
* we offer this functionality is quiet often in unit tests you wish to start a test via loopback,
* followed often by another test via loopback, if the first test does not shut down correctly it
* can impact on the second test. Giving each test a unique port is one solution, but then managing
* those ports can become a problem in its self. So we created the TCPRegistry which manages those
* ports for you, when you come to clean up at the end of each test, all you have to do it call
* TCPRegistry.reset() and it will ensure that any remaining ports that are open will be closed.
*/
public enum TCPRegistry {
;
static final Map<String, InetSocketAddress> HOSTNAME_PORT_ALIAS = new ConcurrentSkipListMap<>();
static final Map<String, ServerSocketChannel> DESC_TO_SERVER_SOCKET_CHANNEL_MAP = new ConcurrentSkipListMap<>();
public static void reset() {
DESC_TO_SERVER_SOCKET_CHANNEL_MAP.values().forEach(Closeable::closeQuietly);
HOSTNAME_PORT_ALIAS.clear();
DESC_TO_SERVER_SOCKET_CHANNEL_MAP.clear();
Jvm.pause(50);
}
public static Set<String> aliases() {
return HOSTNAME_PORT_ALIAS.keySet();
}
public static void assertAllServersStopped() {
@NotNull List<String> closed = new ArrayList<>();
for (@NotNull Map.Entry<String, ServerSocketChannel> entry : DESC_TO_SERVER_SOCKET_CHANNEL_MAP.entrySet()) {
if (entry.getValue().isOpen())
closed.add(entry.toString());
closeQuietly(entry.getValue());
}
HOSTNAME_PORT_ALIAS.clear();
DESC_TO_SERVER_SOCKET_CHANNEL_MAP.clear();
if (!closed.isEmpty())
throw new AssertionError("Had to stop " + closed);
}
public static void setAlias(String name, @NotNull String hostname, int port) {
HOSTNAME_PORT_ALIAS.put(name, new InetSocketAddress(hostname, port));
}
/**
* @param descriptions each string is the name to a reference of a host and port, or if
* correctly formed this example host and port are used instead
* @throws IOException
*/
public static void createServerSocketChannelFor(@NotNull String... descriptions) throws IOException {
for (@NotNull String description : descriptions) {
InetSocketAddress address;
if (description.contains(":")) {
@NotNull String[] split = description.trim().split(":");
String host = split[0];
int port = Integer.parseInt(split[1]);
address = createInetSocketAddress(host, port);
} else {
address = new InetSocketAddress("localhost", 0);
}
createSSC(description, address);
}
}
private static void createSSC(String description, InetSocketAddress address) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().setReuseAddress(true);
ssc.bind(address);
DESC_TO_SERVER_SOCKET_CHANNEL_MAP.put(description, ssc);
HOSTNAME_PORT_ALIAS.put(description, (InetSocketAddress) ssc.socket().getLocalSocketAddress());
}
public static ServerSocketChannel acquireServerSocketChannel(@NotNull String description) throws IOException {
ServerSocketChannel ssc = DESC_TO_SERVER_SOCKET_CHANNEL_MAP.get(description);
if (ssc != null && ssc.isOpen())
return ssc;
InetSocketAddress address = lookup(description);
ssc = ServerSocketChannel.open();
ssc.socket().setReuseAddress(true);
ssc.bind(address);
DESC_TO_SERVER_SOCKET_CHANNEL_MAP.put(description, ssc);
return ssc;
}
public static InetSocketAddress lookup(@NotNull String description) {
InetSocketAddress address = HOSTNAME_PORT_ALIAS.get(description);
if (address != null)
return address;
String property = System.getProperty(description);
if (property != null) {
@NotNull String[] parts = property.split(":", 2);
if (parts[0].equals("null"))
throw new IllegalArgumentException("Invalid hostname \"null\"");
if (parts.length == 1)
throw new IllegalArgumentException("Alias " + description + " as " + property + " malformed, expected hostname:port");
try {
int port = Integer.parseInt(parts[1]);
address = addInetSocketAddress(description, parts[0], port);
return address;
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Alias " + description + " as " + property + " malformed, expected hostname:port with port as a number");
}
}
@NotNull String[] parts = description.split(":", 2);
if (parts.length == 1)
throw new IllegalArgumentException("Description " + description + " malformed, expected hostname:port");
try {
int port = Integer.parseInt(parts[1]);
address = addInetSocketAddress(description, parts[0], port);
return address;
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Description " + description + " malformed, expected hostname:port with port as a number");
}
}
@NotNull
private static InetSocketAddress addInetSocketAddress(String description, @NotNull String hostname, int port) {
if (port <= 0 || port >= 65536)
throw new IllegalArgumentException("Invalid port " + port);
@NotNull InetSocketAddress address = createInetSocketAddress(hostname, port);
HOSTNAME_PORT_ALIAS.put(description, address);
return address;
}
@NotNull
private static InetSocketAddress createInetSocketAddress(@NotNull String hostname, int port) {
return hostname.isEmpty() || hostname.equals("*") ? new InetSocketAddress(port) : new InetSocketAddress(hostname, port);
}
public static SocketChannel createSocketChannel(@NotNull String description) throws IOException {
return SocketChannel.open(lookup(description));
}
}