package jetbrains.mps.vcs.core.mergedriver;
/*Generated by MPS */
import org.apache.log4j.Logger;
import org.apache.log4j.LogManager;
import org.jetbrains.mps.openapi.model.SModelName;
import org.jetbrains.annotations.Nullable;
import jetbrains.mps.baseLanguage.tuples.runtime.Tuples;
import jetbrains.mps.RuntimeFlags;
import jetbrains.mps.project.MPSExtentions;
import org.jetbrains.mps.openapi.model.SModel;
import org.apache.log4j.Level;
import jetbrains.mps.baseLanguage.closures.runtime.Wrappers;
import jetbrains.mps.vcs.diff.merge.MergeSession;
import jetbrains.mps.smodel.ModelAccess;
import jetbrains.mps.internal.collections.runtime.Sequence;
import jetbrains.mps.internal.collections.runtime.IWhereFilter;
import jetbrains.mps.vcs.diff.changes.ModelChange;
import jetbrains.mps.internal.collections.runtime.ListSequence;
import jetbrains.mps.persistence.PersistenceUtil;
import jetbrains.mps.baseLanguage.tuples.runtime.MultiTuple;
import jetbrains.mps.util.FileUtil;
import java.io.File;
import jetbrains.mps.vcs.util.MergeDriverBackupUtil;
import java.io.IOException;
import jetbrains.mps.persistence.PersistenceVersionAware;
import org.jetbrains.mps.openapi.persistence.ModelFactory;
import org.jetbrains.mps.openapi.persistence.PersistenceFacade;
import java.util.HashMap;
import jetbrains.mps.persistence.MetaModelInfoProvider;
import jetbrains.mps.smodel.DefaultSModel;
import jetbrains.mps.extapi.model.SModelBase;
import jetbrains.mps.extapi.model.SModelData;
/*package*/ class ModelMerger extends SimpleMerger {
private static final Logger LOG = LogManager.getLogger(ModelMerger.class);
private SModelName myModelName;
private String myExtension;
public ModelMerger(String extension) {
myExtension = extension;
}
@Override
@Nullable
public Tuples._2<Integer, byte[]> mergeContents(FileContent baseContent, FileContent localContent, FileContent latestContent) {
if (Boolean.getBoolean("mps.mergedriver.model.fail")) {
// fail, so the merge will be done in full MPS
return null;
}
RuntimeFlags.setMergeDriverMode(true);
String ext = (myExtension == null ? MPSExtentions.MODEL : myExtension);
if (MPSExtentions.MODEL_HEADER.equals(myExtension) || MPSExtentions.MODEL_ROOT.equals(myExtension)) {
// special support for per-root persistence
ext = MPSExtentions.MODEL;
}
if (LOG.isInfoEnabled()) {
LOG.info("Reading models...");
}
final SModel baseModel = loadModel(baseContent, ext);
final SModel localModel = loadModel(localContent, ext);
final SModel latestModel = loadModel(latestContent, ext);
if (baseModel == null || localModel == null || latestModel == null) {
return backup(baseContent, localContent, latestContent);
}
myModelName = baseModel.getName();
int baseP = getPersistenceVersion(baseModel);
int localP = getPersistenceVersion(localModel);
int latestP = getPersistenceVersion(latestModel);
if (baseP >= 7 && localP >= 7 && latestP >= 7 || baseP < 7 && localP < 7 && latestP < 7) {
// ok, can merge
} else {
if (LOG.isEnabledFor(Level.ERROR)) {
LOG.error(String.format("%s: Conflicting model persistence versions", myModelName));
}
return backup(baseContent, localContent, latestContent);
}
try {
if (LOG.isInfoEnabled()) {
LOG.info("Merging " + baseModel.getReference() + "...");
}
final Wrappers._T<MergeSession> mergeSession = new Wrappers._T<MergeSession>(null);
ModelAccess.instance().runReadAction(new Runnable() {
public void run() {
mergeSession.value = MergeSession.createMergeSession(baseModel, localModel, latestModel);
}
});
int conflictingChangesCount = Sequence.fromIterable(mergeSession.value.getAllChanges()).where(new IWhereFilter<ModelChange>() {
public boolean accept(ModelChange c) {
return Sequence.fromIterable(mergeSession.value.getConflictedWith(c)).isNotEmpty();
}
}).count();
if (conflictingChangesCount == 0) {
if (LOG.isInfoEnabled()) {
LOG.info(String.format("%s: %d changes detected: %d local and %d latest.", myModelName, Sequence.fromIterable(mergeSession.value.getAllChanges()).count(), ListSequence.fromList(mergeSession.value.getMyChangeSet().getModelChanges()).count(), ListSequence.fromList(mergeSession.value.getRepositoryChangeSet().getModelChanges()).count()));
}
ModelAccess.instance().runReadAction(new Runnable() {
public void run() {
mergeSession.value.applyChanges(mergeSession.value.getAllChanges());
}
});
} else {
if (LOG.isInfoEnabled()) {
LOG.info(String.format("%s: %d changes detected, %d of them are conflicting", myModelName, Sequence.fromIterable(mergeSession.value.getAllChanges()).count(), conflictingChangesCount));
}
return backup(baseContent, localContent, latestContent);
}
if (mergeSession.value.hasIdsToRestore()) {
if (LOG.isInfoEnabled()) {
LOG.info(String.format("%s: node id duplication detected, should merge in UI.", myModelName));
}
} else {
String resultString;
SModel resultModel = mergeSession.value.getResultModel();
if (LOG.isInfoEnabled()) {
LOG.info(String.format("%s: Saving merged model...", myModelName));
}
updateMetaModelInfo(resultModel, baseModel, localModel, latestModel);
if (MPSExtentions.MODEL_HEADER.equals(myExtension) || MPSExtentions.MODEL_ROOT.equals(myExtension)) {
// special support for per-root persistence
resultString = PersistenceUtil.savePerRootModel(resultModel, MPSExtentions.MODEL_HEADER.equals(myExtension));
} else {
resultString = PersistenceUtil.saveModel(resultModel, ext);
}
if (resultString == null) {
if (LOG.isEnabledFor(Level.ERROR)) {
LOG.error("Error while saving result model");
}
return backup(baseContent, localContent, latestContent);
}
if (LOG.isInfoEnabled()) {
LOG.info(String.format("%s: merged successfully.", myModelName));
}
backup(baseContent, localContent, latestContent);
return MultiTuple.<Integer,byte[]>from(MERGED, resultString.getBytes(FileUtil.DEFAULT_CHARSET));
}
} catch (Throwable e) {
if (LOG.isEnabledFor(Level.ERROR)) {
LOG.error("Exception while merging", e);
}
}
return backup(baseContent, localContent, latestContent);
}
private Tuples._2<Integer, byte[]> backup(FileContent baseContent, FileContent localContent, FileContent latestContent) {
try {
File zipModel = MergeDriverBackupUtil.zipModel(new byte[][]{baseContent.getData(), localContent.getData(), latestContent.getData()}, myModelName);
if (zipModel != null) {
if (LOG.isInfoEnabled()) {
LOG.info("Saved merge backup to " + zipModel);
}
}
} catch (IOException e) {
if (LOG.isEnabledFor(Level.ERROR)) {
LOG.error(String.format("%s: exception while backuping", myModelName), e);
}
}
return null;
}
private static int getPersistenceVersion(SModel model) {
if (model instanceof PersistenceVersionAware) {
return ((PersistenceVersionAware) model).getPersistenceVersion();
}
return -1;
}
/*package*/ static SModel loadModel(FileContent content, String fnameExtension) {
ModelFactory modelFactory = PersistenceFacade.getInstance().getModelFactory(fnameExtension);
if (modelFactory == null) {
return null;
}
HashMap<String, String> options = new HashMap<String, String>();
options.put(ModelFactory.OPTION_CONTENT_ONLY, Boolean.TRUE.toString());
options.put(MetaModelInfoProvider.OPTION_KEEP_READ_METAINFO, Boolean.TRUE.toString());
try {
return modelFactory.load(content, options);
} catch (IOException ex) {
if (LOG.isEnabledFor(Level.WARN)) {
LOG.warn("Failed to read model", ex);
}
}
return null;
}
private static void updateMetaModelInfo(SModel resultModel, SModel baseModel, SModel localModel, SModel remoteModel) {
// we don't care to fix MetaModelInfoProvider for versions it was not utilized in.
if (getPersistenceVersion(resultModel) < 9) {
return;
}
DefaultSModel resultModelInternal = tryInternalModelData(resultModel);
if (resultModelInternal == null) {
return;
}
DefaultSModel baseModelInternal = tryInternalModelData(baseModel);
DefaultSModel localModelInternal = tryInternalModelData(localModel);
DefaultSModel remoteModelInternal = tryInternalModelData(remoteModel);
// if there's nothing collected during model read, can't help but let it go
if (baseModelInternal == null && localModelInternal == null && remoteModelInternal == null) {
return;
}
// build sequence of meta-info providers, so that result model would consult local, remote, base and own MMIP sequentially, trying to find meta-info
// If none succeed, fail with null values from BaseMetaModelInfo. Allow MMIP from result model to answer differently
MetaModelInfoProvider delegate = resultModelInternal.getSModelHeader().getMetaInfoProvider();
if (delegate == null) {
delegate = new MetaModelInfoProvider.BaseMetaModelInfo();
}
for (DefaultSModel m : new DefaultSModel[]{baseModelInternal, remoteModelInternal, localModelInternal}) {
MetaModelInfoProvider.StuffedMetaModelInfo provider;
if ((provider = tryStuffedProvider(m)) != null) {
MetaModelInfoProvider.StuffedMetaModelInfo nextInChain = new MetaModelInfoProvider.StuffedMetaModelInfo(delegate);
provider.populate(nextInChain);
delegate = nextInChain;
}
}
resultModelInternal.getSModelHeader().setMetaInfoProvider(delegate);
return;
}
private static DefaultSModel tryInternalModelData(SModel model) {
if (model instanceof SModelBase) {
SModelData modelData = ((SModelBase) model).getModelData();
return (modelData instanceof DefaultSModel ? ((DefaultSModel) modelData) : null);
}
return null;
}
private static MetaModelInfoProvider.StuffedMetaModelInfo tryStuffedProvider(DefaultSModel model) {
if (model == null) {
return null;
}
MetaModelInfoProvider p = model.getSModelHeader().getMetaInfoProvider();
if (p instanceof MetaModelInfoProvider.StuffedMetaModelInfo) {
return ((MetaModelInfoProvider.StuffedMetaModelInfo) p);
}
return null;
}
}