blob: 9a6b5b69fe54671b6367eab3623ad40a34900adc [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.chromium.build.BuildConfig;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
import java.util.WeakHashMap;
/**
* UnownedUserDataKey is used in conjunction with a particular {@link UnownedUserData} as the key
* for that when it is added to an {@link UnownedUserDataHost}.
* <p>
* This key is supposed to be private and not visible to other parts of the code base. Instead of
* using the class as a key like in owned {@link org.chromium.base.UserData}, for {@link
* UnownedUserData}, a particular object is used, ensuring that even if a class is visible outside
* its own module, the instance of it as referenced from a {@link UnownedUserDataHost}, can not be
* retrieved.
* <p>
* In practice, instances will typically be stored on this form:
*
* <pre>{@code
* public class Foo implements UnownedUserData {
* private static final UnownedUserDataKey<Foo> KEY = new UnownedUserDataKey<>(Foo.class);
* ...
* }
* }
* </pre>
* <p>
* This class and all its methods are final to ensure that no usage of the class leads to leaking
* data about the object it is used as a key for.
* <p>
* It is OK to attach this key to as many different {@link UnownedUserDataHost} instances as
* necessary, but doing so requires the client to invoke either {@link
* #detachFromHost(UnownedUserDataHost)} or {@link #detachFromAllHosts(UnownedUserData)} during
* cleanup.
* <p>
* Guarantees provided by this class together with {@link UnownedUserDataHost}:
* <ul>
* <li> One key can be used for multiple {@link UnownedUserData}s.
* <li> One key can be attached to multiple {@link UnownedUserDataHost}s.
* <li> One key can be attached to a particular {@link UnownedUserDataHost} only once. This ensures
* a pair of {@link UnownedUserDataHost} and UnownedUserDataKey can only refer to a single
* UnownedUserData.
* <li> When a {@link UnownedUserData} is detached from a particular host, it is informed of this,
* except if it has been garbage collected.
* <li> When an {@link UnownedUserData} object is replaced with a different {@link UnownedUserData}
* using the same UnownedUserDataKey, the former is detached.
* </ul>
*
* @param <T> The Class this key is used for.
* @see UnownedUserDataHost for more details on ownership and typical usage.
* @see UnownedUserData for the marker interface used for this type of data.
*/
public final class UnownedUserDataKey<T extends UnownedUserData> {
@NonNull private final Class<T> mClazz;
// A Set that uses WeakReference<UnownedUserDataHost> internally.
private final Set<UnownedUserDataHost> mWeakHostAttachments =
Collections.newSetFromMap(new WeakHashMap<>());
/**
* Constructs a key to use for attaching to a particular {@link UnownedUserDataHost}.
*
* @param clazz The particular {@link UnownedUserData} class.
*/
public UnownedUserDataKey(@NonNull Class<T> clazz) {
mClazz = clazz;
}
@NonNull
/* package */ final Class<T> getValueClass() {
return mClazz;
}
/**
* Attaches the {@link UnownedUserData} object to the given {@link UnownedUserDataHost}, and
* stores the host as a {@link WeakReference} to be able to detach from it later.
*
* @param host The host to attach the {@code object} to.
* @param object The object to attach.
*/
public final void attachToHost(@NonNull UnownedUserDataHost host, @NonNull T object) {
Objects.requireNonNull(object);
// Setting a new value might lead to detachment of previously attached data, including
// re-entry to this key, to happen before we update the {@link #mHostAttachments}.
host.set(this, object);
if (!isAttachedToHost(host)) {
mWeakHostAttachments.add(host);
}
}
/**
* Attempts to retrieve the instance of the {@link UnownedUserData} from the given {@link
* UnownedUserDataHost}. It will return {@code null} if the object is not attached to that
* particular {@link UnownedUserDataHost} using this key, or the {@link UnownedUserData} has
* been garbage collected.
*
* @param host The host to retrieve the {@link UnownedUserData} from.
* @return The current {@link UnownedUserData} stored in the {@code host}, or {@code null}.
*/
@Nullable
public final T retrieveDataFromHost(@NonNull UnownedUserDataHost host) {
assertNoDestroyedAttachments();
for (UnownedUserDataHost attachedHost : mWeakHostAttachments) {
if (host.equals(attachedHost)) {
return host.get(this);
}
}
return null;
}
/**
* Detaches the key and object from the given host if it is attached with this key. It is OK to
* call this for already detached objects.
*
* @param host The host to detach from.
*/
public final void detachFromHost(@NonNull UnownedUserDataHost host) {
assertNoDestroyedAttachments();
for (UnownedUserDataHost attachedHost : new ArrayList<>(mWeakHostAttachments)) {
if (host.equals(attachedHost)) {
removeHostAttachment(attachedHost);
}
}
}
/**
* Detaches the {@link UnownedUserData} from all hosts that it is currently attached to with
* this key. It is OK to call this for already detached objects.
*
* @param object The object to detach from all hosts.
*/
public final void detachFromAllHosts(@NonNull T object) {
assertNoDestroyedAttachments();
for (UnownedUserDataHost attachedHost : new ArrayList<>(mWeakHostAttachments)) {
if (object.equals(attachedHost.get(this))) {
removeHostAttachment(attachedHost);
}
}
}
/**
* Checks if the {@link UnownedUserData} is currently attached to the given host with this key.
*
* @param host The host to check if the {@link UnownedUserData} is attached to.
* @return true if currently attached, false otherwise.
*/
public final boolean isAttachedToHost(@NonNull UnownedUserDataHost host) {
T t = retrieveDataFromHost(host);
return t != null;
}
/**
* @return Whether the {@link UnownedUserData} is currently attached to any hosts with this key.
*/
public final boolean isAttachedToAnyHost(@NonNull T object) {
return getHostAttachmentCount(object) > 0;
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
/* package */ int getHostAttachmentCount(@NonNull T object) {
assertNoDestroyedAttachments();
int ret = 0;
for (UnownedUserDataHost attachedHost : mWeakHostAttachments) {
if (object.equals(attachedHost.get(this))) {
ret++;
}
}
return ret;
}
private void removeHostAttachment(UnownedUserDataHost host) {
host.remove(this);
mWeakHostAttachments.remove(host);
}
private void assertNoDestroyedAttachments() {
if (BuildConfig.ENABLE_ASSERTS) {
for (UnownedUserDataHost attachedHost : mWeakHostAttachments) {
if (attachedHost.isDestroyed()) {
assert false : "Host should have been removed already.";
throw new IllegalStateException();
}
}
}
}
}