| /* |
| * Copyright (c) OSGi Alliance (2008, 2010). 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 <i>importer</i>. 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 |
| * @version $Id: 5eee827d56c59f61ee7c7b2e07601e20e97813b5 $ |
| */ |
| |
| public class EndpointDescription { |
| private final Map<String, Object> properties; |
| private final List<String> interfaces; |
| private final long serviceId; |
| private final String frameworkUUID; |
| private final String id; |
| |
| /** |
| * Create an Endpoint Description from a Map. |
| * |
| * <p> |
| * 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<String, ? > properties) { |
| Map<String, Object> props = new TreeMap<String, Object>( |
| 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<String>(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. |
| * |
| * <p> |
| * 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. |
| * <p> |
| * 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<String, ? > properties) { |
| Map<String, Object> props = new TreeMap<String, Object>( |
| 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<String>(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<String>() { |
| 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<String, Object> props) { |
| // ensure service.imported is set |
| if (!props.containsKey(SERVICE_IMPORTED)) { |
| props.put(SERVICE_IMPORTED, Boolean.toString(true)); |
| } |
| |
| // remove service.exported.* properties |
| for (Iterator<String> 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<String> 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<String> 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: |
| * |
| * <pre> |
| * endpoint.package.version.com.acme |
| * </pre> |
| * |
| * 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<String> 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<String> 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<String> 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<String> result = new ArrayList<String>(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<String> result = new ArrayList<String>(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<String, Object> 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. |
| * |
| * <p> |
| * An Endpoint Description is considered to be <b>equal to</b> 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<String, Object> d = new UnmodifiableDictionary<String, Object>( |
| 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<Map.Entry<String, Object>> iter = properties.entrySet() |
| .iterator(); |
| boolean comma = false; |
| while (iter.hasNext()) { |
| Map.Entry<String, Object> 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<K, V> extends Dictionary<K, V> { |
| private final Map<K, V> wrapped; |
| |
| UnmodifiableDictionary(Map<K, V> wrapped) { |
| this.wrapped = wrapped; |
| } |
| |
| public Enumeration<V> elements() { |
| return Collections.enumeration(wrapped.values()); |
| } |
| |
| public V get(Object key) { |
| return wrapped.get(key); |
| } |
| |
| public boolean isEmpty() { |
| return wrapped.isEmpty(); |
| } |
| |
| public Enumeration<K> 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(); |
| } |
| } |
| } |