/*
* 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.util;
import jetbrains.mps.project.AbstractModule;
import jetbrains.mps.project.structure.modules.Dependency;
import jetbrains.mps.project.structure.modules.GeneratorDescriptor;
import jetbrains.mps.project.structure.modules.LanguageDescriptor;
import jetbrains.mps.project.structure.modules.mappingpriorities.MappingConfig_AbstractRef;
import jetbrains.mps.project.structure.modules.mappingpriorities.MappingConfig_ExternalRef;
import jetbrains.mps.project.structure.modules.mappingpriorities.MappingConfig_RefSet;
import jetbrains.mps.project.structure.modules.mappingpriorities.MappingConfig_SimpleRef;
import jetbrains.mps.project.structure.modules.mappingpriorities.MappingPriorityRule;
import jetbrains.mps.smodel.Generator;
import jetbrains.mps.smodel.Language;
import jetbrains.mps.smodel.SModelInternal;
import jetbrains.mps.smodel.StaticReference;
import jetbrains.mps.smodel.adapter.MetaAdapterByDeclaration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.language.SLanguage;
import org.jetbrains.mps.openapi.model.EditableSModel;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.module.SDependency;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.persistence.ModelRoot;
import org.jetbrains.mps.openapi.persistence.PersistenceFacade;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Utility class that provides model/module reference updating in a group of models/modules.
*
* Expected workflow:
* You collect models & modules where you want update references
* by {@link #addModelToAdjust(SModel, SModel)} and {@link #addModuleToAdjust(SModule, SModule)}.
*
* Than you invoke {@link #adjust()} to replace all old references with new references in collected models & modules.
*
* @author Radimir.Sorokin
*
*/
public final class ReferenceUpdater {
private final List<SModule> myModules = new ArrayList<>();
private final List<SModel> myModels = new ArrayList<>();
private final Map<SModuleReference, SModuleReference> myModuleReferenceMap = new HashMap<>();
private final Map<SModelReference, SModelReference> myModelReferenceMap = new HashMap<>();
private final Map<SLanguage, SLanguage> myUsedLanguagesMap = new HashMap<>();
private boolean myAdjusted = false;
/**
* Add {@code newModule} to adjust.
* After adjusting, other modules & models will refer to {@code newModule} instead of {@code oldModule}
* Updates inner models by default
*
* @param oldModule old module - others contain refs to it
* @param newModule new module - others will contain refs to it
*/
public void addModuleToAdjust(@NotNull SModule oldModule, @NotNull SModule newModule) throws RefUpdateException {
assertNotAdjusted();
addModuleToAdjustImpl(oldModule, newModule);
if (oldModule instanceof Language && newModule instanceof Language) {
addLanguageToAdjustImpl((Language) oldModule, (Language) newModule);
}
}
private void addModuleToAdjustImpl(@NotNull SModule oldModule, @NotNull SModule newModule) throws RefUpdateException {
myModules.add(newModule);
myModuleReferenceMap.put(oldModule.getModuleReference(), newModule.getModuleReference());
// AP let us assume that the models are in the same order (it is rational since we are _cloning_ modules)
List<ModelRoot> oldRoots = IterableUtil.asList(oldModule.getModelRoots());
List<ModelRoot> newRoots = IterableUtil.asList(newModule.getModelRoots());
if (oldRoots.size() != newRoots.size()) {
throw new RefUpdateException("The number of the model roots are not the same");
}
for (int i = 0; i < oldRoots.size(); ++i) {
ModelRoot oldRoot = oldRoots.get(i);
ModelRoot newRoot = newRoots.get(i);
if (!oldRoot.getClass().equals(newRoot.getClass())) {
throw new RefUpdateException("Model roots are of different types " + oldRoot + " " + newRoot);
}
List<SModel> oldModels = IterableUtil.asList(oldRoot.getModels());
List<SModel> newModels = IterableUtil.asList(newRoot.getModels());
if (oldModels.size() != newModels.size()) {
throw new RefUpdateException("Model roots are supposed to have the same number of models " + oldRoot + " " + newRoot);
}
for (int j = 0; j < oldModels.size(); ++j) {
SModel oldModel = oldModels.get(j);
SModel newModel = newModels.get(j);
if (!oldModel.isReadOnly()) {
addModelToAdjustImpl(oldModel, newModel);
} else {
if (!newModel.isReadOnly()) {
throw new RefUpdateException("Readonly status differs in the clone " + newModel);
}
}
}
}
}
private void addLanguageToAdjustImpl(@NotNull Language oldLanguage, @NotNull Language newLanguage) throws RefUpdateException {
myUsedLanguagesMap.put(
MetaAdapterByDeclaration.getLanguage(oldLanguage),
MetaAdapterByDeclaration.getLanguage(newLanguage)
);
if (oldLanguage.getGenerators().size() != newLanguage.getGenerators().size()) {
throw new RefUpdateException("The number of generators do not match!");
}
Iterator<Generator> newGeneratorIt = newLanguage.getGenerators().iterator();
for (Generator oldGenerator : oldLanguage.getGenerators()) {
addModuleToAdjustImpl(oldGenerator, newGeneratorIt.next());
}
}
/**
* Add {@code newModel} to adjust if it is editable.
* After adjusting, other models will refer to {@code newModel} instead of {@code oldModel}
*
* @param oldModel old model - other models contain refs to it
* @param newModel new model - other models will be contain refs to it
*/
public void addModelToAdjust(@NotNull SModel oldModel, @NotNull SModel newModel) throws RefUpdateException {
assertNotAdjusted();
if (newModel.isReadOnly()) {
throw new RefUpdateException(String.format("The model '%s' is readonly", newModel));
}
addModelToAdjustImpl(oldModel, newModel);
}
/**
* For each collected module:
* 1) update all module dependencies according to {@link #myModuleReferenceMap}
*
* For each collected model:
* 1) update all model references in model imports according to {@link #myModelReferenceMap}
* 2) update all languages imports according to {@link #myUsedLanguagesMap}
* 3) update all model references in it's nodes according to {@link #myModelReferenceMap}
*
* It saves all models after references updating.
* Note that after calling this method you can't use this instance to update references.
*/
public void adjust() throws RefUpdateException {
assertNotAdjusted();
myModules.forEach(module -> {
if (module instanceof Generator) {
adjustGenerator((Generator) module);
}else if (module instanceof Language) {
adjustLanguage((Language) module);
} else if (module instanceof AbstractModule) {
adjustModule((AbstractModule) module);
}
});
myModels.forEach(model -> {
SModelInternal modelInternal = (SModelInternal) model;
for (SModelReference aImport : modelInternal.getModelImports()) {
if (myModelReferenceMap.containsKey(aImport)) {
modelInternal.deleteModelImport(aImport);
modelInternal.addModelImport(myModelReferenceMap.get(aImport));
}
}
List<SLanguage> usedLanguages = new ArrayList<>(modelInternal.importedLanguageIds());
for (SLanguage usedLanguage : usedLanguages) {
if (myUsedLanguagesMap.containsKey(usedLanguage)) {
modelInternal.deleteLanguageId(usedLanguage);
modelInternal.addLanguage(myUsedLanguagesMap.get(usedLanguage));
}
}
model.getRootNodes().forEach(this::updateReferences);
});
myModels.forEach((model -> {
if (model instanceof EditableSModel) {
((EditableSModel) model).setChanged(true);
((EditableSModel) model).save();
}
}));
myAdjusted = true;
}
public List<SModule> getModules() {
return Collections.unmodifiableList(myModules);
}
public List<SModel> getModels() {
return Collections.unmodifiableList(myModels);
}
private void addModelToAdjustImpl(@NotNull SModel oldModel, @NotNull SModel newModel) {
if (!newModel.isReadOnly()) {
myModels.add(newModel);
}
myModelReferenceMap.put(oldModel.getReference(), newModel.getReference());
}
private void assertNotAdjusted() throws RefUpdateException {
if (myAdjusted) {
throw new RefUpdateException("ReferenceUpdater instances can't be reused");
}
}
private void updateReferences(SNode node) {
node.getReferences().forEach(ref -> {
if (ref instanceof StaticReference) {
StaticReference reference = (StaticReference) ref;
SModelReference targetSModelReference = reference.getTargetSModelReference();
if (myModelReferenceMap.containsKey(targetSModelReference)) {
StaticReference newReference = new StaticReference(
reference.getLink(),
node,
myModelReferenceMap.get(targetSModelReference),
reference.getTargetNodeId(),
reference.getResolveInfo()
);
node.setReference(newReference.getLink(), newReference);
}
}
});
node.getChildren().forEach(this::updateReferences);
}
private void adjustLanguage(Language language) {
adjustModule(language);
LanguageDescriptor descriptor = language.getModuleDescriptor();
Set<SModelReference> accessoryModels = descriptor.getAccessoryModels();
Set<SModelReference> newAccessoryModels = new LinkedHashSet<>();
for (SModelReference modelReference : accessoryModels) {
SModelReference newModelReference = myModelReferenceMap.get(modelReference);
newAccessoryModels.add(newModelReference != null ? newModelReference : modelReference);
}
accessoryModels.clear();
accessoryModels.addAll(newAccessoryModels);
language.setModuleDescriptor(descriptor);
}
private void adjustGenerator(Generator generator) {
adjustModule(generator);
GeneratorDescriptor descriptor = generator.getModuleDescriptor();
for (MappingPriorityRule rule : descriptor.getPriorityRules()) {
adjustMappingConfig(rule.getLeft());
adjustMappingConfig(rule.getRight());
}
generator.setModuleDescriptor(descriptor);
}
private void adjustMappingConfig(MappingConfig_AbstractRef config) {
if (config instanceof MappingConfig_SimpleRef) {
MappingConfig_SimpleRef config_simpleRef = (MappingConfig_SimpleRef) config;
SModelReference oldModelRef = PersistenceFacade.getInstance().createModelReference(config_simpleRef.getModelUID());
SModelReference newModelRef = myModelReferenceMap.get(oldModelRef);
if (newModelRef != null) {
config_simpleRef.setModelUID(newModelRef.toString());
}
} else if (config instanceof MappingConfig_ExternalRef) {
MappingConfig_ExternalRef config_externalRef = (MappingConfig_ExternalRef) config;
SModuleReference oldModuleRef = config_externalRef.getGenerator();
SModuleReference newModuleRef = myModuleReferenceMap.get(oldModuleRef);
if (newModuleRef != null) {
config_externalRef.setGenerator(newModuleRef);
}
adjustMappingConfig(config_externalRef.getMappingConfig());
} else if (config instanceof MappingConfig_RefSet) {
for (MappingConfig_AbstractRef configElement: ((MappingConfig_RefSet) config).getMappingConfigs()) {
adjustMappingConfig(configElement);
}
}
}
private void adjustModule(AbstractModule module) {
for (SDependency dependency : module.getDeclaredDependencies()) {
SModuleReference depReference = dependency.getTargetModule();
if (myModuleReferenceMap.containsKey(depReference)) {
module.removeDependency(new Dependency(depReference, dependency.getScope(), dependency.isReexport()));
module.addDependency(myModuleReferenceMap.get(depReference), dependency.isReexport());
}
}
}
public static final class RefUpdateException extends Exception {
public RefUpdateException(@NotNull String message) {
super(message);
}
}
}