/*
* Copyright (C) 2014-2016 LinkedIn Corp. All rights reserved.
*
* 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.
*/
package gobblin.util;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.Maps;
import java.net.ServerSocket;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PortUtils {
public static final int MINIMUM_PORT = 1025;
public static final int MAXIMUM_PORT = 65535;
private static final Pattern PORT_REGEX =
Pattern.compile("\\$\\{PORT_(?>(?>\\?(\\d+))|(?>(\\d+)\\?)|(\\d+|\\?))\\}");
private final PortLocator portLocator;
private final ConcurrentMap<Integer, Boolean> assignedPorts;
public PortUtils() {
this(new ServerSocketPortLocator());
}
@VisibleForTesting
PortUtils(PortLocator locator) {
this.portLocator = locator;
this.assignedPorts = Maps.newConcurrentMap();
}
/**
* Replaces any port tokens in the specified string.
*
* NOTE: Tokens can be in the following forms:
* 1. ${PORT_123}
* 2. ${PORT_?123}
* 3. ${PORT_123?}
* 4. ${PORT_?}
*
* @param value The string in which to replace port tokens.
* @return The replaced string.
*/
public String replacePortTokens(String value) {
BiMap<String, Optional<Integer>> portMappings = HashBiMap.create();
Matcher regexMatcher = PORT_REGEX.matcher(value);
while (regexMatcher.find()) {
String token = regexMatcher.group(0);
if (!portMappings.containsKey(token)) {
Optional<Integer> portStart = Optional.absent();
Optional<Integer> portEnd = Optional.absent();
String unboundedStart = regexMatcher.group(1);
if (unboundedStart != null) {
int requestedEndPort = Integer.parseInt(unboundedStart);
Preconditions.checkArgument(requestedEndPort <= PortUtils.MAXIMUM_PORT);
portEnd = Optional.of(requestedEndPort);
} else {
String unboundedEnd = regexMatcher.group(2);
if (unboundedEnd != null) {
int requestedStartPort = Integer.parseInt(unboundedEnd);
Preconditions.checkArgument(requestedStartPort >= PortUtils.MINIMUM_PORT);
portStart = Optional.of(requestedStartPort);
} else {
String absolute = regexMatcher.group(3);
if (!"?".equals(absolute)) {
int requestedPort = Integer.parseInt(absolute);
Preconditions.checkArgument(requestedPort >= PortUtils.MINIMUM_PORT &&
requestedPort <= PortUtils.MAXIMUM_PORT);
portStart = Optional.of(requestedPort);
portEnd = Optional.of(requestedPort);
}
}
}
Optional<Integer> port = takePort(portStart, portEnd);
portMappings.put(token, port);
}
}
for (Map.Entry<String, Optional<Integer>> port : portMappings.entrySet()) {
if (port.getValue().isPresent()) {
value = value.replace(port.getKey(), port.getValue().get().toString());
}
}
return value;
}
/**
* Finds an open port. {@param portStart} and {@param portEnd} can be absent
*
* ______________________________________________________
* | portStart | portEnd | takenPort |
* |-----------|----------|-----------------------------|
* | absent | absent | random |
* | absent | provided | 1024 < port <= portEnd |
* | provided | absent | portStart <= port <= 65535 |
* | provided | provided | portStart = port = portEnd |
* ------------------------------------------------------
*
* @param portStart the inclusive starting port
* @param portEnd the inclusive ending port
* @return The selected open port.
*/
private synchronized Optional<Integer> takePort(Optional<Integer> portStart, Optional<Integer> portEnd) {
if (!portStart.isPresent() && !portEnd.isPresent()) {
for (int i = 0; i < 65535; i++) {
try {
int port = this.portLocator.random();
Boolean wasAssigned = assignedPorts.putIfAbsent(port, true);
if (wasAssigned == null || !wasAssigned) {
return Optional.of(port);
}
} catch (Exception ignored) {
}
}
}
for (int port = portStart.or(MINIMUM_PORT); port <= portEnd.or(MAXIMUM_PORT); port++) {
try {
this.portLocator.specific(port);
Boolean wasAssigned = assignedPorts.putIfAbsent(port, true);
if (wasAssigned == null || !wasAssigned) {
return Optional.of(port);
}
} catch (Exception ignored) {
}
}
throw new RuntimeException(String.format("No open port could be found for %s to %s", portStart, portEnd));
}
@VisibleForTesting
interface PortLocator {
int random() throws Exception;
int specific(int port) throws Exception;
}
private static class ServerSocketPortLocator implements PortLocator {
@Override
public int random() throws Exception {
try (ServerSocket serverSocket = new ServerSocket(0)) {
return serverSocket.getLocalPort();
}
}
@Override
public int specific(int port) throws Exception {
try (ServerSocket serverSocket = new ServerSocket(port)) {
return serverSocket.getLocalPort();
}
}
}
}