DefaultPortFinder.java

package fr.sii.ogham.testing.util.port;

import java.util.Random;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.IntPredicate;

import fr.sii.ogham.testing.util.RandomPortUtils;

/**
 * Default implementation that search for ports by delegating port availability
 * check to a function.
 * 
 * @author Aurélien Baudet
 *
 */
public class DefaultPortFinder implements PortFinder {
	protected final Random random;
	protected final String protocol;
	protected final IntPredicate isPortAvailable;

	/**
	 * Initialize with a function to check if a port is available or not.
	 * 
	 * @param protocol
	 *            the protocol
	 * @param isPortAvailable
	 *            Determine if the specified port for this {@code SocketType} is
	 *            currently available on {@code localhost}.
	 */
	@SuppressWarnings("java:S2245")
	public DefaultPortFinder(String protocol, IntPredicate isPortAvailable) {
		this(protocol, isPortAvailable, new Random(System.nanoTime()));
	}

	/**
	 * Initialize with a function to check if a port is available or not.
	 * 
	 * @param protocol
	 *            the protocol
	 * @param isPortAvailable
	 *            Determine if the specified port for this {@code SocketType} is
	 *            currently available on {@code localhost}.
	 * @param random
	 *            the random implementation to use
	 */
	public DefaultPortFinder(String protocol, IntPredicate isPortAvailable, Random random) {
		super();
		this.protocol = protocol;
		this.isPortAvailable = isPortAvailable;
		this.random = random;
	}

	/**
	 * Find an available port for this {@code SocketType}, randomly selected
	 * from the range [{@code minPort}, {@code maxPort}].
	 * 
	 * @param minPort
	 *            the minimum port number
	 * @param maxPort
	 *            the maximum port number
	 * @return an available port number for this socket type
	 * @throws IllegalStateException
	 *             if no available port could be found
	 */
	public int findAvailablePort(int minPort, int maxPort) {
		assertIsTrue(minPort > 0, "'minPort' must be greater than 0");
		assertIsTrue(maxPort >= minPort, "'maxPort' must be greater than or equal to 'minPort'");
		assertIsTrue(maxPort <= RandomPortUtils.PORT_RANGE_MAX, "'maxPort' must be less than or equal to " + RandomPortUtils.PORT_RANGE_MAX);

		int portRange = maxPort - minPort;
		int candidatePort;
		int searchCounter = 0;
		do {
			if (searchCounter > portRange) {
				throw new IllegalStateException(String.format("Could not find an available %s port in the range [%d, %d] after %d attempts", protocol, minPort, maxPort, searchCounter));
			}
			candidatePort = findRandomPort(minPort, maxPort);
			searchCounter++;
		} while (!isPortAvailable.test(candidatePort));

		return candidatePort;
	}

	/**
	 * Find the requested number of available ports for this {@code SocketType},
	 * each randomly selected from the range [{@code minPort}, {@code maxPort}].
	 * 
	 * @param numRequested
	 *            the number of available ports to find
	 * @param minPort
	 *            the minimum port number
	 * @param maxPort
	 *            the maximum port number
	 * @return a sorted set of available port numbers for this socket type
	 * @throws IllegalStateException
	 *             if the requested number of available ports could not be found
	 */
	public SortedSet<Integer> findAvailablePorts(int numRequested, int minPort, int maxPort) {
		assertIsTrue(minPort > 0, "'minPort' must be greater than 0");
		assertIsTrue(maxPort > minPort, "'maxPort' must be greater than 'minPort'");
		assertIsTrue(maxPort <= RandomPortUtils.PORT_RANGE_MAX, "'maxPort' must be less than or equal to " + RandomPortUtils.PORT_RANGE_MAX);
		assertIsTrue(numRequested > 0, "'numRequested' must be greater than 0");
		assertIsTrue((maxPort - minPort) >= numRequested, "'numRequested' must not be greater than 'maxPort' - 'minPort'");

		SortedSet<Integer> availablePorts = new TreeSet<>();
		int attemptCount = 0;
		while ((++attemptCount <= numRequested + 100) && availablePorts.size() < numRequested) {
			availablePorts.add(findAvailablePort(minPort, maxPort));
		}

		if (availablePorts.size() != numRequested) {
			throw new IllegalStateException(String.format("Could not find %d available %s ports in the range [%d, %d]", numRequested, protocol, minPort, maxPort));
		}

		return availablePorts;
	}
	
	/**
	 * Find a pseudo-random port number within the range [{@code minPort},
	 * {@code maxPort}].
	 * 
	 * @param minPort
	 *            the minimum port number
	 * @param maxPort
	 *            the maximum port number
	 * @return a random port number within the specified range
	 */
	private int findRandomPort(int minPort, int maxPort) {
		int portRange = maxPort - minPort;
		return minPort + random.nextInt(portRange + 1);
	}

	private static void assertIsTrue(boolean condition, String message) {
		if (!condition) {
			throw new IllegalArgumentException(message);
		}
	}
}