/*******************************************************************************
* Copyright (c) 2014, 2016 itemis AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Alexander Nyßen (itemis AG) - initial API and implementation
*
*******************************************************************************/
package org.eclipse.gef.common.adapt;
import java.beans.PropertyChangeSupport;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.TreeMap;
import org.eclipse.gef.common.beans.property.ReadOnlyMapWrapperEx;
import org.eclipse.gef.common.dispose.IDisposable;
import org.eclipse.gef.common.reflect.Types;
import com.google.common.reflect.TypeToken;
import javafx.beans.property.ReadOnlyMapProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;
/**
* A support class to manage adapters for a source {@link IAdaptable}. It offers
* all methods defined by {@link IAdaptable}, while not formally implementing
* the interface, and can thus be used by a source {@link IAdaptable} as a
* delegate.
*
* @author anyssen
*
* @param <A>
* The type of {@link IAdaptable} supported by this class. If
* passed-in adapters implement the {@link IAdaptable.Bound}
* interface, the generic type parameter of {@link IAdaptable.Bound}
* has to match this one.
*/
public class AdaptableSupport<A extends IAdaptable> implements IDisposable {
// XXX: We keep a sorted map of adapters (so activation/deactivation is in
// deterministic order)
private ObservableMap<AdapterKey<?>, Object> adapters = FXCollections
.observableMap(new TreeMap<AdapterKey<?>, Object>());
private ObservableMap<AdapterKey<?>, Object> adaptersUnmodifiable;
private ReadOnlyMapWrapperEx<AdapterKey<?>, Object> adaptersUnmodifiableProperty;
private A source;
/**
* Creates a new {@link AdaptableSupport} for the given source
* {@link IAdaptable} and a related {@link PropertyChangeSupport}.
*
* @param source
* The {@link IAdaptable} that encloses the to be created
* {@link AdaptableSupport}, delegating calls to it. May not be
* <code>null</code>
*/
public AdaptableSupport(A source) {
if (source == null) {
throw new IllegalArgumentException("source may not be null.");
}
this.source = source;
}
/**
* Returns a read-only map property, containing the adapters mapped to their
* keys.
*
* @return A read-only map property.
*/
// TODO: renamed to adaptersUnmodifiableProperty
public ReadOnlyMapProperty<AdapterKey<?>, Object> adaptersProperty() {
if (adaptersUnmodifiableProperty == null) {
adaptersUnmodifiableProperty = new ReadOnlyMapWrapperEx<>(source,
IAdaptable.ADAPTERS_PROPERTY, getAdapters());
}
return adaptersUnmodifiableProperty.getReadOnlyProperty();
}
/**
* Disposes this {@link AdaptableSupport}, which will unregister all
* currently registered adapters, unbind them from their source
* {@link IAdaptable} (in case they are {@link IAdaptable.Bound}), and
* dispose them (if they are {@link IDisposable}).
*/
@Override
@SuppressWarnings("unchecked")
public void dispose() {
Map<AdapterKey<?>, Object> oldAdapters = new HashMap<>(adapters);
for (AdapterKey<?> key : oldAdapters.keySet()) {
Object adapter = adapters.remove(key);
if (adapter != null) {
// unbind adapter (if its bound)
if (adapter instanceof IAdaptable.Bound) {
((IAdaptable.Bound<A>) adapter).setAdaptable(null);
}
}
// dispose adapter (if its disposable)
if (adapter instanceof IDisposable) {
((IDisposable) adapter).dispose();
}
}
adapters.clear();
source = null;
}
/**
* Returns an adapter for the given {@link AdapterKey} if one can
* unambiguously be retrieved, i.e. if there is only a single adapter
* registered under a key that 'matches' the given {@link AdapterKey}.
*
* @param <T>
* The adapter type.
* @param key
* The {@link AdapterKey} used to retrieve a registered adapter.
* @return The unambiguously retrievable adapter for the given
* {@link AdapterKey} or <code>null</code> if none could be
* retrieved.
*
* @see IAdaptable#getAdapter(AdapterKey)
*/
public <T> T getAdapter(AdapterKey<T> key) {
if (adapters.isEmpty()) {
return null;
}
// see if we can unambiguously retrieve a matching adapter
Map<AdapterKey<? extends T>, T> adaptersForTypeKey = getAdapters(
key.getKey(), key.getRole());
// an adapter instance may be registered under different keys
int adapterCount = new HashSet<>(adaptersForTypeKey.values()).size();
if (adapterCount == 1) {
return adaptersForTypeKey.values().iterator().next();
}
return null;
}
/**
* Returns an adapter for the given {@link Class} key if one can
* unambiguously be retrieved. That is, if there is only a single adapter
* that 'matches' the given {@link Class} key, this adapter is returned,
* ignoring the role under which it is registered (see
* {@link AdapterKey#getRole()}).
*
* @param <T>
* The adapter type.
* @param key
* The {@link Class} key used to retrieve a registered adapter.
* @return The unambiguously retrievable adapter for the given {@link Class}
* key or <code>null</code> if none could be retrieved.
*
* @see IAdaptable#getAdapter(Class)
*/
public <T> T getAdapter(Class<T> key) {
return this.<T> getAdapter(TypeToken.of(key));
}
/**
* Returns an adapter for the given {@link TypeToken} key if one can
* unambiguously be retrieved. That is, if there is only a single adapter
* that 'matches' the given {@link TypeToken} key, this adapter is returned,
* ignoring the role under which it is registered (see
* {@link AdapterKey#getRole()}).
*
* @param <T>
* The adapter type.
* @param key
* The {@link TypeToken} key used to retrieve a registered
* adapter.
* @return The unambiguously retrievable adapter for the given
* {@link TypeToken} key or <code>null</code> if none could be
* retrieved.
*
* @see IAdaptable#getAdapter(TypeToken)
*/
public <T> T getAdapter(TypeToken<T> key) {
// if we have only one adapter (instance) for the given type key
// (disregarding the
// role), return this one
Map<AdapterKey<? extends T>, T> adaptersForTypeKey = getAdapters(key,
null);
// an adapter instance may be registered under different keys
int adapterCount = new HashSet<>(adaptersForTypeKey.values()).size();
if (adapterCount == 1) {
return adaptersForTypeKey.values().iterator().next();
}
if (adapterCount > 1) {
// if we have more than one adapter instance, try to retrieve one
// unambiguously by using the default role
return getAdapter(AdapterKey.get(key, AdapterKey.DEFAULT_ROLE));
}
return null;
}
/**
* Returns the key under which the given adapter is bound.
*
* @param <T>
* The adapter type.
* @param adapter
* The adapter whose key to retrieve.
* @return The {@link AdapterKey} under which the respective adapter is
* bound, or <code>null</code> if the adapter is not registered.
*/
@SuppressWarnings("unchecked")
public <T> AdapterKey<T> getAdapterKey(T adapter) {
for (AdapterKey<?> key : adapters.keySet()) {
if (adapters.get(key) == adapter) {
return (AdapterKey<T>) key;
}
}
return null;
}
/**
* Retrieves all registered adapters, mapped to the respective
* {@link AdapterKey}s they are registered.
*
* @return An unmodifiable observable map containing the registered adapters
* under their {@link AdapterKey}s as a copy.
*/
public ObservableMap<AdapterKey<?>, Object> getAdapters() {
if (adaptersUnmodifiable == null) {
adaptersUnmodifiable = FXCollections
.unmodifiableObservableMap(adapters);
}
return adaptersUnmodifiable;
}
/**
* Returns all adapters 'matching' the given {@link Class} key, i.e. all
* adapters whose {@link AdapterKey}'s {@link TypeToken} key
* {@link AdapterKey#getKey()}) refers to the same or a sub-type of the
* given {@link Class} key.
*
* @param <T>
* The adapter type.
* @param key
* The {@link Class} key to retrieve adapters for.
* @return A {@link Map} containing all those adapters registered at this
* {@link AdaptableSupport}, whose {@link AdapterKey}'s
* {@link TypeToken} key ({@link AdapterKey#getKey()}) refers to the
* same or a sub-type of the given {@link Class} key, qualified by
* their respective {@link AdapterKey}s.
*
* @see IAdaptable#getAdapters(Class)
*/
public <T> Map<AdapterKey<? extends T>, T> getAdapters(
Class<? super T> key) {
return getAdapters(TypeToken.of(key));
}
/**
* Returns all adapters 'matching' the given {@link TypeToken} key, i.e. all
* adapters whose {@link AdapterKey}'s {@link TypeToken} key
* {@link AdapterKey#getKey()}) refers to the same or a sub-type or of the
* given {@link TypeToken} key.
*
* @param <T>
* The adapter type.
* @param key
* The {@link TypeToken} key to retrieve adapters for.
* @return A {@link Map} containing all those adapters registered at this
* {@link AdaptableSupport}, whose {@link AdapterKey}'s
* {@link TypeToken} key ({@link AdapterKey#getKey()}) refers to the
* same or a sub-type of the given {@link TypeToken} key, qualified
* by their respective {@link AdapterKey}s.
*
* @see IAdaptable#getAdapters(TypeToken)
*/
@SuppressWarnings("unchecked")
public <T> Map<AdapterKey<? extends T>, T> getAdapters(
TypeToken<? super T> key) {
if (adapters.isEmpty()) {
return Collections.emptyMap();
}
Map<AdapterKey<? extends T>, T> typeSafeAdapters = new TreeMap<>();
for (AdapterKey<?> k : adapters.keySet()) {
if (Types.isAssignable(key, k.getKey())) {
// check type compliance...
typeSafeAdapters.put((AdapterKey<? extends T>) k,
(T) adapters.get(k));
}
}
return typeSafeAdapters;
}
@SuppressWarnings("unchecked")
private <T> Map<AdapterKey<? extends T>, T> getAdapters(
TypeToken<? super T> typeKey, String role) {
if (typeKey == null) {
throw new IllegalArgumentException("typeKey may not be null");
}
if (adapters.isEmpty()) {
return Collections.emptyMap();
}
Map<AdapterKey<? extends T>, T> typeSafeAdapters = new TreeMap<>();
for (AdapterKey<?> k : adapters.keySet()) {
if (role == null || k.getRole().equals(role)) {
// return all adapters assignable to the given type
// key
if (Types.isAssignable(typeKey, k.getKey())) {
typeSafeAdapters.put((AdapterKey<? extends T>) k,
(T) adapters.get(k));
}
}
}
return typeSafeAdapters;
}
/**
* Registers the given adapter under the default role (see
* {@link AdapterKey#DEFAULT_ROLE}.
*
* @param <T>
* The adapter type.
* @param adapter
* The adapter to register under the given {@link Class} key.
*
* @see IAdaptable#setAdapter(TypeToken, Object)
*/
@SuppressWarnings("unchecked")
public <T> void setAdapter(T adapter) {
setAdapter(TypeToken.of((Class<T>) adapter.getClass()), adapter);
}
/**
* Registers the given adapter under the given role .
*
* @param <T>
* The adapter type.
* @param adapter
* The adapter to register.
* @param role
* The role to register this adapter with.
*
* @see IAdaptable#setAdapter(TypeToken, Object)
*/
@SuppressWarnings("unchecked")
public <T> void setAdapter(T adapter, String role) {
setAdapter(TypeToken.of((Class<T>) adapter.getClass()), adapter, role);
}
/**
* Registers the given adapter under an {@link AdapterKey}, which will use
* the given {@link TypeToken} key as well as the default role (see
* {@link AdapterKey#DEFAULT_ROLE}.
*
* @param <T>
* The adapter type.
* @param adapterType
* The {@link TypeToken} reflecting the actual type of the
* adapter.
* @param adapter
* The adapter to register.
*
*/
public <T> void setAdapter(TypeToken<T> adapterType, T adapter) {
setAdapter(adapterType, adapter, AdapterKey.DEFAULT_ROLE);
}
/**
* Registers the given adapter under the given {@link AdapterKey}. The
* {@link AdapterKey} should provide the actual type of the adapter plus a
* role.
*
* @param <T>
* The adapter type.
* @param adapterType
* A {@link TypeToken} representing the actual type of the given
* adapter.
* @param adapter
* The adapter to register.
* @param role
* The role under which to register the adapter.
*
*/
@SuppressWarnings("unchecked")
public <T> void setAdapter(TypeToken<T> adapterType, T adapter,
String role) {
// we can only check raw types here because of type erasure
TypeToken<? extends Object> instanceType = TypeToken
.of(adapter.getClass());
// if we have an adapter of an anonymous class, it has to be assignable
// to the passed in adapter type; otherwise the raw types should be
// equal (so that the specified adapter type is as 'good' as possible.
if (instanceType.getRawType().isAnonymousClass() && !adapterType
.getRawType().isAssignableFrom(instanceType.getRawType())) {
throw new IllegalArgumentException("The passed in adapter type "
+ adapterType.getRawType().getSimpleName()
+ " is not assignable from the runtime (raw) type of the adapter, which is "
+ instanceType.getRawType().getSimpleName());
} else if (!instanceType.getRawType().isAnonymousClass()
&& !instanceType.getRawType()
.equals(adapterType.getRawType())) {
throw new IllegalArgumentException("The given adapter type "
+ adapterType.getType().getClass().getSimpleName()
+ " does not match the passed in adapter's type "
+ adapter.getClass().getSimpleName());
}
AdapterKey<T> key = AdapterKey.get(adapterType, role);
if (adapters.containsKey(key)) {
if (adapters.get(key) != adapter) {
throw new IllegalArgumentException("A different adapter ("
+ adapter + ") is already registered with key " + key
+ " at adaptable " + source);
} else {
System.err.println("The adapter " + adapter
+ " was already registered with key " + key
+ " at adaptable " + source);
}
}
adapters.put(key, adapter);
if (adapter instanceof IAdaptable.Bound) {
((IAdaptable.Bound<A>) adapter).setAdaptable(source);
}
}
/**
* Unregisters the adapter, returning it for convenience.
*
* @param <T>
* The adapter type.
* @param adapter
* The adapter to unregister.
* @see IAdaptable#unsetAdapter(Object)
*/
@SuppressWarnings("unchecked")
public <T> void unsetAdapter(T adapter) {
if (!adapters.containsValue(adapter)) {
throw new IllegalArgumentException(
"Given adapter is not registered.");
}
if (adapter instanceof IAdaptable.Bound) {
((IAdaptable.Bound<A>) adapter).setAdaptable(null);
}
// process all keys and remove those pointing to the given adapter
for (AdapterKey<?> key : new HashMap<>(adapters).keySet()) {
if (adapters.get(key) == adapter) {
adapters.remove(key);
}
}
}
}