/* * Copyright 2000-2015 JetBrains s.r.o. * * 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 com.intellij.openapi.util.objectTree; import com.intellij.openapi.Disposable; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Disposer; import com.intellij.util.ArrayUtil; import com.intellij.util.IncorrectOperationException; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.WeakHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import java.util.*; import java.util.concurrent.atomic.AtomicLong; public final class ObjectTree<T> { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.util.objectTree.ObjectTree"); private final List<ObjectTreeListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList(); // identity used here to prevent problems with hashCode/equals overridden by not very bright minds private final Set<T> myRootObjects = ContainerUtil.newIdentityTroveSet(); // guarded by treeLock private final Map<T, ObjectNode<T>> myObject2NodeMap = ContainerUtil.newIdentityTroveMap(); // guarded by treeLock private final Map<Object, Object> myDisposedObjects = new WeakHashMap<Object, Object>(100, 0.5f, ContainerUtil.identityStrategy()); // guarded by treeLock private final List<ObjectNode<T>> myExecutedNodes = new ArrayList<ObjectNode<T>>(); // guarded by myExecutedNodes private final List<T> myExecutedUnregisteredNodes = new ArrayList<T>(); // guarded by myExecutedUnregisteredNodes final Object treeLock = new Object(); private final AtomicLong myModification = new AtomicLong(0); ObjectNode<T> getNode(@NotNull T object) { return myObject2NodeMap.get(object); } ObjectNode<T> putNode(@NotNull T object, @Nullable("null means remove") ObjectNode<T> node) { return node == null ? myObject2NodeMap.remove(object) : myObject2NodeMap.put(object, node); } @NotNull final List<ObjectNode<T>> getNodesInExecution() { return myExecutedNodes; } public final void register(@NotNull T parent, @NotNull T child) { if (parent == child) throw new IllegalArgumentException("Cannot register to itself: "+parent); Object wasDisposed = getDisposalInfo(parent); if (wasDisposed != null) { throw new IncorrectOperationException("Sorry but parent: " + parent + " has already been disposed " + "(see the cause for stacktrace) so the child: "+child+" will never be disposed", wasDisposed instanceof Throwable ? (Throwable)wasDisposed : null); } synchronized (treeLock) { myDisposedObjects.remove(child); // if we dispose thing and then register it back it means it's not disposed anymore ObjectNode<T> parentNode = getNode(parent); if (parentNode == null) parentNode = createNodeFor(parent, null); ObjectNode<T> childNode = getNode(child); if (childNode == null) { childNode = createNodeFor(child, parentNode); } else { ObjectNode<T> oldParent = childNode.getParent(); if (oldParent != null) { oldParent.removeChild(childNode); } } myRootObjects.remove(child); checkWasNotAddedAlready(parentNode, childNode); parentNode.addChild(childNode); fireRegistered(childNode.getObject()); } } public Object getDisposalInfo(@NotNull T parent) { synchronized (treeLock) { return myDisposedObjects.get(parent); } } private void checkWasNotAddedAlready(ObjectNode<T> childNode, @NotNull ObjectNode<T> parentNode) { for (ObjectNode<T> node = childNode; node != null; node = node.getParent()) { if (node == parentNode) { throw new IncorrectOperationException("'"+childNode.getObject() + "' was already added as a child of '" + parentNode.getObject()+"'"); } } } @NotNull private ObjectNode<T> createNodeFor(@NotNull T object, @Nullable ObjectNode<T> parentNode) { final ObjectNode<T> newNode = new ObjectNode<T>(this, parentNode, object, getNextModification()); if (parentNode == null) { myRootObjects.add(object); } putNode(object, newNode); return newNode; } private long getNextModification() { return myModification.incrementAndGet(); } public final boolean executeAll(@NotNull T object, @NotNull ObjectTreeAction<T> action, boolean processUnregistered) { ObjectNode<T> node; synchronized (treeLock) { node = getNode(object); } if (node == null) { if (processUnregistered) { rememberDisposedTrace(object); executeUnregistered(object, action); return true; } return false; } node.execute(action); return true; } @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") static <T> void executeActionWithRecursiveGuard(@NotNull T object, @NotNull List<T> recursiveGuard, @NotNull final ObjectTreeAction<T> action) { synchronized (recursiveGuard) { if (ArrayUtil.indexOf(recursiveGuard, object, ContainerUtil.<T>identityStrategy()) != -1) return; recursiveGuard.add(object); } try { action.execute(object); } finally { synchronized (recursiveGuard) { int i = ArrayUtil.lastIndexOf(recursiveGuard, object, ContainerUtil.<T>identityStrategy()); assert i != -1; recursiveGuard.remove(i); } } } private void executeUnregistered(@NotNull final T object, @NotNull final ObjectTreeAction<T> action) { executeActionWithRecursiveGuard(object, myExecutedUnregisteredNodes, action); } public final void executeChildAndReplace(@NotNull T toExecute, @NotNull T toReplace, @NotNull ObjectTreeAction<T> action) { final ObjectNode<T> toExecuteNode; T parentObject; synchronized (treeLock) { toExecuteNode = getNode(toExecute); if (toExecuteNode == null) throw new IllegalArgumentException("Object " + toExecute + " wasn't registered or already disposed"); final ObjectNode<T> parent = toExecuteNode.getParent(); if (parent == null) throw new IllegalArgumentException("Object " + toExecute + " is not connected to the tree - doesn't have parent"); parentObject = parent.getObject(); } toExecuteNode.execute(action); register(parentObject, toReplace); } public boolean containsKey(@NotNull T object) { synchronized (treeLock) { return getNode(object) != null; } } @TestOnly // public for Upsource public void assertNoReferenceKeptInTree(@NotNull T disposable) { synchronized (treeLock) { Collection<ObjectNode<T>> nodes = myObject2NodeMap.values(); for (ObjectNode<T> node : nodes) { node.assertNoReferencesKept(disposable); } } } void removeRootObject(@NotNull T object) { myRootObjects.remove(object); } @SuppressWarnings({"UseOfSystemOutOrSystemErr", "HardCodedStringLiteral"}) public void assertIsEmpty(boolean throwError) { synchronized (treeLock) { for (T object : myRootObjects) { if (object == null) continue; ObjectNode<T> objectNode = getNode(object); if (objectNode == null) continue; while (objectNode.getParent() != null) { objectNode = objectNode.getParent(); } final Throwable trace = objectNode.getTrace(); RuntimeException exception = new RuntimeException("Memory leak detected: '" + object + "' of " + object.getClass() + "\nSee the cause for the corresponding Disposer.register() stacktrace:\n", trace); if (throwError) { throw exception; } LOG.error(exception); } } } @TestOnly public boolean isEmpty() { synchronized (treeLock) { return myRootObjects.isEmpty(); } } @NotNull Set<T> getRootObjects() { synchronized (treeLock) { return myRootObjects; } } void addListener(@NotNull ObjectTreeListener listener) { myListeners.add(listener); } void removeListener(@NotNull ObjectTreeListener listener) { myListeners.remove(listener); } private void fireRegistered(@NotNull Object object) { for (ObjectTreeListener each : myListeners) { each.objectRegistered(object); } } void fireExecuted(@NotNull Object object) { for (ObjectTreeListener each : myListeners) { each.objectExecuted(object); } rememberDisposedTrace(object); } private void rememberDisposedTrace(@NotNull Object object) { synchronized (treeLock) { myDisposedObjects.put(object, Disposer.isDebugMode() ? ThrowableInterner.intern(new Throwable()) : Boolean.TRUE); } } int size() { synchronized (treeLock) { return myObject2NodeMap.size(); } } @Nullable public <D extends Disposable> D findRegisteredObject(@NotNull T parentDisposable, @NotNull D object) { synchronized (treeLock) { ObjectNode<T> parentNode = getNode(parentDisposable); if (parentNode == null) return null; return parentNode.findChildEqualTo(object); } } long getModification() { return myModification.get(); } }