/*
* 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.project;
import com.intellij.openapi.ui.Messages;
import jetbrains.mps.module.ReloadableModule;
import jetbrains.mps.project.dependency.VisibilityUtil;
import jetbrains.mps.smodel.Language;
import jetbrains.mps.smodel.SModelInternal;
import jetbrains.mps.smodel.adapter.MetaAdapterByDeclaration;
import jetbrains.mps.smodel.tempmodel.TemporaryModels;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.language.SLanguage;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import java.awt.Component;
import java.util.ArrayList;
import java.util.List;
/**
* Utility to add imports to a model.
* This class doesn't manage read/write access to a model, it's responsibility of a caller.
* FIXME may show UI confirmation dialog from within a command/write action, is it good?
* Now it's up to caller to decide whether to {@link #prepare(SModelReference) prepare} the importer from distinct model access,
* then, if {@link #affectsModuleDependencies()} necessary}, to show {@link #confirmModuleChanges(Component) confirmation}, and
* eventually {@link #execute() execute} in command, although I admit resulting code would be ugly.
*
* @author Alex Pyshkin
* @author Artem Tikhomirov
*/
public class ModelImporter {
private final SModel myModel;
private final List<Entry> myImports = new ArrayList<>();
public ModelImporter(@NotNull SModel model) {
myModel = model;
// This class was designed to edit fully fledged model, if your scenario needs to check for
// visibility/scope of module dependencies for free-floating model, consider passing SRepository here.
assert model.getRepository() != null;
}
public void prepare(SModelReference modelRefToImport) {
Entry e = analyzeImport(modelRefToImport);
myImports.add(e);
}
/**
* @return {@code true} if any model from {@link #prepare(SModelReference)} comes from a module not visible to that of our model,
* {@code false} either if there's no module (i.e. nothing to affect) or all dependencies are visible.
*/
public boolean affectsModuleDependencies() {
for (Entry e : myImports) {
if (e.affectsModule()) {
return true;
}
}
return false;
}
private Entry analyzeImport(final SModelReference modelRefToImport) {
SModel modelToImport = modelRefToImport.resolve(myModel.getRepository());
if (modelToImport == null) {
throw new IllegalArgumentException(String.format("Bad model reference: %s", modelRefToImport));
}
if (myModel.getModule() == null) {
// code below doesn't make sense if there's no module
return new Entry(modelRefToImport);
}
SModule moduleToImport = modelToImport.getModule();
if (VisibilityUtil.isVisible(myModel.getModule(), modelToImport) || moduleToImport == null) {
return new Entry(modelRefToImport);
}
if (moduleToImport instanceof Language && myModel.getModule() instanceof Solution && ((Language) moduleToImport).isAccessoryModel(modelRefToImport)) {
// this dubious condition traces back to https://youtrack.jetbrains.com/issue/MPS-17337
// FIXME discussed with MM, it's just a quick way to fix common scenarios, there's no particular idea behind models in Solutions to get
// used languages, while models in language get module dependency. If we manage to get rid of accessory models (in a way that we
// generate stuff from them, and then reference this generated code), we could drop this. However, it doesn't look feasible
// to throw accessory models in a foreseeable future, nor it is practical (for a modeling environment it's odd to struggle throwing models away),
// and better option is to give a choice here (provided we pop up a dialog anyway) if user meant to use language or to reference model node.
return new Entry(MetaAdapterByDeclaration.getLanguage((Language) moduleToImport));
}
return new Entry(modelRefToImport, moduleToImport.getModuleReference());
}
public void execute() {
boolean shallReload = affectsModuleDependencies();
// affectsModuleDependencies() == true implies myModel got a module, otherwise there'd be nothing to affect.
for (Entry e : myImports) {
e.addImport(myModel);
}
// Reload has meaning only for reloadable modules. Import itself does not depend on module type of model.
if (shallReload && myModel.getModule() instanceof ReloadableModule) {
((ReloadableModule) myModel.getModule()).reload();
}
}
/**
* @return {@code true} if user confirmed changes in the module (or there's no need in such confirmation)
*/
public boolean confirmModuleChanges(Component parentComponent) {
if (!affectsModuleDependencies()) {
return true;
}
if (TemporaryModels.isTemporary(myModel)) {
// I have no idea why temporary models are handled here and not in the caller, left intact.
return true;
}
StringBuilder sb = new StringBuilder();
for (Entry e : myImports) {
if (!e.affectsModule()) {
continue;
}
sb.append(String.format("Model <b>%s</b> is owned by module <b>%s</b> which is not imported.<br/>",
e.myModelToImport.getName(), e.myModuleDep.getModuleName()));
}
final String msg = String.format("<html>%s<br/>Do you want to add module imports automatically?</html>", sb.toString());
// ok / cancel is much better than yes/no. One would read 'no' as 'no module imports, but proceed with model import',
// while 'cancel' suggests whole operation would stop, an it's the way rest of the code behaves
return Messages.showOkCancelDialog(parentComponent, msg, "Module Import", Messages.getQuestionIcon()) == Messages.OK;
}
private static class Entry {
private final SModelReference myModelToImport;
private final SModuleReference myModuleDep;
// When language is specified, it means we import language's accessory model and thus need lang in dependencies
// XXX although this is dubious, why not regular module dep?
private final SLanguage myUsedLanguage;
public Entry(SModelReference modelReference) {
myModelToImport = modelReference;
myModuleDep = null;
myUsedLanguage = null;
}
// add model import and module dependency
public Entry(SModelReference modelReference, SModuleReference moduleDependency) {
myModelToImport = modelReference;
myModuleDep = moduleDependency;
myUsedLanguage = null;
}
// add used language only
public Entry(SLanguage languageToUse) {
myModelToImport = null;
myUsedLanguage = languageToUse;
myModuleDep = null;
}
public void addImport(SModel model) {
if (myModelToImport != null) {
((SModelInternal) model).addModelImport(myModelToImport);
}
if (myModuleDep != null && model.getModule() instanceof AbstractModule) {
((AbstractModule) model.getModule()).addDependency(myModuleDep, false);
}
if (myUsedLanguage != null) {
((SModelInternal) model).addLanguage(myUsedLanguage);
}
}
public boolean affectsModule() {
return myModuleDep != null;
}
}
}