/* * 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.extapi.model; import jetbrains.mps.extapi.module.SModuleBase; import jetbrains.mps.extapi.persistence.FileDataSource; import jetbrains.mps.extapi.persistence.ModelSourceChangeTracker; import jetbrains.mps.extapi.persistence.ModelSourceChangeTracker.ReloadCallback; import jetbrains.mps.extapi.persistence.SourceRoot; import jetbrains.mps.extapi.persistence.SourceRootKinds; import jetbrains.mps.logging.Logger; import jetbrains.mps.persistence.DataSourceFactoryBridge.CompositeResult; import jetbrains.mps.persistence.DataSourceFactoryNotFoundException; import jetbrains.mps.persistence.DefaultModelRoot; import jetbrains.mps.persistence.DataSourceFactoryBridge; import jetbrains.mps.persistence.NoSourceRootsInModelRootException; import jetbrains.mps.persistence.SourceRootDoesNotExistException; import jetbrains.mps.smodel.event.SModelFileChangedEvent; import jetbrains.mps.smodel.event.SModelRenamedEvent; import jetbrains.mps.util.FileUtil; import jetbrains.mps.util.StringUtil; import jetbrains.mps.vfs.IFile; import org.apache.log4j.LogManager; import org.jetbrains.annotations.NotNull; import org.jetbrains.mps.openapi.model.EditableSModel; import org.jetbrains.mps.openapi.model.SModelChangeListener; import org.jetbrains.mps.openapi.model.SModelName; import org.jetbrains.mps.openapi.model.SModelReference; import org.jetbrains.mps.openapi.model.SNodeChangeListener; import org.jetbrains.mps.openapi.module.SRepository; import org.jetbrains.mps.openapi.persistence.DataSource; import org.jetbrains.mps.openapi.persistence.ModelRoot; import org.jetbrains.mps.openapi.persistence.ModelSaveException; import org.jetbrains.mps.openapi.persistence.PersistenceFacade; import java.io.IOException; import java.util.List; /** * Editable model (generally) backed up by file. Implicitly bound to files due to * rename and changeModelFile methods, for a generic editable model, see {@link jetbrains.mps.smodel.EditableModelDescriptor} * evgeny, 11/21/12 */ public abstract class EditableSModelBase extends SModelBase implements EditableSModel { private static final Logger LOG = Logger.wrap(LogManager.getLogger(EditableSModelBase.class)); protected final ModelSourceChangeTracker myTimestampTracker; private boolean myChanged = false; protected EditableSModelBase(@NotNull SModelReference modelReference, @NotNull DataSource source) { super(modelReference, source); myTimestampTracker = new ModelSourceChangeTracker(new ReloadCallback() { @Override public void reloadFromDiskSafe() { doReloadFromDiskSafe(); } }); } @Override public void attach(@NotNull SRepository repository) { super.attach(repository); myTimestampTracker.attach(this); } @Override public void detach() { myTimestampTracker.detach(this); super.detach(); } @Override public boolean isChanged() { return myChanged; } @Override public void setChanged(boolean changed) { myChanged = changed; } @Override public void addRootNode(@NotNull org.jetbrains.mps.openapi.model.SNode node) { assertCanChange(); getModelData().addRootNode(node); } @Override public void removeRootNode(@NotNull org.jetbrains.mps.openapi.model.SNode node) { assertCanChange(); getModelData().removeRootNode(node); } @Override public boolean isReadOnly() { return getSource().isReadOnly(); } @Override public final void unload() { save(); if (needsReloading()) { throw new IllegalStateException("cannot unload model in a conflicting state"); } super.unload(); } @Override public void reloadFromSource() { assertCanChange(); if (getSource().getTimestamp() == -1) { SModuleBase module = (SModuleBase) getModule(); if (module != null) { module.unregisterModel(this); } return; } reloadContents(); updateTimestamp(); LOG.assertLog(!needsReloading()); } @SuppressWarnings("WeakerAccess") /*package*/ void doReloadFromDiskSafe() { assertCanChange(); if (isChanged()) { resolveDiskConflict(); } else { reloadFromSource(); } } protected abstract void reloadContents(); public void resolveDiskConflict() { fireConflictDetected(); } private boolean checkAndResolveConflictOnSave() { if (needsReloading()) { LOG.warning("Model file " + getReference().getModelName() + " was modified externally! " + "You might want to turn \"Synchronize files on frame activation/deactivation\" option on to avoid conflicts."); resolveDiskConflict(); return false; } // FIXME!!!!!!!!!!!!! // Paranoid check to avoid saving model during update (hack for MPS-6772) return !needsReloading(); } private void changeModelFile(IFile newModelFile) { assertCanChange(); if (!(getSource() instanceof FileDataSource)) { throw new UnsupportedOperationException("cannot change model file on non-file data source"); } FileDataSource source = (FileDataSource) getSource(); if (source.getFile().getPath().equals(newModelFile.getPath())) return; IFile oldFile = source.getFile(); jetbrains.mps.smodel.SModel model = getSModel(); fireBeforeModelFileChanged(new SModelFileChangedEvent(model.getModelDescriptor(), oldFile, newModelFile)); source.setFile(newModelFile); updateTimestamp(); fireModelFileChanged(new SModelFileChangedEvent(model.getModelDescriptor(), oldFile, newModelFile)); } @Override public final void save() { assertCanChange(); // probably should be changed to assert // see MPS-18545 SModel api: createModel(), setChanged(), isLoaded(), save() if (!isChanged() && !isLoaded()) { return; } //we must be in command since model save might change model by adding model/language imports //if (!mySModel.isLoading()) LOG.assertInCommand(); LOG.debug("Saving model " + getName().getLongName()); if (!checkAndResolveConflictOnSave()) { return; } boolean isSaved = false; try { boolean reload = saveModel(); setChanged(false); if (reload) { reloadContents(); } isSaved = true; } catch (IOException e) { LOG.error("Can't save " + getModelName() + ": " + e.getMessage(), e); } catch (ModelSaveException e) { fireProblemsDetected(e.getProblems()); } updateTimestamp(); if (isSaved) { fireModelSaved(); } } /** * returns true if the content should be reloaded from storage after save */ protected abstract boolean saveModel() throws IOException, ModelSaveException; @Override public void rename(String newModelName, boolean changeFile) { assertCanChange(); SModelReference oldName = getReference(); fireBeforeModelRenamed(new SModelRenamedEvent(this, oldName.getModelName(), newModelName)); // TODO update SModelId (if it contains modelName) //if(getReference().getModelId().getModelName() != null) { } SModelReference newModelReference = PersistenceFacade.getInstance().createModelReference(getReference().getModuleReference(), getReference().getModelId(), newModelName); fireBeforeModelRenamed(newModelReference); changeModelReference(newModelReference); if (!changeFile) { save(); } else { if (!(getSource() instanceof FileDataSource)) { throw new UnsupportedOperationException("cannot change model file on non-file data source"); } try { ModelRoot root = getModelRoot(); if (root instanceof DefaultModelRoot) { // todo only default model root? this code does not belong here but model root DefaultModelRoot defaultModelRoot = (DefaultModelRoot) root; IFile oldFile = ((FileDataSource) getSource()).getFile(); SourceRoot sourceRoot = findSourceRootOfMyself(oldFile, defaultModelRoot); CompositeResult<DataSource> result = new DataSourceFactoryBridge(defaultModelRoot).createFileDataSource(new SModelName(newModelName), sourceRoot); FileDataSource source = (FileDataSource) result.getDataSource(); IFile newFile = source.getFile(); if (!newFile.equals(oldFile)) { newFile.getParent().mkdirs(); newFile.createNewFile(); changeModelFile(newFile); deleteOldFile(oldFile); } save(); } } catch (DataSourceFactoryNotFoundException | NoSourceRootsInModelRootException | SourceRootDoesNotExistException e) { LOG.error(e); } } fireModelRenamed(new SModelRenamedEvent(this, oldName.getModelName(), newModelName)); fireModelRenamed(oldName); } @SuppressWarnings("ConstantConditions") private void deleteOldFile(IFile oldFile) { FileUtil.deleteWithAllEmptyDirs(oldFile); } private SourceRoot findSourceRootOfMyself(IFile oldFile, DefaultModelRoot defaultModelRoot) { List<SourceRoot> sourceRoots = defaultModelRoot.getSourceRoots(SourceRootKinds.SOURCES); SourceRoot sourceRoot = sourceRoots.get(0); // first one by default for (SourceRoot sourceRoot0 : sourceRoots) { if (oldFile.getPath().startsWith(sourceRoot0.getAbsolutePath().getPath())) { // using the same sourceRoot sourceRoot = sourceRoot0; break; } } return sourceRoot; } @NotNull private String getExtension(IFile oldFile) { return StringUtil.emptyIfNull(FileUtil.getExtension(oldFile.getName())); } @Override public void updateTimestamp() { myTimestampTracker.updateTimestamp(getSource()); } @Override public boolean needsReloading() { return myTimestampTracker.needsReloading(getSource()); } @Override public void addChangeListener(SModelChangeListener l) { getNodeEventDispatch().addChangeListener(l); } @Override public void removeChangeListener(SModelChangeListener l) { getNodeEventDispatch().removeChangeListener(l); } @Override public void addChangeListener(SNodeChangeListener l) { getNodeEventDispatch().addChangeListener(l); } @Override public void removeChangeListener(SNodeChangeListener l) { getNodeEventDispatch().removeChangeListener(l); } public String toString() { return getReference().toString() + " in " + getSource().getLocation(); } }