/* * 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(); } } } }