/*
* Copyright 2003-2016 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.java.stub;
import jetbrains.mps.project.dependency.GlobalModuleDependenciesManager;
import jetbrains.mps.project.dependency.GlobalModuleDependenciesManager.Deptype;
import jetbrains.mps.smodel.DynamicReference;
import jetbrains.mps.smodel.SModelStereotype;
import jetbrains.mps.util.IterableUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.language.SReferenceLink;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelName;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeId;
import org.jetbrains.mps.openapi.model.SReference;
import org.jetbrains.mps.openapi.module.SModule;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Look up models with same name among all visible models for a given module.
* Has nothing to do with stubs except for the fact its only use is in java stub resolution mechanism.
* @author Artem Tikhomirov
*/
public final class StubReferenceFactory implements ReferenceFactory {
private final SModule myModule;
private final VisibleModel myModel;
private final String myModelLongName;
private final SModelReference myModelReference;
// 1. we used to keep this cache separately, in StubModelsResolver, which might be better approach
// if we decide to re-use this cache throughout all models loaded within a module. We didn't use this cache,
// and created a new one for each SReferenceCreator, and it didn't cause any performance issue, thus moved into single class and not reused.
// 2. We wrap SModel into VisibleModel to keep set of known model roots along with the model, to avoid attempt to load model again and again from model.getNode()
private final Map<SModelName, List<VisibleModel>> myName2Models = new HashMap<SModelName, List<VisibleModel>>();
private final Set<SModelReference> myModelImports = new HashSet<SModelReference>();
/**
* @param module module we try to resolve references in, provides dependencies
* @param model model we try to resolve references in, ensures priority of local nodes over those from dependencies
*/
public StubReferenceFactory(@NotNull SModule module, @NotNull SModel model) {
myModule = module;
myModel = new VisibleModel(model);
myModelReference = model.getReference();
myModelLongName = model.getName().getLongName();
}
@NotNull
@Override
public SReference create(SNode source, String pack, SNodeId targetNodeId, SReferenceLink role, String resolveInfo, SNodeId targetTopClassifier) {
if (pack.equals(myModelLongName)) {
if (myModel.isKnownRoot(targetTopClassifier)) {
return jetbrains.mps.smodel.SReference.create(role, source, myModelReference, targetNodeId, resolveInfo);
}
}
Collection<VisibleModel> possibleModels = findModels(new SModelName(pack, SModelStereotype.JAVA_STUB));
if (possibleModels.isEmpty()) {
return jetbrains.mps.smodel.SReference.create(role, source, null, targetNodeId, resolveInfo);
}
// first, try to find match
for (VisibleModel vm : possibleModels) {
final SModelReference modelRef = vm.getModelReference();
if (myModelReference.equals(modelRef)) {
continue;
}
if (vm.isKnownRoot(targetTopClassifier)) {
addImport(modelRef);
return jetbrains.mps.smodel.SReference.create(role, source, modelRef, targetNodeId, resolveInfo);
}
}
// ok, there are matching models, and none knows the node with targetNodeId
if (possibleModels.size() == 1) {
// only one possible model
SModelReference targetModel = possibleModels.iterator().next().getModelReference();
addImport(targetModel);
return jetbrains.mps.smodel.SReference.create(role, source, targetModel, targetNodeId, resolveInfo);
} else {
// XXX not quite sure if dynamic reference is reasonable here
// anyway, this is the way it was
for (VisibleModel m : possibleModels) {
addImport(m.getModelReference());
}
return DynamicReference.createDynamicReference(role, source, pack, resolveInfo);
}
}
private void addImport(SModelReference mr) {
myModelImports.add(mr);
}
public Collection<SModelReference> getImports() {
myModelImports.remove(myModelReference); // just in case it's there
return myModelImports;
}
/**
* FIXME use SModelId, once have switched to package id without module id (now need module reference to build stub ModuleId)
* Also, shall use myModule.resolveInDependencies() then, to keep GMDM knowledge private to module implementation
* @param modelName qualified name including stereotype (if any), not <code>null</code>
* @return ordered collection, first come local matches, if any; never <code>null</code>
*/
private List<VisibleModel> findModels(SModelName modelName) {
if (myName2Models.isEmpty()) {
ensureInitialized();
}
final List<VisibleModel> rv = myName2Models.get(modelName);
return rv == null ? Collections.<VisibleModel>emptyList() : Collections.unmodifiableList(rv);
}
private void ensureInitialized() {
LinkedHashSet<SModel> visibleModels = new LinkedHashSet<SModel>();
// local models get precedence over those from imports
visibleModels.addAll(IterableUtil.asCollection(myModule.getModels()));
for (SModule visibleModule : new GlobalModuleDependenciesManager(myModule).getModules(Deptype.VISIBLE)) {
visibleModels.addAll(IterableUtil.asCollection(visibleModule.getModels()));
}
for (SModel model : visibleModels) {
final SModelName modelName = model.getName();
List<VisibleModel> modelsFromCache = myName2Models.get(modelName);
if (modelsFromCache == null) {
myName2Models.put(modelName, modelsFromCache = new ArrayList<VisibleModel>(3));
}
modelsFromCache.add(new VisibleModel(model));
}
}
/**
* In addition to model, keep its root ids, to avoid deadlock when
* two models in parallel reads simultaneously are in update model and resolve references to each other with getNode().
* Since all we need is to check presence of top classifier, and not interested in full model load, we just cache
* known classifiers and proceed gracefully if no match found.
* <p/>
* Indeed, this is sort of a hack, were we rely on hidden knowledge java stub models are loaded (or could be safely loaded
* under given state) at least to the level of top classifiers.
*/
private static class VisibleModel {
private final SModel myModel;
private final Set<SNodeId> myKnownRoots;
public VisibleModel(SModel model) {
myModel = model;
myKnownRoots = new HashSet<SNodeId>();
for (SNode n : model.getRootNodes()) {
myKnownRoots.add(n.getNodeId());
}
}
public boolean isKnownRoot(SNodeId nodeId) {
return myKnownRoots.contains(nodeId);
}
public SModelReference getModelReference() {
return myModel.getReference();
}
}
}