/* * 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.extapi.model; import jetbrains.mps.extapi.module.SModuleBase; import jetbrains.mps.smodel.IllegalModelAccessException; import jetbrains.mps.smodel.InvalidSModel; import jetbrains.mps.smodel.MPSModuleRepository; import jetbrains.mps.smodel.event.ModelEventDispatch; import jetbrains.mps.smodel.event.ModelListenerDispatch; import jetbrains.mps.smodel.loading.ModelLoadingState; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.language.SConcept; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SModelAccessListener; import org.jetbrains.mps.openapi.model.SModelId; import org.jetbrains.mps.openapi.model.SModelListener; import org.jetbrains.mps.openapi.model.SModelName; import org.jetbrains.mps.openapi.model.SModelReference; import org.jetbrains.mps.openapi.model.SNode; import org.jetbrains.mps.openapi.model.SNodeAccessListener; import org.jetbrains.mps.openapi.model.SNodeChangeListener; import org.jetbrains.mps.openapi.model.SNodeId; import org.jetbrains.mps.openapi.module.SModule; import org.jetbrains.mps.openapi.module.SRepository; import org.jetbrains.mps.openapi.persistence.DataSource; import org.jetbrains.mps.openapi.persistence.ModelRoot; import java.util.Collections; /** * Base implementation of {@link org.jetbrains.mps.openapi.model.SModel}, with actual * {@link jetbrains.mps.extapi.model.SModelData model data} kept separately, ready for e.g. re-load. * * This implementation tracks load state of the model data ({@link #getLoadingState()}) and expects * subclasses to {@link #setLoadingState(ModelLoadingState) update} this state appropriately. * * {@link #getModelData()} provides access to actual node storage. * * TODO relocate to [smodel] */ public abstract class SModelBase extends SModelDescriptorStub implements SModel { private static final Logger LOG = LogManager.getLogger(SModelBase.class); private final ModelEventDispatch myNodeEventDispatch; // XXX when necessary, shall get exposed with protected accessor. fire* methods kept for now as some of them do delegation to legacy // listeners as well, could get removed once smodel.SModelListener gone. Besides, I'm not yet sure multi-cast approach of // ModelListenerDispatch shall prevail, to limit future changes, let fire* method stay non deprecated for now. private final ModelListenerDispatch myModelEventDispatch; @NotNull private final DataSource mySource; @NotNull private SModelReference myModelReference; @Nullable private ModelRoot myModelRoot; private SModule myModule; private volatile SRepository myRepository = null; /** * model is treated {@link #isLoaded() loaded} when the state == FULLY_LOADED. * There are model implementations with simple NOT_LOADED -- FULLY_LOADED cycle, * and more complex with NOT_LOADED -- INTERFACE_LOADED -- FULLY_LOADED. */ private ModelLoadingState myModelLoadState = ModelLoadingState.NOT_LOADED; protected SModelBase(@NotNull SModelReference modelReference, @NotNull DataSource source) { myModelReference = modelReference; mySource = source; myNodeEventDispatch = new ModelEventDispatch(this); myModelEventDispatch = new ModelListenerDispatch(); } @Override public SRepository getRepository() { // assertCanRead(); we don't require write lock when myRepo is assigned, why would require read to get? return myRepository; } @Override public SNode createNode(@NotNull SConcept concept) { // nodeId should be model's responsibility, not SNode's as we shall migrate towards model-local node ids, preferably int instead of long, // and at least not random return new jetbrains.mps.smodel.SNode(concept, jetbrains.mps.smodel.SModel.generateUniqueId()); } @Override public SNode createNode(@NotNull SConcept concept, @Nullable SNodeId nodeId) { if (nodeId == null) { nodeId = jetbrains.mps.smodel.SModel.generateUniqueId(); } return new jetbrains.mps.smodel.SNode(concept, nodeId); } public void attach(@NotNull SRepository repo) { if (myRepository == repo) { LOG.warn("The model " + this + " is already attached to the repository " + repo); return; } if (myRepository != null) { throw new IllegalModelAccessException("Model is already attached to a repository, can't attach to another one"); } repo.getModelAccess().checkReadAccess(); myRepository = repo; myModelEventDispatch.modelAttached(this, repo); } public void detach() { assertCanChange(); if (myRepository != null) { myModelEventDispatch.modelDetached(this, myRepository); myRepository = null; } fireBeforeModelDisposed(this); jetbrains.mps.smodel.SModel model = getCurrentModelInternal(); if (model != null) { model.dispose(); } clearListeners(); } @Override public Iterable<SNode> getRootNodes() { assertCanRead(); return getModelData().getRootNodes(); } @Override public SNode getNode(SNodeId id) { assertCanRead(); return getModelData().getNode(id); } @Override @NotNull public SModelReference getReference() { // assertCanRead(); model reference is read-only attribute, why care about read lock? return myModelReference; } @NotNull @Override public SModelId getModelId() { // assertCanRead(); model reference is read-only attribute, why care about read lock? return myModelReference.getModelId(); } @Override @Deprecated public String getModelName() { // assertCanRead(); model reference is read-only attribute, why care about read lock? return myModelReference.getModelName(); } @NotNull @Override public SModelName getName() { return myModelReference.getName(); } @Override @NotNull public DataSource getSource() { // assertCanRead(); Is source access truly read operation over model? return mySource; } public void setModule(SModule module) { assertCanRead(); // FIXME why not write? myModule = module; } /** * TODO make final */ @Override @Nullable public SModule getModule() { // FIXME provided setModule() requires read lock, another read lock here doesn't prevent from // myModule being modified in a parallel read, and the reason to have read check here eludes from me. // Code like SModuleOperations.getOutputRoot(SModel) fails with assert enabled, and // it's not obvious whether it's the client code to fix (to obtain read lock) or // this method shall not check for read access at all. // assertCanRead(); return myModule; } public void setModelRoot(@Nullable ModelRoot modelRoot) { assertCanChange(); // if (myModelRoot != null && modelRoot != null) { // LOG.error("Duplicate model roots for model " + getLongName() + " in module " + modelRoot.getModule() + ": \n" + // "1. " + myModelRoot.getPresentation() + "\n" + // "2. " + modelRoot.getPresentation() // ); // } myModelRoot = modelRoot; } @Override @Nullable public ModelRoot getModelRoot() { assertCanRead(); return myModelRoot; } @Override public void addRootNode(@NotNull SNode node) { throw new UnsupportedOperationException(); } @Override public void removeRootNode(@NotNull SNode node) { throw new UnsupportedOperationException(); } @Override public boolean isReadOnly() { // assertCanRead(); no apparent reason why we shall demand read lock here. Few subclasses, that override the method, do not check access at all. return true; } public boolean isRegistered() { SModule copy = myModule; return copy != null && copy.getRepository() != null; } /** * Access actual node storage. Might trigger model load if model is not yet loaded. * XXX perhaps, this method shall live in SModelDescriptorStub? * @return node storage. Generally, shall not return <code>null</code> (FIXME revisit contract, enforce) */ public SModelData getModelData() { return getSModel(); } /** * Likely, shall return SModelData eventually * * @return actual model data or <code>null</code> if not initialized yet */ @Nullable protected abstract jetbrains.mps.smodel.SModel getCurrentModelInternal(); @NotNull @Override public Iterable<Problem> getProblems() { assertCanRead(); jetbrains.mps.smodel.SModel sModelInternal = getSModelInternal(); if (sModelInternal instanceof InvalidSModel) { return ((InvalidSModel) sModelInternal).getProblems(); } return Collections.emptySet(); } @Override public void load() { // perhaps, both load() and unload() shall be left to implementors? getModelData(); } /** * Dispose model data, change model's {@link #getLoadingState() loading state} and * dispatch {@link #fireModelStateChanged(ModelLoadingState, ModelLoadingState) state change event}. * Base implementation does nothing if there's no {@link #getCurrentModelInternal() initialized model data}. * Generally, subclasses shall override {@link #doUnload()} to perform actual cleanup of instance fields. */ @Override public void unload() { assertCanChange(); if (getCurrentModelInternal() == null) { return; } final ModelLoadingState oldState = getLoadingState(); doUnload(); fireModelStateChanged(oldState, getLoadingState()); } /** * Perform actual dispose of model data and {@link #setLoadingState(ModelLoadingState)} changes loading state}. * No loading state event is sent (responsibility of {@link #unload()}. Subclasses shall override to * clean instance fields and generally shall delegate to this implementation first to dispose model data. */ protected void doUnload() { jetbrains.mps.smodel.SModel modelData = getCurrentModelInternal(); if (modelData == null) { return; } modelData.setModelDescriptor(null); modelData.dispose(); setLoadingState(ModelLoadingState.NOT_LOADED); } @Override public boolean isLoaded() { return getLoadingState() == ModelLoadingState.FULLY_LOADED; } @Override public void addModelListener(SModelListener l) { myModelEventDispatch.addListener(l); } @Override public void removeModelListener(SModelListener l) { myModelEventDispatch.removeListener(l); } @Override public void addAccessListener(SModelAccessListener l) { myNodeEventDispatch.addAccessListener(l); } @Override public void removeAccessListener(SModelAccessListener l) { myNodeEventDispatch.removeAccessListener(l); } @Override public void addAccessListener(SNodeAccessListener l) { myNodeEventDispatch.addAccessListener(l); } @Override public void removeAccessListener(SNodeAccessListener l) { myNodeEventDispatch.removeAccessListener(l); } /** * This class doesn't dispatch change events, no listeners are tracked. */ @Override public void addChangeListener(SNodeChangeListener l) { // intentionally no-op } /** * This class doesn't dispatch change events, no listeners are tracked. */ @Override public void removeChangeListener(SNodeChangeListener l) { // intentionally no-op } protected final void fireBeforeModelRenamed(SModelReference newName) { SModule module = getModule(); if (module instanceof SModuleBase) { ((SModuleBase) module).fireBeforeModelRenamed(this, newName); } } protected final void fireModelRenamed(SModelReference oldName) { SModule module = getModule(); if (module instanceof SModuleBase) { ((SModuleBase) module).fireModelRenamed(this, oldName); } } /** * This method sends out proper notifications unless old and new state values are the same. * Note, it's not this method's responsibility to do actual change of the state, do it with {@link #setLoadingState(ModelLoadingState)} */ protected void fireModelStateChanged(ModelLoadingState oldState, ModelLoadingState newState) { if (oldState == newState) { return; } super.fireModelStateChanged(newState); if (newState == ModelLoadingState.NOT_LOADED) { myModelEventDispatch.modelUnloaded(this); } else { myModelEventDispatch.modelLoaded(this, newState == ModelLoadingState.INTERFACE_LOADED); } } @Override protected void fireModelSaved() { super.fireModelSaved(); myModelEventDispatch.modelSaved(this); } protected void fireConflictDetected() { myModelEventDispatch.conflictDetected(this); } protected void fireProblemsDetected(Iterable<Problem> problems) { myModelEventDispatch.problemsDetected(this, problems); } protected void fireModelReplaced() { myModelEventDispatch.modelReplaced(this); } @Override public void changeModelReference(SModelReference newModelReference) { super.changeModelReference(newModelReference); myModelReference = newModelReference; } /** * This method does nothing about model load state, it updates model descriptor of the models passed and dispatches a notification. * Seems reasonable to dispatch proper modelUnloaded/modelLoaded events in addition to modelReplaced as there are listeners that * expect either, not both. Especially, in case if load level is changed due to replacement (i.e. was FULL, became INTERFACE) * FIXME it's synchronized, do we still need that (with RegularModelDescriptor using distinct lock object) * XXX there are two uses in subclasses of not-so-nice EditableSModelBase (lazy and custom) that can't get replaced readily with * nice and convenient RegularModelDescriptor.replace() call. */ protected synchronized void replaceModelAndFireEvent(jetbrains.mps.smodel.SModel oldModel, jetbrains.mps.smodel.SModel newModel) { if (oldModel != null) { oldModel.setModelDescriptor(null); oldModel.dispose(); } if (newModel != null) { newModel.setModelDescriptor(this); } fireModelReplaced(); if (getRepository() instanceof MPSModuleRepository) { // for a model not yet visible to anyone, no reason to drop a cache // FIXME cache invalidation shall be a repository listener, and not done forcefully on model change // Besides, invalidateCaches() doesn't really care about model contents at all, it refreshes module scope which deals with modules only. ((MPSModuleRepository) getRepository()).invalidateCaches(); } } @Override protected void assertCanRead() { final SRepository repo = myRepository; if (repo != null) { repo.getModelAccess().checkReadAccess(); } } @Override protected void assertCanChange() { final SRepository repo = myRepository; if (repo != null) { repo.getModelAccess().checkWriteAccess(); } // if (!UndoHelper.getInstance().isInsideUndoableCommand()) { // throw new IllegalModelChangeError("registered model can only be modified inside undoable command"); // } } /** * Generally, shall be protected (as well as {@link #setLoadingState(ModelLoadingState)}, but made public for uses in * {@code PartialModelDataSupport} aka {@code jetbrains.mps.smodel.loading.UpdateableModel}. */ @NotNull public final ModelLoadingState getLoadingState() { return myModelLoadState; } public final void setLoadingState(@NotNull ModelLoadingState modelLoadState) { myModelLoadState = modelLoadState; } /** * CLIENTS SHALL NOT USE THIS METHOD. It's public merely to overcome java package boundaries (those of SModelData implementation and this class). * FIXME This is a hack. We shall pass myEventDispatch the moment internal model is initialized. * However, it's tricky to find out exact moment with present approach (getSModelInternal() either * returns existing or creates new), fireModeStateChanged is feasible option, but misguiding as well. * Refactoring required to split access to SModel internal from initialization. * To put event dispatch into smodel.SModel doesn't seem to be an option as we need to add listeners without * loading whole model. */ @NotNull public final ModelEventDispatch getNodeEventDispatch() { return myNodeEventDispatch; } }