/*
* 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.smodel;
import jetbrains.mps.project.AbstractModule;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SLanguage;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.module.SearchScope;
import java.util.HashSet;
import java.util.Set;
/**
* Facility to update dependencies of a model based on its actual content.
* Doesn't address model access here.
* Generally, clients shall {@link #updateUsedLanguages()} first (as it might affect implicit model imports,
* like language's accessory models), then {@link #updateImportedModels(SRepository)}. Then, if desired
* and model belongs to a proper module, {@link #updateModuleDependencies(SRepository)} could bring module's
* dependencies in the matching state. Nevertheless, methods could be invoked individually according to
* a task at hand.
*
* XXX Likely, shall reside to [smodel], but now depends from [project] (AbstractModule)
* and [kernel] (ModelImports).
*
* @author Artem Tikhomirov
* @since 2017.2
*/
public final class ModelDependencyUpdate {
private final org.jetbrains.mps.openapi.model.SModel myModel;
private final ModelDependencyScanner myModelScanner;
public ModelDependencyUpdate(@NotNull org.jetbrains.mps.openapi.model.SModel model) {
myModel = model;
myModelScanner = new ModelDependencyScanner();
myModelScanner.crossModelReferences(true).usedLanguages(true).walk(model);
}
public ModelDependencyUpdate updateUsedLanguages() {
// we look at directly mentioned languages only, and don't respect the languages they extend.
// Is it what we really want here? If we try to minimize imported models (see updateImportedModels), why
// not minimize used languages?
Set<SLanguage> modelDeclaredUsedLanguages = SModelOperations.getAllLanguageImports(myModel);
ModelImports modelImports = new ModelImports(myModel);
for (SLanguage language : myModelScanner.getUsedLanguages()) {
if (!modelDeclaredUsedLanguages.contains(language)) {
modelDeclaredUsedLanguages.add(language);
modelImports.addUsedLanguage(language);
}
}
return this;
}
/**
* Updates list of imported models, with respect to models visible through actual list of used languages.
* It's recommended to {@link #updateUsedLanguages()} first so that accessory models of used languages are
* not imported explicitly.
*
* Resembles {@code ModelImporter} and might be part of {@link ModelImports} directly
*
* @param languageModuleRepo optional repository to resolve accessory models of used languages so that they
* get imported implicitly. Implementation expects source modules of deployed languages
* to be resolved in the supplied repository. If no repository supplied, all model imports
* will be explicit.
*/
public ModelDependencyUpdate updateImportedModels(@Nullable SRepository languageModuleRepo) {
Set<org.jetbrains.mps.openapi.model.SModelReference> importedModels = new HashSet<org.jetbrains.mps.openapi.model.SModelReference>();
// XXX why allImportedModels? it gives models from used language accessories, is it what we really need here?
ModelImports modelImports = new ModelImports(myModel);
importedModels.addAll(modelImports.getImportedModels());
if (languageModuleRepo != null) {
for (SLanguage lang : modelImports.getUsedLanguages()) {
// FIXME perhaps, we shall expose accessory models (as reference) much like
// we do with getLanguageRuntimes()? We don't even need to resolve them here, SModelReference would suffice!
SModuleReference langModuleRef = lang.getSourceModuleReference();
SModule languageModule = langModuleRef == null ? null : langModuleRef.resolve(languageModuleRepo);
if (false == languageModule instanceof Language) {
continue;
}
for (SModel am : ((Language) languageModule).getAccessoryModels()) {
importedModels.add(am.getReference());
}
}
}
for (org.jetbrains.mps.openapi.model.SModelReference targetModelReference : myModelScanner.getCrossModelReferences()) {
if (importedModels.add(targetModelReference)) {
modelImports.addModelImport(targetModelReference);
}
}
return this;
}
/**
* Takes actual list of model imports and propagates these dependencies into model's module dependencies.
* Works with {@link AbstractModule} instances only, and relies on {@linkplain AbstractModule#getScope() module's scope}
* to decide whether an import is needed.
*
* XXX Does pretty much the same what {@code MissingDependenciesFixer} does.
* @param importsRepo repository where imported models could get resolved
*/
public ModelDependencyUpdate updateModuleDependencies(@NotNull SRepository importsRepo) {
SModule module = myModel.getModule();
if (module == null) {
throw new IllegalStateException("Could not update module dependencies of a model which doesn't belong to any module");
}
if (false == module instanceof AbstractModule) {
// we can update AbstractModule instances only.
return this;
}
ModelImports modelImports = new ModelImports(myModel);
SearchScope moduleScope = ((AbstractModule) module).getScope();
for (org.jetbrains.mps.openapi.model.SModelReference importRef : modelImports.getImportedModels()) {
SModel importedModel = moduleScope.resolve(importRef);
if (importedModel != null) {
// imported model is visible, nothing to do.
continue;
}
SModuleReference moduleReference = importRef.getModuleReference();
if (moduleReference == null) {
importedModel = importRef.resolve(importsRepo);
if (importedModel != null) {
moduleReference = importedModel.getModule().getModuleReference();
}
}
if (moduleReference == null) {
// XXX do I care to report import failed?
continue;
}
((AbstractModule) module).addDependency(moduleReference, false);
}
return this;
}
}