/*
* Copyright 2003-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 jetbrains.mps.smodel;
import gnu.trove.THashMap;
import jetbrains.mps.util.Computable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.event.SNodeAddEvent;
import org.jetbrains.mps.openapi.event.SNodeRemoveEvent;
import org.jetbrains.mps.openapi.event.SPropertyChangeEvent;
import org.jetbrains.mps.openapi.event.SReferenceChangeEvent;
import org.jetbrains.mps.openapi.language.SAbstractConcept;
import org.jetbrains.mps.openapi.language.SConcept;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeChangeListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Base implementation for FastNodeFinder, thread-aware and map update ready.
* Doesn't track model changes. Model implementation agnostic, doesn't rely on internal SModel implementation
*
* @author Artem Tikhomirov
*/
public class BaseFastNodeFinder implements FastNodeFinder {
protected final SModel myModel;
private final ConceptNodeMap myNodeMap = new ConceptNodeMap();
public BaseFastNodeFinder(SModel model) {
myModel = model;
}
/**
* Walk associated model and build concept instance map. Subclasses
* may override e.g. if they need to control read events during the walk.
*/
protected ConceptInstanceMap build(Computable<ConceptInstanceMap> b) {
return b.compute();
}
/**
* Subclasses shall invoke once model had changed
*/
protected void added(SNode n) {
if (myNodeMap.isEmpty()) {
return;
}
ConceptInstanceMap toAdd = build(new ConceptNodeMapBuilder(n));
synchronized (myNodeMap) {
myNodeMap.merge(toAdd);
}
}
/**
* Subclasses shall invoke once model had changed
*/
protected void removed(SNode n) {
if (myNodeMap.isEmpty()) {
return;
}
ConceptInstanceMap toDelete = build(new ConceptNodeMapBuilder(n));
synchronized (myNodeMap) {
myNodeMap.forget(toDelete);
}
}
/**
* Subclasses shall invoke once model had changed
*/
protected void reset() {
synchronized (myNodeMap) {
myNodeMap.clear();
}
}
@Override
public void dispose() {
reset();
}
@NotNull
@Override
public List<SNode> getNodes(@NotNull SAbstractConcept concept, boolean includeInherited) {
// notify 'model nodes read access'
myModel.getRootNodes().iterator();
if (!myNodeMap.isEmpty()) {
return getNodesImpl(concept, includeInherited);
}
synchronized (myNodeMap) {
if (myNodeMap.isEmpty()) {
ConceptInstanceMap all = build(new ConceptNodeMapBuilder(myModel));
all.trimValues(); // merge may reuse lists,
myNodeMap.merge(all);
}
return getNodesImpl(concept, includeInherited);
}
}
@NotNull
private List<SNode> getNodesImpl(SAbstractConcept concept, boolean includeInherited) {
if (includeInherited) {
Set<SAbstractConcept> allDescendantsOfConcept = ConceptDescendantsCache.getInstance().getDescendants(concept);
final ArrayList<List<SNode>> nodesOfConcept = new ArrayList<List<SNode>>(allDescendantsOfConcept.size());
int cnt = 0;
synchronized (myNodeMap) { // utilize the fact values in map are immutable
for (SAbstractConcept d : allDescendantsOfConcept) {
List<SNode> n = myNodeMap.get(d);
nodesOfConcept.add(n);
cnt += n.size();
}
}
final ArrayList<SNode> result = new ArrayList<SNode>(cnt);
for (List<SNode> l : nodesOfConcept) {
result.addAll(l);
}
return result;
} else {
synchronized (myNodeMap) {
return myNodeMap.get(concept);
}
}
}
private static class ConceptNodeMapBuilder implements Computable<ConceptInstanceMap> {
private final ConceptInstanceMap myMap = new ConceptInstanceMap();
private final SNode myNodeInput;
private final SModel myModelInput;
ConceptNodeMapBuilder(SNode root) {
assert root != null;
myNodeInput = root;
myModelInput = null;
}
ConceptNodeMapBuilder(SModel model) {
assert model != null;
myModelInput = model;
myNodeInput = null;
}
@Override
public ConceptInstanceMap compute() {
if (myModelInput != null) {
fillMap(myModelInput.getRootNodes());
} else {
fillMap(Collections.singletonList(myNodeInput));
}
return myMap;
}
private void fillMap(Iterable<? extends SNode> roots) {
for (SNode root : roots) {
myMap.add(root);
fillMap(root.getChildren());
}
}
}
/**
* Simple wrap of Map('concept name' to concept instances).
*/
protected static final class ConceptInstanceMap {
private final Map<SAbstractConcept, ArrayList<SNode>> myNodes = new HashMap<SAbstractConcept, ArrayList<SNode>>();
/**
* this method doesn't expect root to be added twice to the same map (to keep impl simple)
*/
public void add(SNode root) {
SConcept concept = root.getConcept();
ArrayList<SNode> set = myNodes.get(concept);
if (set == null) {
myNodes.put(concept, set = new ArrayList<SNode>());
}
set.add(root);
}
public void trimValues() {
for (ArrayList<SNode> v : myNodes.values()) {
v.trimToSize();
}
}
}
/**
* Concurrency-aware, updatable storage of concept to instance map.
* Collections of instances are immutable
*/
private static final class ConceptNodeMap {
private final Map<SAbstractConcept, List<SNode>> myNodes = new THashMap<SAbstractConcept, List<SNode>>();
public void forget(ConceptInstanceMap other) {
for (SAbstractConcept cn : other.myNodes.keySet()) {
assert myNodes.containsKey(cn); // other shall be subset of this map
List<SNode> nodes = myNodes.get(cn);
LinkedHashSet<SNode> newNodes = new LinkedHashSet<SNode>(nodes);
newNodes.removeAll(other.myNodes.get(cn));
if (newNodes.isEmpty()) {
myNodes.remove(cn);
} else {
myNodes.put(cn, new ArrayList<SNode>(newNodes));
}
}
}
public void merge(ConceptInstanceMap other) {
for (SAbstractConcept cn : other.myNodes.keySet()) {
List<SNode> nodes = myNodes.get(cn);
if (nodes == null) {
myNodes.put(cn, other.myNodes.get(cn));
} else {
LinkedHashSet<SNode> newNodes = new LinkedHashSet<SNode>(nodes);
newNodes.addAll(other.myNodes.get(cn));
myNodes.put(cn, new ArrayList<SNode>(newNodes));
}
}
}
public List<SNode> get(SAbstractConcept conceptFQName) {
List<SNode> n = myNodes.get(conceptFQName);
return n == null ? Collections.<SNode>emptyList() : n;
}
public boolean isEmpty() {
return myNodes.isEmpty();
}
public void clear() {
myNodes.clear();
}
}
/**
* Handy, ready-to use listener implementation to use.
* BaseFastNodeFinder itself doesn't track model changes, it's up to subclass to decide whether change tracking is vital.
*/
protected class ChangeTracker implements SNodeChangeListener {
public ChangeTracker() {
}
@Override
public void nodeAdded(@NotNull SNodeAddEvent event) {
added(event.getChild());
}
@Override
public void nodeRemoved(@NotNull SNodeRemoveEvent event) {
removed(event.getChild());
}
@Override
public void propertyChanged(@NotNull SPropertyChangeEvent event) {
// no-op
}
@Override
public void referenceChanged(@NotNull SReferenceChangeEvent event) {
// no-op, FNF doesn't depend on references, structure only
}
}
}