org.osgi.service.remoteserviceadmin.EndpointDescription Maven / Gradle / Ivy
/*
* Copyright (c) OSGi Alliance (2008, 2013). 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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.osgi.service.remoteserviceadmin;
import static org.osgi.service.remoteserviceadmin.RemoteConstants.ENDPOINT_FRAMEWORK_UUID;
import static org.osgi.service.remoteserviceadmin.RemoteConstants.ENDPOINT_ID;
import static org.osgi.service.remoteserviceadmin.RemoteConstants.ENDPOINT_PACKAGE_VERSION_;
import static org.osgi.service.remoteserviceadmin.RemoteConstants.ENDPOINT_SERVICE_ID;
import static org.osgi.service.remoteserviceadmin.RemoteConstants.SERVICE_IMPORTED;
import static org.osgi.service.remoteserviceadmin.RemoteConstants.SERVICE_IMPORTED_CONFIGS;
import static org.osgi.service.remoteserviceadmin.RemoteConstants.SERVICE_INTENTS;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.osgi.framework.Constants;
import org.osgi.framework.Filter;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.Version;
/**
* A description of an endpoint that provides sufficient information for a
* compatible distribution provider to create a connection to this endpoint
*
* An Endpoint Description is easy to transfer between different systems because
* it is property based where the property keys are strings and the values are
* simple types. This allows it to be used as a communications device to convey
* available endpoint information to nodes in a network.
*
* An Endpoint Description reflects the perspective of an importer. That
* is, the property keys have been chosen to match filters that are created by
* client bundles that need a service. Therefore the map must not contain any
* {@code service.exported.*} property and must contain the corresponding
* {@code service.imported.*} ones.
*
* The {@code service.intents} property must contain the intents provided by the
* service itself combined with the intents added by the exporting distribution
* provider. Qualified intents appear fully expanded on this property.
*
* @Immutable
* @author $Id: 535d484835c708e62f9f52f4facbda354e85664a $
*/
public class EndpointDescription {
private final Map properties;
private final List interfaces;
private final long serviceId;
private final String frameworkUUID;
private final String id;
/**
* Create an Endpoint Description from a Map.
*
*
* The {@link RemoteConstants#ENDPOINT_ID endpoint.id},
* {@link RemoteConstants#SERVICE_IMPORTED_CONFIGS service.imported.configs}
* and {@code objectClass} properties must be set.
*
* @param properties The map from which to create the Endpoint Description.
* The keys in the map must be type {@code String} and, since the
* keys are case insensitive, there must be no duplicates with case
* variation.
* @throws IllegalArgumentException When the properties are not proper for
* an Endpoint Description.
*/
public EndpointDescription(Map properties) {
Map props = new TreeMap(String.CASE_INSENSITIVE_ORDER);
try {
props.putAll(properties);
} catch (ClassCastException e) {
IllegalArgumentException iae = new IllegalArgumentException("non-String key in properties");
iae.initCause(e);
throw iae;
}
if (props.size() < properties.size()) {
throw new IllegalArgumentException("duplicate keys with different cases in properties: " + new ArrayList(props.keySet()).removeAll(properties.keySet()));
}
conditionProperties(props);
this.properties = Collections.unmodifiableMap(props);
/* properties must be initialized before calling the following methods */
interfaces = verifyObjectClassProperty();
serviceId = verifyLongProperty(ENDPOINT_SERVICE_ID);
frameworkUUID = verifyStringProperty(ENDPOINT_FRAMEWORK_UUID);
id = verifyStringProperty(ENDPOINT_ID).trim();
if (id == null) {
throw new IllegalArgumentException(ENDPOINT_ID + " property must be set");
}
if (getConfigurationTypes().isEmpty()) {
throw new IllegalArgumentException(SERVICE_IMPORTED_CONFIGS + " property must be set and non-empty");
}
}
/**
* Create an Endpoint Description based on a Service Reference and a Map of
* properties. The properties in the map take precedence over the properties
* in the Service Reference.
*
*
* This method will automatically set the
* {@link RemoteConstants#ENDPOINT_FRAMEWORK_UUID endpoint.framework.uuid}
* and {@link RemoteConstants#ENDPOINT_SERVICE_ID endpoint.service.id}
* properties based on the specified Service Reference as well as the
* {@link RemoteConstants#SERVICE_IMPORTED service.imported} property if
* they are not specified as properties.
*
* The {@link RemoteConstants#ENDPOINT_ID endpoint.id},
* {@link RemoteConstants#SERVICE_IMPORTED_CONFIGS service.imported.configs}
* and {@code objectClass} properties must be set.
*
* @param reference A service reference that can be exported.
* @param properties Map of properties. This argument can be {@code null}.
* The keys in the map must be type {@code String} and, since the
* keys are case insensitive, there must be no duplicates with case
* variation.
* @throws IllegalArgumentException When the properties are not proper for
* an Endpoint Description
*/
public EndpointDescription(final ServiceReference reference, final Map properties) {
Map props = new TreeMap(String.CASE_INSENSITIVE_ORDER);
if (properties != null) {
try {
props.putAll(properties);
} catch (ClassCastException e) {
IllegalArgumentException iae = new IllegalArgumentException("non-String key in properties");
iae.initCause(e);
throw iae;
}
if (props.size() < properties.size()) {
throw new IllegalArgumentException("duplicate keys with different cases in properties: " + new ArrayList(props.keySet()).removeAll(properties.keySet()));
}
}
for (String key : reference.getPropertyKeys()) {
if (!props.containsKey(key)) {
props.put(key, reference.getProperty(key));
}
}
if (!props.containsKey(ENDPOINT_SERVICE_ID)) {
props.put(ENDPOINT_SERVICE_ID, reference.getProperty(Constants.SERVICE_ID));
}
if (!props.containsKey(ENDPOINT_FRAMEWORK_UUID)) {
String uuid = null;
try {
uuid = AccessController.doPrivileged(new PrivilegedAction() {
public String run() {
return reference.getBundle().getBundleContext().getProperty("org.osgi.framework.uuid");
}
});
} catch (SecurityException e) {
// if we don't have permission, we can't get the property
}
if (uuid != null) {
props.put(ENDPOINT_FRAMEWORK_UUID, uuid);
}
}
conditionProperties(props);
this.properties = Collections.unmodifiableMap(props);
/* properties must be initialized before calling the following methods */
interfaces = verifyObjectClassProperty();
serviceId = verifyLongProperty(ENDPOINT_SERVICE_ID);
frameworkUUID = verifyStringProperty(ENDPOINT_FRAMEWORK_UUID);
id = verifyStringProperty(ENDPOINT_ID).trim();
if (id == null) {
throw new IllegalArgumentException(ENDPOINT_ID + " property must be set");
}
if (getConfigurationTypes().isEmpty()) {
throw new IllegalArgumentException(SERVICE_IMPORTED_CONFIGS + " property must be set and non-empty");
}
}
private static final String SERVICE_EXPORTED_ = "service.exported.";
private static final int SERVICE_EXPORTED_length = SERVICE_EXPORTED_.length();
/**
* Condition the properties.
*
* @param props Property map to condition.
*/
private void conditionProperties(Map props) {
// ensure service.imported is set
if (!props.containsKey(SERVICE_IMPORTED)) {
props.put(SERVICE_IMPORTED, Boolean.toString(true));
}
// remove service.exported.* properties
for (Iterator iter = props.keySet().iterator(); iter.hasNext();) {
String key = iter.next();
if (SERVICE_EXPORTED_.regionMatches(true, 0, key, 0, SERVICE_EXPORTED_length)) {
iter.remove();
}
}
}
/**
* Verify and obtain the interface list from the properties.
*
* @return A list with the interface names.
* @throws IllegalArgumentException If the objectClass property is not set
* or is empty or if the package version property values are
* malformed.
*/
private List verifyObjectClassProperty() {
Object o = properties.get(Constants.OBJECTCLASS);
if (!(o instanceof String[])) {
throw new IllegalArgumentException("objectClass value must be of type String[]");
}
String[] objectClass = (String[]) o;
if (objectClass.length < 1) {
throw new IllegalArgumentException("objectClass is empty");
}
for (String interf : objectClass) {
int index = interf.lastIndexOf('.');
if (index == -1) {
continue;
}
String packageName = interf.substring(0, index);
try {
/* Make sure any package version properties are well formed */
getPackageVersion(packageName);
} catch (IllegalArgumentException e) {
IllegalArgumentException iae = new IllegalArgumentException("Improper version for package " + packageName);
iae.initCause(e);
throw iae;
}
}
return Collections.unmodifiableList(Arrays.asList(objectClass));
}
/**
* Verify and obtain a required String property.
*
* @param propName The name of the property
* @return The value of the property or {@code null} if the property is not
* set.
* @throws IllegalArgumentException when the property doesn't have the
* correct data type.
*/
private String verifyStringProperty(String propName) {
Object r = properties.get(propName);
try {
return (String) r;
} catch (ClassCastException e) {
IllegalArgumentException iae = new IllegalArgumentException("property value is not a String: " + propName);
iae.initCause(e);
throw iae;
}
}
/**
* Verify and obtain a required long property.
*
* @param propName The name of the property
* @return The value of the property or 0 if the property is not set.
* @throws IllegalArgumentException when the property doesn't have the
* correct data type.
*/
private long verifyLongProperty(String propName) {
Object r = properties.get(propName);
if (r == null) {
return 0l;
}
try {
return ((Long) r).longValue();
} catch (ClassCastException e) {
IllegalArgumentException iae = new IllegalArgumentException("property value is not a Long: " + propName);
iae.initCause(e);
throw iae;
}
}
/**
* Returns the endpoint's id.
*
* The id is an opaque id for an endpoint. No two different endpoints must
* have the same id. Two Endpoint Descriptions with the same id must
* represent the same endpoint.
*
* The value of the id is stored in the {@link RemoteConstants#ENDPOINT_ID}
* property.
*
* @return The id of the endpoint, never {@code null}. The returned value
* has leading and trailing whitespace removed.
*/
public String getId() {
return id;
}
/**
* Provide the list of interfaces implemented by the exported service.
*
* The value of the interfaces is derived from the {@code objectClass}
* property.
*
* @return An unmodifiable list of Java interface names implemented by this
* endpoint.
*/
public List getInterfaces() {
return interfaces;
}
/**
* Provide the version of the given package name.
*
* The version is encoded by prefixing the given package name with
* {@link RemoteConstants#ENDPOINT_PACKAGE_VERSION_
* endpoint.package.version.}, and then using this as an endpoint property
* key. For example:
*
*
* endpoint.package.version.com.acme
*
*
* The value of this property is in String format and will be converted to a
* {@code Version} object by this method.
*
* @param packageName The name of the package for which a version is
* requested.
* @return The version of the specified package or
* {@code Version.emptyVersion} if the package has no version in
* this Endpoint Description.
* @throws IllegalArgumentException If the version property value is not
* String.
*/
public Version getPackageVersion(String packageName) {
String key = ENDPOINT_PACKAGE_VERSION_ + packageName;
Object value = properties.get(key);
String version;
try {
version = (String) value;
} catch (ClassCastException e) {
IllegalArgumentException iae = new IllegalArgumentException(key + " property value is not a String");
iae.initCause(e);
throw iae;
}
return Version.parseVersion(version);
}
/**
* Returns the service id for the service exported through this endpoint.
*
* This is the service id under which the framework has registered the
* service. This field together with the Framework UUID is a globally unique
* id for a service.
*
* The value of the remote service id is stored in the
* {@link RemoteConstants#ENDPOINT_SERVICE_ID} endpoint property.
*
* @return Service id of a service or 0 if this Endpoint Description does
* not relate to an OSGi service.
*
*/
public long getServiceId() {
return serviceId;
}
/**
* Returns the configuration types.
*
* A distribution provider exports a service with an endpoint. This endpoint
* uses some kind of communications protocol with a set of configuration
* parameters. There are many different types but each endpoint is
* configured by only one configuration type. However, a distribution
* provider can be aware of different configuration types and provide
* synonyms to increase the change a receiving distribution provider can
* create a connection to this endpoint.
*
* This value of the configuration types is stored in the
* {@link RemoteConstants#SERVICE_IMPORTED_CONFIGS} service property.
*
* @return An unmodifiable list of the configuration types used for the
* associated endpoint and optionally synonyms.
*/
public List getConfigurationTypes() {
return getStringPlusProperty(SERVICE_IMPORTED_CONFIGS);
}
/**
* Return the list of intents implemented by this endpoint.
*
* The intents are based on the service.intents on an imported service,
* except for any intents that are additionally provided by the importing
* distribution provider. All qualified intents must have been expanded.
*
* This value of the intents is stored in the
* {@link RemoteConstants#SERVICE_INTENTS} service property.
*
* @return An unmodifiable list of expanded intents that are provided by
* this endpoint.
*/
public List getIntents() {
return getStringPlusProperty(SERVICE_INTENTS);
}
/**
* Reads a 'String+' property from the properties map, which may be of type
* String, String[] or Collection<String> and returns it as an
* unmodifiable List.
*
* @param key The property
* @return An unmodifiable list
*/
private List getStringPlusProperty(String key) {
Object value = properties.get(key);
if (value == null) {
return Collections.EMPTY_LIST;
}
if (value instanceof String) {
return Collections.singletonList((String) value);
}
if (value instanceof String[]) {
String[] values = (String[]) value;
List result = new ArrayList(values.length);
for (String v : values) {
if (v != null) {
result.add(v);
}
}
return Collections.unmodifiableList(result);
}
if (value instanceof Collection>) {
Collection> values = (Collection>) value;
List result = new ArrayList(values.size());
for (Iterator> iter = values.iterator(); iter.hasNext();) {
Object v = iter.next();
if (v instanceof String) {
result.add((String) v);
}
}
return Collections.unmodifiableList(result);
}
return Collections.EMPTY_LIST;
}
/**
* Return the framework UUID for the remote service, if present.
*
* The value of the remote framework uuid is stored in the
* {@link RemoteConstants#ENDPOINT_FRAMEWORK_UUID} endpoint property.
*
* @return Remote Framework UUID, or {@code null} if this endpoint is not
* associated with an OSGi framework having a framework uuid.
*/
public String getFrameworkUUID() {
return frameworkUUID;
}
/**
* Returns all endpoint properties.
*
* @return An unmodifiable map referring to the properties of this Endpoint
* Description.
*/
public Map getProperties() {
return properties;
}
/**
* Answers if this Endpoint Description refers to the same service instance
* as the given Endpoint Description.
*
* Two Endpoint Descriptions point to the same service if they have the same
* id or their framework UUIDs and remote service ids are equal.
*
* @param other The Endpoint Description to look at
* @return True if this endpoint description points to the same service as
* the other
*/
public boolean isSameService(EndpointDescription other) {
if (this.equals(other)) {
return true;
}
if (this.getFrameworkUUID() == null) {
return false;
}
return (this.getServiceId() == other.getServiceId()) && this.getFrameworkUUID().equals(other.getFrameworkUUID());
}
/**
* Returns a hash code value for the object.
*
* @return An integer which is a hash code value for this object.
*/
public int hashCode() {
return getId().hashCode();
}
/**
* Compares this {@code EndpointDescription} object to another object.
*
*
* An Endpoint Description is considered to be equal to another
* Endpoint Description if their ids are equal.
*
* @param other The {@code EndpointDescription} object to be compared.
* @return {@code true} if {@code object} is a {@code EndpointDescription}
* and is equal to this object; {@code false} otherwise.
*/
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof EndpointDescription)) {
return false;
}
return getId().equals(((EndpointDescription) other).getId());
}
/**
* Tests the properties of this {@code EndpointDescription} against the
* given filter using a case insensitive match.
*
* @param filter The filter to test.
* @return {@code true} If the properties of this
* {@code EndpointDescription} match the filter, {@code false}
* otherwise.
* @throws IllegalArgumentException If {@code filter} contains an invalid
* filter string that cannot be parsed.
*/
public boolean matches(String filter) {
Filter f;
try {
f = FrameworkUtil.createFilter(filter);
} catch (InvalidSyntaxException e) {
IllegalArgumentException iae = new IllegalArgumentException(e.getMessage());
iae.initCause(e);
throw iae;
}
Dictionary d = new UnmodifiableDictionary(properties);
/*
* we can use matchCase here since properties already supports case
* insensitive key lookup.
*/
return f.matchCase(d);
}
/**
* Returns the string representation of this EndpointDescription.
*
* @return String form of this EndpointDescription.
*/
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append('{');
Iterator> iter = properties.entrySet().iterator();
boolean comma = false;
while (iter.hasNext()) {
Map.Entry entry = iter.next();
if (comma) {
sb.append(", ");
} else {
comma = true;
}
sb.append(entry.getKey());
sb.append('=');
Object value = entry.getValue();
if (value != null) {
Class> valueType = value.getClass();
if (Object[].class.isAssignableFrom(valueType)) {
append(sb, (Object[]) value);
continue;
}
}
sb.append(value);
}
sb.append('}');
return sb.toString();
}
/**
* Append the specified Object array to the specified StringBuffer.
*
* @param sb Receiving StringBuffer.
* @param value Object array to append to the specified StringBuffer.
*/
private static void append(StringBuffer sb, Object[] value) {
sb.append('[');
boolean comma = false;
final int length = value.length;
for (int i = 0; i < length; i++) {
if (comma) {
sb.append(", ");
} else {
comma = true;
}
sb.append(String.valueOf(value[i]));
}
sb.append(']');
}
/**
* Unmodifiable Dictionary wrapper for a Map. This class is also used by
* EndpointPermission.
*/
static final class UnmodifiableDictionary extends Dictionary {
private final Map wrapped;
UnmodifiableDictionary(Map wrapped) {
this.wrapped = wrapped;
}
public Enumeration elements() {
return Collections.enumeration(wrapped.values());
}
public V get(Object key) {
return wrapped.get(key);
}
public boolean isEmpty() {
return wrapped.isEmpty();
}
public Enumeration keys() {
return Collections.enumeration(wrapped.keySet());
}
public V put(K key, V value) {
throw new UnsupportedOperationException();
}
public V remove(Object key) {
throw new UnsupportedOperationException();
}
public int size() {
return wrapped.size();
}
public String toString() {
return wrapped.toString();
}
}
}