package org.netbeans.gradle.project.view;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.jtrim.concurrent.UpdateTaskExecutor;
import org.jtrim.event.CopyOnTriggerListenerManager;
import org.jtrim.event.EventDispatcher;
import org.jtrim.event.ListenerManager;
import org.jtrim.event.ListenerRef;
import org.jtrim.property.PropertyFactory;
import org.jtrim.property.PropertySource;
import org.jtrim.property.swing.SwingForwarderFactory;
import org.jtrim.property.swing.SwingProperties;
import org.jtrim.property.swing.SwingPropertySource;
import org.jtrim.swing.concurrent.SwingUpdateTaskExecutor;
import org.jtrim.utils.ExceptionHelper;
import org.netbeans.api.project.Project;
import org.netbeans.gradle.model.util.CollectionUtils;
import org.netbeans.gradle.project.api.entry.GradleProjectIDs;
import org.netbeans.gradle.project.api.event.NbListenerRefs;
import org.netbeans.gradle.project.api.nodes.SingleNodeFactory;
import org.netbeans.gradle.project.properties.NbProperties;
import org.netbeans.gradle.project.util.NbBiFunction;
import org.netbeans.gradle.project.util.NbFunction;
import org.netbeans.gradle.project.util.NbSupplier;
import org.netbeans.spi.project.ui.support.NodeFactory;
import org.netbeans.spi.project.ui.support.NodeList;
import org.openide.nodes.Node;
import org.openide.util.Lookup;
import org.openide.util.LookupEvent;
import org.openide.util.LookupListener;
import org.openide.util.lookup.Lookups;
public final class AnnotationChildNodes {
private static final Logger LOGGER = Logger.getLogger(AnnotationChildNodes.class.getName());
private static final PropertySource<Collection<SingleNodeFactory>> NO_FACTORIES
= PropertyFactory.<Collection<SingleNodeFactory>>constSource(Collections.<SingleNodeFactory>emptySet());
private final Project project;
private final RemovedChildrenProperty removeChildrenRef;
private final PropertySource<Collection<? extends NodeFactory>> nodeFactories;
private final PropertySource<Collection<SingleNodeFactory>> singleNodeFactories;
private final Lock nodeLock;
private boolean removedChildren;
private Set<NodeList<?>> currentNodeLists;
public AnnotationChildNodes(Project project) {
this(project, new NbSupplier<Lookup>() {
@Override
public Lookup get() {
return Lookups.forPath("Projects/" + GradleProjectIDs.MODULE_NAME + "/Nodes");
}
});
}
public AnnotationChildNodes(Project project, NbSupplier<? extends Lookup> factoryLookupProvider) {
ExceptionHelper.checkNotNullArgument(project, "project");
ExceptionHelper.checkNotNullArgument(factoryLookupProvider, "factoryLookupProvider");
this.project = project;
this.nodeLock = new ReentrantLock();
this.removedChildren = true;
this.removeChildrenRef = new RemovedChildrenProperty();
this.currentNodeLists = Collections.emptySet();
this.nodeFactories = NbProperties.combine(new NodeFactories(factoryLookupProvider), this.removeChildrenRef, new NbBiFunction<Collection<? extends NodeFactory>, Boolean, Collection<? extends NodeFactory>>() {
@Override
public Collection<? extends NodeFactory> apply(Collection<? extends NodeFactory> factories, Boolean removedChildren) {
return removedChildren ? null : factories;
}
});
this.singleNodeFactories = NbProperties.propertyOfProperty(nodeFactories, new NbFunction<Collection<? extends NodeFactory>, PropertySource<Collection<SingleNodeFactory>>>() {
@Override
public PropertySource<Collection<SingleNodeFactory>> apply(Collection<? extends NodeFactory> arg) {
return convertFactories(arg);
}
});
}
private void createAll(
Collection<? extends NodeFactory> factories,
Collection<NodeList<?>> result) {
for (NodeFactory factory: factories) {
try {
NodeList<?> nodeList = factory.createNodes(project);
result.add(nodeList);
} catch (Throwable ex) {
LOGGER.log(Level.SEVERE, "Exception thrown by NodeFactory.createNodes: " + factory, ex);
}
}
}
private Collection<NodeList<?>> updateAndGetNodeLists(Collection<? extends NodeFactory> factories) {
if (factories == null) {
return Collections.emptyList();
}
Collection<NodeList<?>> result = CollectionUtils.newLinkedHashSet(factories.size());
createAll(factories, result);
List<NodeList<?>> removed = new ArrayList<>();
List<NodeList<?>> added = new ArrayList<>();
nodeLock.lock();
try {
if (removedChildren) {
assert currentNodeLists.isEmpty();
return Collections.emptyList();
}
Set<NodeList<?>> prevNodeLists = currentNodeLists;
currentNodeLists = new LinkedHashSet<>(result);
for (NodeList<?> nodeList: result) {
if (!prevNodeLists.contains(nodeList)) {
added.add(nodeList);
}
}
for (NodeList<?> nodeList: prevNodeLists) {
if (!result.contains(nodeList)) {
removed.add(nodeList);
}
}
} finally {
nodeLock.unlock();
}
removeNotifyAll(removed);
addNotifyAll(added);
return result;
}
private PropertySource<Collection<SingleNodeFactory>> convertFactories(Collection<? extends NodeFactory> factories) {
final Collection<NodeList<?>> nodeLists = updateAndGetNodeLists(factories);
if (nodeLists.isEmpty()) {
return NO_FACTORIES;
}
SwingPropertySource<Collection<SingleNodeFactory>, ChangeListener> result = new SwingPropertySource<Collection<SingleNodeFactory>, ChangeListener>() {
@Override
public Collection<SingleNodeFactory> getValue() {
List<SingleNodeFactory> nodeFactories = new ArrayList<>();
for (NodeList<?> nodeList: nodeLists) {
addNodeListNodes(nodeList, nodeFactories);
}
return nodeFactories;
}
@Override
public void addChangeListener(ChangeListener listener) {
for (NodeList<?> nodeList: nodeLists) {
nodeList.addChangeListener(listener);
}
}
@Override
public void removeChangeListener(ChangeListener listener) {
for (NodeList<?> nodeList: nodeLists) {
nodeList.removeChangeListener(listener);
}
}
};
final UpdateTaskExecutor listenerExecutor = new SwingUpdateTaskExecutor(false);
return SwingProperties.fromSwingSource(result, new SwingForwarderFactory<ChangeListener>() {
@Override
public ChangeListener createForwarder(final Runnable listener) {
return new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
listenerExecutor.execute(listener);
}
};
}
});
}
private static <T> void addNodeListNodes(NodeList<T> nodeList, List<SingleNodeFactory> toPopulate) {
for (T key: nodeList.keys()) {
toPopulate.add(new NodeListNodeFactory<>(nodeList, key));
}
}
private static void addNotifyAll(Collection<NodeList<?>> nodeLists) {
for (NodeList<?> nodeList: nodeLists) {
try {
nodeList.addNotify();
} catch (Throwable ex) {
LOGGER.log(Level.SEVERE, "Exception thrown by NodeList.addNotify: " + nodeList, ex);
}
}
}
private static void removeNotifyAll(Collection<NodeList<?>> nodeLists) {
for (NodeList<?> nodeList: nodeLists) {
try {
nodeList.removeNotify();
} catch (Throwable ex) {
LOGGER.log(Level.SEVERE, "Exception thrown by NodeList.removeNotify: " + nodeList, ex);
}
}
}
public void addNotify() {
nodeLock.lock();
try {
removedChildren = false;
} finally {
nodeLock.unlock();
}
removeChildrenRef.fireOnChange();
}
public void removeNotify() {
List<NodeList<?>> removed;
nodeLock.lock();
try {
removedChildren = true;
removed = new ArrayList<>(currentNodeLists);
currentNodeLists = Collections.emptySet();
} finally {
nodeLock.unlock();
}
removeChildrenRef.fireOnChange();
removeNotifyAll(removed);
}
public PropertySource<Collection<SingleNodeFactory>> nodeFactories() {
return singleNodeFactories;
}
private class RemovedChildrenProperty implements PropertySource<Boolean> {
private final ListenerManager<Runnable> changeListeners;
private final EventDispatcher<Runnable, Void> listenerDispatcher;
public RemovedChildrenProperty() {
this.changeListeners = new CopyOnTriggerListenerManager<>();
final UpdateTaskExecutor listenerExecutor = new SwingUpdateTaskExecutor(false);
this.listenerDispatcher = new EventDispatcher<Runnable, Void>() {
@Override
public void onEvent(Runnable eventListener, Void arg) {
listenerExecutor.execute(eventListener);
}
};
}
public void fireOnChange() {
changeListeners.onEvent(listenerDispatcher, null);
}
@Override
public Boolean getValue() {
nodeLock.lock();
try {
return removedChildren;
} finally {
nodeLock.unlock();
}
}
@Override
public ListenerRef addChangeListener(Runnable listener) {
return changeListeners.registerListener(listener);
}
}
private static class NodeFactories implements PropertySource<Collection<? extends NodeFactory>> {
private final AtomicReference<Lookup.Result<NodeFactory>> nodeListsRef;
private final NbSupplier<? extends Lookup> factoryLookupProvider;
private final UpdateTaskExecutor listenerExecutor;
public NodeFactories(NbSupplier<? extends Lookup> factoryLookupProvider) {
assert factoryLookupProvider != null;
this.factoryLookupProvider = factoryLookupProvider;
this.nodeListsRef = new AtomicReference<>(null);
this.listenerExecutor = new SwingUpdateTaskExecutor(false);
}
private Lookup.Result<NodeFactory> getNodeListsResult() {
Lookup.Result<NodeFactory> result = nodeListsRef.get();
if (result == null) {
Lookup nodeListsLookup = factoryLookupProvider.get();
result = nodeListsLookup.lookupResult(NodeFactory.class);
if (!nodeListsRef.compareAndSet(null, result)) {
result = nodeListsRef.get();
}
}
return result;
}
@Override
public Collection<? extends NodeFactory> getValue() {
return getNodeListsResult().allInstances();
}
@Override
public ListenerRef addChangeListener(final Runnable listener) {
final LookupListener wrapper = new LookupListener() {
@Override
public void resultChanged(LookupEvent ev) {
listenerExecutor.execute(listener);
}
};
final Lookup.Result<NodeFactory> nodeLists = getNodeListsResult();
nodeLists.addLookupListener(wrapper);
return NbListenerRefs.fromRunnable(new Runnable() {
@Override
public void run() {
nodeLists.removeLookupListener(wrapper);
}
});
}
}
private static final class NodeListNodeFactory<T> implements SingleNodeFactory {
private final NodeList<? super T> nodeList;
private final T key;
public NodeListNodeFactory(NodeList<? super T> nodeList, T key) {
this.nodeList = nodeList;
this.key = key;
}
@Override
public Node createNode() {
return nodeList.node(key);
}
@Override
public int hashCode() {
return 355 + Objects.hashCode(key);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
final NodeListNodeFactory<?> other = (NodeListNodeFactory<?>)obj;
return Objects.equals(this.key, other.key);
}
}
}