/*
* Copyright 2003-2017 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.ide.findusages;
import gnu.trove.TIntArrayList;
import gnu.trove.TObjectIntHashMap;
import jetbrains.mps.components.CoreComponent;
import jetbrains.mps.ide.findusages.findalgorithm.finders.GeneratedFinder;
import jetbrains.mps.ide.findusages.findalgorithm.finders.IInterfacedFinder;
import jetbrains.mps.ide.findusages.findalgorithm.finders.ReloadableFinder;
import jetbrains.mps.smodel.LanguageAspect;
import jetbrains.mps.smodel.adapter.ids.SLanguageId;
import jetbrains.mps.smodel.language.LanguageRegistry;
import jetbrains.mps.smodel.language.LanguageRegistryListener;
import jetbrains.mps.smodel.language.LanguageRuntime;
import jetbrains.mps.smodel.runtime.FindUsageAspectDescriptor;
import jetbrains.mps.smodel.runtime.FinderRegistry;
import jetbrains.mps.util.annotation.ToRemove;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SAbstractConcept;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
public final class FindersManager implements CoreComponent, LanguageRegistryListener {
private static final Logger LOG = LogManager.getLogger(FindersManager.class.getName());
private static FindersManager INSTANCE;
public static FindersManager getInstance() {
return INSTANCE;
}
private final Map<GeneratedFinder, SNodeReference> myNodesByFinder = new HashMap<>();
// XXX the only place I use SLanguageId map key is compatibility with legacy #addFinder(), to match SModuleReference to LanguageRuntime
private final Map<SLanguageId, LanguageFinders> myLanguageFindersMap = new HashMap<>();
private boolean myLoaded = false;
private LanguageRegistry myLanguageRegistry;
public FindersManager(LanguageRegistry languageRegistry) {
myLanguageRegistry = languageRegistry;
}
@Override
public void init() {
if (INSTANCE != null) {
throw new IllegalStateException("double initialization");
}
INSTANCE = this;
myLanguageRegistry.addRegistryListener(this);
}
@Override
public void dispose() {
myLanguageRegistry.removeRegistryListener(this);
INSTANCE = null;
}
public Set<IInterfacedFinder> getAvailableFinders(final SNode node) {
checkLoaded();
final Set<IInterfacedFinder> result = new HashSet<>();
for (LanguageFinders lf : myLanguageFindersMap.values()) {
try {
lf.findersForConcept(node.getConcept()).filter(finder -> finder.isVisible(node) && finder.isApplicable(node)).forEach(result::add);
} catch (Throwable t) {
LOG.error("Finder's isApplicable method failed " + t.getMessage(), t);
}
}
return Collections.unmodifiableSet(result);
}
/**
* Generally, external code shall not care to get reloadable finder directly, it's for specific scenarios like query persistence in Find Usages view.
* @deprecated Though we still use finder implementation class fqn as a finder persistable identity, I don't want this exposed in the method name.
* Left for compatibility with external code (just in case there's some). Perhaps, should bear explicit 'Proxy' or 'Reloadable' part.
*/
@Nullable
@Deprecated
@ToRemove(version = 3.5)
public ReloadableFinder getFinderByClassName(String className) {
IInterfacedFinder finder = getFinder(className);
return finder == null ? null : new ReloadableFinder(className);
}
/**
* @param finderIdentity at the moment, fqn of finder implementation class. NOTE, it's not used for classloading as is, merely as identifier to find
* registered implementation
* @return {@code null} if no finder with supplied identity found or identity is null.
*/
@Nullable
public IInterfacedFinder getFinder(@Nullable String finderIdentity) {
// Function.identity magic is to convey the idea finderIdentity is an identity, not a class name.
// and to avoid IDEA's warning, too ;)
final String className = Function.<String>identity().apply(finderIdentity);
if (className == null) {
return null;
}
checkLoaded();
final String aspectNameWithDots = '.' + LanguageAspect.FIND_USAGES.getName() + '.';
int aspectNamePos = className.lastIndexOf(aspectNameWithDots);
final String cnSuffix = "_Finder";
if (aspectNamePos == -1 || !className.endsWith(cnSuffix)) {
return null;
}
final String declaringLanguageName = className.substring(0, aspectNamePos);
// finderMangledName == NameUtil.toValidIdentifier(finder.name)
final String finderMangledName = className.substring(aspectNamePos + aspectNameWithDots.length(), className.length() - cnSuffix.length());
for (LanguageFinders lf : myLanguageFindersMap.values()) {
if (!lf.matchesLanguage(declaringLanguageName)) {
continue;
}
return lf.findByMangledName(finderMangledName);
}
return null;
}
//-------------reloading stuff----------------
private void checkLoaded() {
if (myLoaded) {
return;
}
myLoaded = true;
load();
}
private void load() {
Collection<LanguageRuntime> availableLanguages = myLanguageRegistry.getAvailableLanguages();
if (availableLanguages == null) {
return;
}
for (LanguageRuntime language : availableLanguages) {
initFindersDescriptor(language);
}
}
private void clear() {
myLanguageFindersMap.clear();
myNodesByFinder.clear();
myLoaded = false;
}
private void initFindersDescriptor(LanguageRuntime language) {
try {
FindUsageAspectDescriptor descr = language.getAspect(FindUsageAspectDescriptor.class);
if (descr != null) {
// FIXME shall refactor load/clear mechanism to drop/load relevant LanguageFinder instances only.
assert !myLanguageFindersMap.containsKey(language.getId()) : "At the moment, there's clear() once any language is unloaded, we shall not replace finders.";
LanguageFinders finders = new LanguageFinders(language);
myLanguageFindersMap.put(language.getId(), finders);
descr.init(finders);
}
} catch (Throwable throwable) {
LOG.error("Error while initializing find usages descriptor for language " + language.getNamespace(), throwable);
}
}
@Override
public void afterLanguagesLoaded(Iterable<LanguageRuntime> languages) {
}
@Override
public void beforeLanguagesUnloaded(Iterable<LanguageRuntime> languages) {
// FIXME shall drop relevant LanguageFinder instances only!
// However myNodesByFinder is global and would either keep stale entries or cleared altogether on any reload.
// Perhaps, shall drop it as it's not vital to have getDeclarationNode for legacy (non-migrated) finders.
clear();
}
// XXX doesn't care about threading, although likely should
private static final class LanguageFinders implements FinderRegistry {
private final LanguageRuntime myLanguageRuntime;
// XXX maps that keep actual instances would cease once 3.5 is out.
// Although LF would still keep reference to LR effectively holding its classloader, it's still better to
// use new finder instance for each run (to avoid concurrency management inside finders).
private final Map<SAbstractConcept, Set<GeneratedFinder>> myLegacyFinders = new HashMap<>();
private final Map<String, GeneratedFinder> myNameToFinder = new HashMap<>();
private final Map<SAbstractConcept, TIntArrayList> myFinders = new HashMap<>();
private final TObjectIntHashMap<String> myNameToFinder2 = new TObjectIntHashMap<>();
LanguageFinders(LanguageRuntime lr) {
myLanguageRuntime = lr;
}
@Override
public void add(@NotNull SAbstractConcept concept, int identityToken, @NotNull String mangledName) {
TIntArrayList finderTokens = myFinders.get(concept);
if (finderTokens == null) {
myFinders.put(concept, finderTokens = new TIntArrayList());
}
if (!finderTokens.contains(identityToken)) {
finderTokens.add(identityToken);
}
myNameToFinder2.put(mangledName, identityToken);
}
void addLegacy(GeneratedFinder finder) {
SAbstractConcept concept = finder.getSConcept();
Set<GeneratedFinder> finders;
if ((finders = myLegacyFinders.get(concept)) == null) {
myLegacyFinders.put(concept, finders = new HashSet<>());
}
finders.add(finder);
String cn = finder.getClass().getSimpleName();
assert cn.endsWith("_Finder");
GeneratedFinder previous = myNameToFinder.put(cn.substring(0, cn.length() - "_Finder".length()), finder);
assert previous == null : "Finders with same mangled name would end up as identical java classes";
}
boolean matchesLanguage(String namespace) {
return myLanguageRuntime.getNamespace().equals(namespace);
}
IInterfacedFinder findByMangledName(String finderMangledName) {
if (myNameToFinder2.contains(finderMangledName)) {
return instantiate(myNameToFinder2.get(finderMangledName));
}
return myNameToFinder.get(finderMangledName);
}
// XXX findersForNode(SNode) instead, to perform filtering isVisible+isApplicable here as well?
Stream<IInterfacedFinder> findersForConcept(SAbstractConcept c) {
return Stream.concat(myFinders.keySet().stream().filter(c::isSubConceptOf).flatMap(concept -> instantiate(myFinders.get(concept))),
myLegacyFinders.keySet().stream().filter(c::isSubConceptOf).flatMap(concept -> myLegacyFinders.get(concept).stream()));
}
private IInterfacedFinder instantiate(int token) {
FindUsageAspectDescriptor descr = myLanguageRuntime.getAspect(FindUsageAspectDescriptor.class);
// could have passed descr instance as cons argument, otoh LR keeps its instance anyway, why bother.
assert descr != null;
return descr.instantiate(token);
}
private Stream<IInterfacedFinder> instantiate(TIntArrayList tokens) {
FindUsageAspectDescriptor descr = myLanguageRuntime.getAspect(FindUsageAspectDescriptor.class);
assert descr != null;
return Arrays.stream(tokens.toNativeArray()).mapToObj(descr::instantiate);
}
}
}