org.openide.nodes.EntrySupportDefault Maven / Gradle / Ivy
The newest version!
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.openide.nodes;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.openide.nodes.Children.Entry;
import org.openide.util.Utilities;
/** Default support that just fires changes directly to children and is suitable
* for simple mappings.
*/
class EntrySupportDefault extends EntrySupport {
private List entries = Collections.emptyList();
private static final Reference EMPTY = new WeakReference(null);
/** array of children Reference (ChildrenArray) */
private Reference array = EMPTY;
/** mapping from entries to info about them */
private Map map;
private static final Object LOCK = new Object();
private static final Logger LOGGER = Logger.getLogger(EntrySupportDefault.class.getName()); // NOI18N
//private static final boolean LOG_ENABLED = LOGGER.isLoggable(Level.FINER);
private Thread initThread;
private boolean inited = false;
public EntrySupportDefault(Children ch) {
super(ch);
}
@Override
public String toString() {
return super.toString() + " array: " + array.get(); // NOI18N
}
public boolean isInitialized() {
ChildrenArray arr = array.get();
return inited && arr != null && arr.isInitialized();
}
@Override
List snapshot() {
Node[] nodes = getNodes();
try {
Children.PR.enterReadAccess();
return createSnapshot();
} finally {
Children.PR.exitReadAccess();
}
}
DefaultSnapshot createSnapshot() {
return new DefaultSnapshot(getNodes(), array.get());
}
public final Node[] getNodes() {
final boolean LOG_ENABLED = LOGGER.isLoggable(Level.FINER);
if (LOG_ENABLED) {
LOGGER.finer("getNodes() " + this);
}
boolean[] results = new boolean[2];
for (;;) {
// initializes the ChildrenArray possibly calls
// addNotify if this is for the first time
ChildrenArray tmpArray = getArray(results); // fils results[0]
Node[] nodes;
try {
Children.PR.enterReadAccess();
if (this != children.getEntrySupport()) {
// support was switched while we were waiting for access
return new Node[0];
}
results[1] = isInitialized();
nodes = tmpArray.nodes();
} finally {
Children.PR.exitReadAccess();
}
if (LOG_ENABLED) {
LOGGER.finer(" length : " + (nodes == null ? "nodes is null" : nodes.length)); // NOI18N
LOGGER.finer(" init now : " + isInitialized()); // NOI18N
}
// if not initialized that means that after
// we computed the nodes, somebody changed them (as a
// result of addNotify) => we have to compute them
// again
if (results[1]) {
// otherwise it is ok.
return nodes;
}
if (results[0]) {
// looks like the result cannot be computed, just give empty one
notifySetEntries();
return (nodes == null) ? new Node[0] : nodes;
}
}
}
public Node[] getNodes(boolean optimalResult) {
final boolean LOG_ENABLED = LOGGER.isLoggable(Level.FINER);
ChildrenArray hold = null;
Node find = null;
if (optimalResult) {
if (LOG_ENABLED) {
LOGGER.finer("computing optimal result"); // NOI18N
}
hold = getArray(null);
if (LOG_ENABLED) {
LOGGER.finer("optimal result is here: " + hold); // NOI18N
}
find = children.findChild(null);
if (LOG_ENABLED) {
LOGGER.finer("Find child got: " + find); // NOI18N
}
Children.LOG.log(Level.FINEST, "after findChild: {0}", optimalResult);
}
return getNodes();
}
public final int getNodesCount(boolean optimalResult) {
return getNodes(optimalResult).length;
}
@Override
public Node getNodeAt(int index) {
Node[] nodes = getNodes();
return index < nodes.length ? nodes[index] : null;
}
/** Computes the nodes now.
*/
final Node[] justComputeNodes() {
if (map == null) {
map = Collections.synchronizedMap(new HashMap(17));
LOGGER.finer("Map initialized");
}
List l = new LinkedList();
for (Entry entry : entries) {
Info info = findInfo(entry);
l.addAll(info.nodes(false));
}
Node[] arr = l.toArray(new Node[0]);
// initialize parent nodes
for (int i = 0; i < arr.length; i++) {
Node n = arr[i];
if (n == null) {
LOGGER.warning("null node among children!");
for (int j = 0; j < arr.length; j++) {
LOGGER.log(Level.WARNING, " {0} = {1}", new Object[]{j, arr[j]});
}
for (Entry entry : entries) {
Info info = findInfo(entry);
LOGGER.log(Level.WARNING, " entry: {0} info {1} nodes: {2}", new Object[]{entry, info, info.nodes(false)});
}
throw new NullPointerException("arr[" + i + "] is null"); // NOI18N
}
n.assignTo(children, i);
n.fireParentNodeChange(null, children.parent);
}
return arr;
}
/** Finds info for given entry, or registers
* it, if not registered yet.
*/
private Info findInfo(Entry entry) {
synchronized (map) {
Info info = map.get(entry);
if (info == null) {
info = new Info(entry);
map.put(entry, info);
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("Put: " + entry + " info: " + info);
}
}
return info;
}
}
//
// Entries
//
private boolean mustNotifySetEnties = false;
void notifySetEntries() {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer(this + " mustNotifySetEntries()");
}
mustNotifySetEnties = true;
}
private void checkConsistency() {
assert map.size() == this.entries.size() : "map.size()=" + map.size() + " entries.size()=" + this.entries.size();
}
@Override
protected void setEntries(Collection extends Entry> entries, boolean noCheck) {
assert noCheck || Children.MUTEX.isWriteAccess();
final boolean LOG_ENABLED = LOGGER.isLoggable(Level.FINER);
// current list of nodes
ChildrenArray holder = array.get();
if (LOG_ENABLED) {
LOGGER.finer("setEntries for " + this + " on " + Thread.currentThread()); // NOI18N
LOGGER.finer(" values: " + entries); // NOI18N
LOGGER.finer(" holder: " + holder); // NOI18N
LOGGER.finer(" mustNotifySetEntries: " + mustNotifySetEnties); // NOI18N
}
Node[] current = holder == null ? null : holder.nodes();
if (mustNotifySetEnties) {
if (holder == null) {
holder = getArray(null);
}
if (current == null) {
holder.entrySupport = this;
current = holder.nodes();
}
mustNotifySetEnties = false;
} else if (holder == null || current == null) {
this.entries = new ArrayList(entries);
if (map != null) {
map.keySet().retainAll(new HashSet(this.entries));
}
return;
}
checkConsistency();
// what should be removed
Set toRemove = new LinkedHashSet(this.entries);
Set entriesSet = new HashSet(entries);
toRemove.removeAll(entriesSet);
if (!toRemove.isEmpty()) {
// notify removing, the set must be ready for
// callbacks with questions
updateRemove(current, toRemove);
current = holder.nodes();
}
// change the order of entries, notifies
// it and again brings children to up-to-date state
Collection toAdd = updateOrder(current, entries);
if (!toAdd.isEmpty()) {
// toAdd contains Info objects that should bee added
updateAdd(toAdd, new ArrayList(entries));
}
}
private void checkInfo(Info info, Entry entry, Collection extends Entry> entries, java.util.Map map) {
if (info == null) {
StringBuilder sb = new StringBuilder();
sb.append("Error in ").append(getClass().getName()).
append(" with entry ").append(entry).append(" from among entries:");
for (Entry e : entries) {
sb.append("\n ").append(e).append(" contained: ").append(map.containsKey(e));
}
sb.append("\nprobably caused by faulty key implementation. The key hashCode() and equals() methods must behave as for an IMMUTABLE object" + " and the hashCode() must return the same value for equals() keys."); // NOI18N
sb.append("\nmapping:");
for (Map.Entry ei : map.entrySet()) {
sb.append("\n ").append(ei.getKey()).append(" => ").append(ei.getValue());
}
throw new IllegalStateException(sb.toString());
}
}
/** Removes the objects from the children.
*/
private void updateRemove(Node[] current, Set toRemove) {
assert Children.MUTEX.isWriteAccess();
List nodes = new LinkedList();
ChildrenArray cha = array.get();
for (Entry en : toRemove) {
Info info = map.remove(en);
checkInfo(info, en, new ArrayList(), map);
nodes.addAll(info.nodes(true));
cha.remove(info);
}
// modify the current set of entries
entries.removeAll(toRemove);
checkConsistency();
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("Current : " + this.entries);
LOGGER.finer("Removing: " + toRemove);
}
// empty the list of nodes so it has to be recreated again
if (!nodes.isEmpty()) {
clearNodes();
notifyRemove(nodes, current);
}
}
/** Updates the order of entries.
* @param current current state of nodes
* @param entries new set of entries
* @return list of infos that should be added
*/
private List updateOrder(Node[] current, Collection extends Entry> newEntries) {
assert Children.MUTEX.isWriteAccess();
List toAdd = new LinkedList();
// that assignes entries their begining position in the array
// of nodes
java.util.Map offsets = new HashMap();
{
int previousPos = 0;
for (Entry entry : entries) {
Info info = map.get(entry);
checkInfo(info, entry, entries, map);
offsets.put(info, previousPos);
previousPos += info.length();
}
}
int[] perm = new int[current.length];
int currentPos = 0;
int permSize = 0;
List reorderedEntries = null;
for (Entry entry : newEntries) {
Info info = map.get(entry);
if (info == null) {
// this info has to be added
info = new Info(entry);
toAdd.add(info);
} else {
int len = info.length();
if (reorderedEntries == null) {
reorderedEntries = new LinkedList();
}
reorderedEntries.add(entry);
// already there => test if it should not be reordered
Integer previousInt = offsets.get(info);
/*
if (previousInt == null) {
System.err.println("Offsets: " + offsets);
System.err.println("Info: " + info);
System.err.println("Entry: " + info.entry);
System.err.println("This entries: " + this.entries);
System.err.println("Entries: " + entries);
System.err.println("Map: " + map);
System.err.println("---------vvvvv");
System.err.println(debug);
System.err.println("---------^^^^^");
}
*/
int previousPos = previousInt;
if (currentPos != previousPos) {
for (int i = 0; i < len; i++) {
perm[previousPos + i] = 1 + currentPos + i;
}
permSize += len;
}
}
currentPos += info.length();
}
if (permSize > 0) {
// now the perm array contains numbers 1 to ... and
// 0 one places where no permutation occures =>
// decrease numbers, replace zeros
for (int i = 0; i < perm.length; i++) {
if (perm[i] == 0) {
// fixed point
perm[i] = i;
} else {
// decrease
perm[i]--;
}
}
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("Entries before reordering: " + entries);
LOGGER.finer("Entries after reordering: " + reorderedEntries);
}
// reorderedEntries are not null
entries = reorderedEntries;
checkConsistency();
// notify the permutation to the parent
clearNodes();
Node p = children.parent;
if (p != null) {
p.fireReorderChange(perm);
}
}
return toAdd;
}
/** Updates the state of children by adding given Infos.
* @param infos list of Info objects to add
* @param entries the final state of entries that should occur
*/
private void updateAdd(Collection infos, List entries) {
assert Children.MUTEX.isWriteAccess();
List nodes = new LinkedList();
for (Info info : infos) {
nodes.addAll(info.nodes(false));
map.put(info.entry, info);
}
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("Entries before updateAdd(): " + this.entries);
LOGGER.finer("Entries after updateAdd(): " + entries);
}
this.entries = entries;
checkConsistency();
if (!nodes.isEmpty()) {
clearNodes();
notifyAdd(nodes);
}
}
/** Refreshes content of one entry. Updates the state of children
* appropriately.
*/
final void refreshEntry(Entry entry) {
// current list of nodes
ChildrenArray holder = array.get();
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("refreshEntry: " + entry + " holder=" + holder);
}
if (holder == null) {
return;
}
Node[] current = holder.nodes();
if (current == null) {
// the initialization is not finished yet =>
return;
}
checkConsistency();
Info info = map.get(entry);
if (info == null) {
// refresh of entry that is not present =>
return;
}
Collection oldNodes = info.nodes(false);
Collection newNodes = info.entry.nodes(null);
if (oldNodes.equals(newNodes)) {
// nodes are the same =>
return;
}
Set toRemove = new HashSet(oldNodes);
toRemove.removeAll(new HashSet(newNodes));
if (!toRemove.isEmpty()) {
// notify removing, the set must be ready for
// callbacks with questions
// modifies the list associated with the info
oldNodes.removeAll(toRemove);
clearNodes();
// now everything should be consistent => notify the remove
notifyRemove(toRemove, current);
current = holder.nodes();
}
List toAdd = refreshOrder(entry, oldNodes, newNodes);
info.useNodes(newNodes);
if (!toAdd.isEmpty()) {
// modifies the list associated with the info
clearNodes();
notifyAdd(toAdd);
}
}
/** Updates the order of nodes after a refresh.
* @param entry the refreshed entry
* @param oldNodes nodes that are currently in the list
* @param newNodes new nodes (defining the order of oldNodes and some more)
* @return list of infos that should be added
*/
private List refreshOrder(Entry entry, Collection oldNodes, Collection newNodes) {
List toAdd = new LinkedList();
Set oldNodesSet = new HashSet(oldNodes);
Set toProcess = new HashSet(oldNodesSet);
Node[] permArray = new Node[oldNodes.size()];
Iterator it2 = newNodes.iterator();
int pos = 0;
while (it2.hasNext()) {
Node n = it2.next();
if (oldNodesSet.remove(n)) {
// the node is in the old set => test for permuation
permArray[pos++] = n;
} else {
if (!toProcess.contains(n)) {
// if the node has not been processed yet
toAdd.add(n);
} else {
it2.remove();
}
}
}
// JST: If you get IllegalArgumentException in following code
// then it can be cause by wrong synchronization between
// equals and hashCode methods. First of all check them!
int[] perm = NodeOp.computePermutation(oldNodes.toArray(new Node[0]), permArray);
if (perm != null) {
// apply the permutation
clearNodes();
// temporarily change the nodes the entry should use
findInfo(entry).useNodes(Arrays.asList(permArray));
Node p = children.parent;
if (p != null) {
p.fireReorderChange(perm);
}
}
return toAdd;
}
/** Notifies that a set of nodes has been removed from
* children. It is necessary that the system is already
* in consistent state, so any callbacks will return
* valid values.
*
* @param nodes list of removed nodes
* @param current state of nodes
* @return array of nodes that were deleted
*/
Node[] notifyRemove(Collection nodes, Node[] current) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("notifyRemove: " + nodes);
LOGGER.finer("Current : " + Arrays.asList(current));
}
// during a deserialization it may have parent == null
Node[] arr = nodes.toArray(new Node[0]);
if (children.parent != null) {
// fire change of nodes
if (children.getEntrySupport() == this) {
children.parent.fireSubNodesChange(false, arr, current);
}
// fire change of parent
Iterator it = nodes.iterator();
while (it.hasNext()) {
Node n = it.next();
n.deassignFrom(children);
n.fireParentNodeChange(children.parent, null);
}
}
children.destroyNodes(arr);
return arr;
}
/** Notifies that a set of nodes has been add to
* children. It is necessary that the system is already
* in consistent state, so any callbacks will return
* valid values.
*
* @param nodes list of removed nodes
*/
void notifyAdd(Collection nodes) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("notifyAdd: " + nodes);
}
// notify about parent change
for (Node n : nodes) {
n.assignTo(children, -1);
n.fireParentNodeChange(null, children.parent);
}
Node[] arr = nodes.toArray(new Node[0]);
Node n = children.parent;
if (n != null && children.getEntrySupport() == this) {
n.fireSubNodesChange(true, arr, null);
}
}
/**
* @return either nodes associated with this children or null if they are not created
*/
public Node[] testNodes() {
ChildrenArray arr = array.get();
if (arr == null) {
return null;
}
try {
Children.PR.enterReadAccess();
return arr.nodes();
} finally {
Children.PR.exitReadAccess();
}
}
/** Obtains references to array holder. If it does not exist, it is created.
*
* @param cannotWorkBetter array of size 1 or null, will contain true, if
* the getArray cannot be initialized (we are under read access
* and another thread is responsible for initialization, in such case
* give up on computation of best result
*/
private ChildrenArray getArray(boolean[] cannotWorkBetter) {
final boolean LOG_ENABLED = LOGGER.isLoggable(Level.FINER);
ChildrenArray arr;
boolean doInitialize = false;
synchronized (LOCK) {
arr = array.get();
if (arr == null) {
arr = new ChildrenArray();
// register the array with the children
registerChildrenArray(arr, true);
doInitialize = true;
initThread = Thread.currentThread();
}
}
if (doInitialize) {
if (LOG_ENABLED) {
LOGGER.finer("Initialize " + this + " on " + Thread.currentThread()); // NOI18N
}
// this call can cause a lot of callbacks => be prepared
// to handle them as clean as possible
try {
children.callAddNotify();
if (LOG_ENABLED) {
LOGGER.finer("addNotify successfully called for " + this + " on " + Thread.currentThread()); // NOI18N
}
} finally {
boolean notifyLater;
notifyLater = Children.MUTEX.isReadAccess();
if (LOG_ENABLED) {
LOGGER.finer("notifyAll for " + this + " on " + Thread.currentThread() + " notifyLater: " + notifyLater); // NOI18N
}
// now attach to entrySupport, so when entrySupport == null => we are
// not fully initialized!!!!
arr.entrySupport = this;
inited = true;
class SetAndNotify implements Runnable {
public ChildrenArray toSet;
public Children whatSet;
public void run() {
synchronized (LOCK) {
initThread = null;
LOCK.notifyAll();
}
if (LOG_ENABLED) {
LOGGER.finer("notifyAll done"); // NOI18N
}
}
}
SetAndNotify setAndNotify = new SetAndNotify();
setAndNotify.toSet = arr;
setAndNotify.whatSet = children;
if (notifyLater) {
// the notify to the lock has to be done later than
// setKeys is executed, otherwise the result of addNotify
// might not be visible to other threads
// fix for issue 50308
Children.MUTEX.postWriteRequest(setAndNotify);
} else {
setAndNotify.run();
}
}
} else if (initThread != null) {
// otherwise, if not initialize yet (arr.children) wait
// for the initialization to finish, but only if we can wait
if (Children.MUTEX.isReadAccess() || Children.MUTEX.isWriteAccess() || (initThread == Thread.currentThread())) {
// fail, we are in read access
if (LOG_ENABLED) {
LOGGER.log(Level.FINER, "cannot initialize better " + this + " on " + Thread.currentThread() + " read access: " + Children.MUTEX.isReadAccess() + " write access: " + Children.MUTEX.isWriteAccess() + " initThread: " + initThread);
}
if (cannotWorkBetter != null) {
cannotWorkBetter[0] = true;
}
arr.entrySupport = this;
return arr;
}
// otherwise we can wait
synchronized (LOCK) {
while (initThread != null) {
if (LOG_ENABLED) {
LOGGER.finer("waiting for children for " + this + " on " + Thread.currentThread());
}
try {
LOCK.wait();
} catch (InterruptedException ex) {
}
}
}
if (LOG_ENABLED) {
LOGGER.finer(" children are here for " + this + " on " + Thread.currentThread() + " children " + children); // NOI18N
}
}
return arr;
}
/** Clears the nodes
*/
private void clearNodes() {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer(" clearNodes()"); // NOI18N
}
ChildrenArray arr = array.get();
if (arr != null) {
// clear the array
arr.clear();
}
}
/** Registration of ChildrenArray.
* @param chArr the associated ChildrenArray
* @param weak use weak or hard reference
*/
final void registerChildrenArray(final ChildrenArray chArr, boolean weak) {
final boolean LOG_ENABLED = LOGGER.isLoggable(Level.FINER);
if (LOG_ENABLED) {
LOGGER.finer("registerChildrenArray: " + chArr + " weak: " + weak); // NOI18N
}
synchronized (LOCK) {
if (this.array != null && this.array.get() == chArr && ((ChArrRef) this.array).isWeak() == weak) {
return;
}
this.array = new ChArrRef(chArr, weak);
}
if (LOG_ENABLED) {
LOGGER.finer("pointed by: " + chArr + " to: " + this.array); // NOI18N
}
}
/** Finalized.
*/
final void finalizedChildrenArray(Reference caller) {
assert caller.get() == null : "Should be null";
// usually in removeNotify setKeys is called => better require write access
try {
Children.PR.enterWriteAccess();
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.fine("previous array: " + array + " caller: " + caller); // NOI18N
}
synchronized (LOCK) {
if (array == caller && children.getEntrySupport() == this) {
// really finalized and not reconstructed
mustNotifySetEnties = false;
array = EMPTY;
inited = false;
children.callRemoveNotify();
assert array == EMPTY;
}
}
} finally {
Children.PR.exitWriteAccess();
}
}
@Override
protected List getEntries() {
return new ArrayList(entries);
}
/** Information about an entry. Contains number of nodes,
* position in the array of nodes, etc.
*/
final class Info extends Object {
int length;
final Entry entry;
public Info(Entry entry) {
this.entry = entry;
}
public Collection nodes(boolean hasToExist) {
// forces creation of the array
assert !hasToExist || array.get() != null : "ChildrenArray is not initialized";
ChildrenArray arr = getArray(null);
return arr.nodesFor(this, hasToExist);
}
public void useNodes(Collection nodes) {
// forces creation of the array
ChildrenArray arr = getArray(null);
arr.useNodes(this, nodes);
// assign all there nodes the new children
for (Node n : nodes) {
n.assignTo(EntrySupportDefault.this.children, -1);
n.fireParentNodeChange(null, children.parent);
}
}
public int length() {
return length;
}
@Override
public String toString() {
return "Children.Info[" + entry + ",length=" + length + "]"; // NOI18N
}
}
static class DefaultSnapshot extends AbstractList {
private Node[] nodes;
Object holder;
public DefaultSnapshot(Node[] nodes, ChildrenArray cha) {
this.nodes = nodes;
this.holder = cha;
}
public Node get(int index) {
return nodes != null && index < nodes.length ? nodes[index] : null;
}
public int size() {
return nodes != null ? nodes.length : 0;
}
}
private class ChArrRef extends WeakReference implements Runnable {
private final ChildrenArray chArr;
public ChArrRef(ChildrenArray referent, boolean weak) {
super(referent, Utilities.activeReferenceQueue());
this.chArr = weak ? null : referent;
}
@Override
public ChildrenArray get() {
return chArr != null ? chArr : super.get();
}
boolean isWeak() {
return chArr == null;
}
@Override
public void run() {
finalizedChildrenArray(this);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy