NetworkModuleService.java 6.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
/*
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Eclipse Distribution License v1.0 which accompany this distribution.
 *
 * The Eclipse Public License is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 */
package org.eclipse.paho.client.mqttv3.internal;

import java.lang.reflect.Field;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ServiceLoader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.logging.Logger;
import org.eclipse.paho.client.mqttv3.logging.LoggerFactory;
import org.eclipse.paho.client.mqttv3.spi.NetworkModuleFactory;

/**
 * The NetworkModuleService uses the installed {@link NetworkModuleFactory}s to create {@link NetworkModule} instances.
 * <p>
 * The selection of the appropriate NetworkModuleFactory is based on the URI scheme.
 *
 * @author Maik Scheibler
 */
public class NetworkModuleService {
	private static Logger LOG = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT,
			NetworkModuleService.class.getSimpleName());
	private static final ServiceLoader<NetworkModuleFactory> FACTORY_SERVICE_LOADER = ServiceLoader.load(
			NetworkModuleFactory.class, NetworkModuleService.class.getClassLoader());

	/** Pattern to match URI authority parts: {@code authority = [userinfo"@"]host[":"port]} */
	private static final Pattern AUTHORITY_PATTERN = Pattern.compile("((.+)@)?([^:]*)(:(\\d+))?");
	private static final int AUTH_GROUP_USERINFO = 2;
	private static final int AUTH_GROUP_HOST = 3;
	private static final int AUTH_GROUP_PORT = 5;

	private NetworkModuleService() {
		// no instances
	}

	/**
	 * Validates the provided URI to be valid and that a NetworkModule is installed to serve it.
	 *
	 * @param brokerUri to be validated
	 * @throws IllegalArgumentException is case the URI is invalid or there is no {@link NetworkModule} installed for
	 * the URI scheme
	 */
	public static void validateURI(String brokerUri) throws IllegalArgumentException {
		try {
			URI uri = new URI(brokerUri);
			String scheme = uri.getScheme();
			if (scheme == null || scheme.isEmpty()) {
				throw new IllegalArgumentException("missing scheme in broker URI: " + brokerUri);
			}
			scheme = scheme.toLowerCase();
			for (NetworkModuleFactory factory : FACTORY_SERVICE_LOADER) {
				if (factory.getSupportedUriSchemes().contains(scheme)) {
					factory.validateURI(uri);
					return;
				}
			}
			throw new IllegalArgumentException("no NetworkModule installed for scheme \"" + scheme
					+ "\" of URI \"" + brokerUri + "\"");
		} catch (URISyntaxException e) {
			throw new IllegalArgumentException("Can't parse string to URI \"" + brokerUri + "\"", e);
		}
	}

	/**
	 * Creates a {@link NetworkModule} instance for the provided address, using the given options.
	 *
	 * @param address must be a valid URI
	 * @param options used to initialize the NetworkModule
	 * @param clientId a client identifier that is unique on the server being connected to
	 * @return a new NetworkModule instance
	 * @throws MqttException if the initialization fails
	 * @throws IllegalArgumentException if the provided {@code address} is invalid
	 */
	public static NetworkModule createInstance(String address, MqttConnectOptions options, String clientId)
			throws MqttException, IllegalArgumentException
	{
		try {
			URI brokerUri = new URI(address);
			applyRFC3986AuthorityPatch(brokerUri);
			String scheme = brokerUri.getScheme().toLowerCase();
			for (NetworkModuleFactory factory : FACTORY_SERVICE_LOADER) {
				if (factory.getSupportedUriSchemes().contains(scheme)) {
					return factory.createNetworkModule(brokerUri, options, clientId);
				}
			}
			/*
			 * To throw an IllegalArgumentException exception matches the previous behavior of
			 * MqttConnectOptions.validateURI(String), but it would be nice to provide something more meaningful.
			 */
			throw new IllegalArgumentException(brokerUri.toString());
		} catch (URISyntaxException e) {
			throw new IllegalArgumentException(address, e);
		}
	}

	/**
	 * Java does URI parsing according to RFC2396 and thus hostnames are limited to alphanumeric characters and '-'.
	 * But the current &quot;Uniform Resource Identifier (URI): Generic Syntax&quot; (RFC3986) allows for a much wider
	 * range of valid characters. This causes Java to fail parsing the authority part and thus the user-info, host and
	 * port will not be set on an URI which does not conform to RFC2396.
	 * <p>
	 * This workaround tries to detect such a parsing failure and does tokenize the authority parts according to
	 * RFC3986, but does not enforce any character restrictions (for sake of simplicity).
	 *
	 * @param toPatch - The URI To patch
	 * @see <a href="https://tools.ietf.org/html/rfc3986#section-3.2">rfc3986 - section-3.2</a>
	 */
	public static void applyRFC3986AuthorityPatch(URI toPatch) {
		if (toPatch == null
				|| toPatch.getHost() != null // already successfully parsed
				|| toPatch.getAuthority() == null
				|| toPatch.getAuthority().isEmpty())
		{
			return;
		}
		Matcher matcher = AUTHORITY_PATTERN.matcher(toPatch.getAuthority());
		if (matcher.find()) {
			setURIField(toPatch, "userInfo", matcher.group(AUTH_GROUP_USERINFO));
			setURIField(toPatch, "host", matcher.group(AUTH_GROUP_HOST));
			String portString = matcher.group(AUTH_GROUP_PORT);
			setURIField(toPatch, "port", portString != null ? Integer.parseInt(portString) : -1);
		}
	}

	/**
	 * Reflective manipulation of a URI field to work around the URI parser, because all fields are validated even on
	 * the full qualified URI constructor.
	 *
	 * @see URI#URI(String, String, String, int, String, String, String)
	 */
	private static void setURIField(URI toManipulate, String fieldName, Object newValue) {
		try {
			Field field = URI.class.getDeclaredField(fieldName);
			field.setAccessible(true);
			field.set(toManipulate, newValue);
		} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
			LOG.warning(NetworkModuleService.class.getName(), "setURIField", "115", new Object[] {
					toManipulate.toString() }, e);
		}
	}
}