/* * 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.persistence; import jetbrains.mps.extapi.model.EditableSModelBase; import jetbrains.mps.extapi.model.SModelBase; import jetbrains.mps.extapi.module.SModuleBase; import org.apache.log4j.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SModelId; import org.jetbrains.mps.openapi.module.SModule; import org.jetbrains.mps.openapi.module.SModuleListenerBase; import org.jetbrains.mps.openapi.module.SRepository; import org.jetbrains.mps.openapi.persistence.ModelRoot; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.TreeSet; import static jetbrains.mps.extapi.module.SModuleBase.MODEL_BY_NAME_COMPARATOR; /** * Base model root implementation which relies on module. Note that the model root might be not attached to module. * FIXME a module ought to be passed in constructor * * evgeny, 10/23/12 */ public abstract class ModelRootBase implements ModelRoot { private static final Logger LOG = Logger.getLogger(ModelRootBase.class); @Nullable private SModuleBase myModule; @Nullable private volatile SRepository myRepository; private final Set<SModel> myModels = new TreeSet<>(MODEL_BY_NAME_COMPARATOR); private final SyncModuleListener myModuleListener = new SyncModuleListener(); /*@NotNull*/ @Override public final SModule getModule() { return myModule; } public void setModule(@NotNull SModuleBase module) { if (myModule != null) { throw new IllegalStateException("Already attached to the module " + myModule); } checkNotRegistered(); myModule = module; } private void assertCanRead() { final SRepository repository = myRepository; if (repository != null) { repository.getModelAccess().checkReadAccess(); } } private void assertCanChange() { final SRepository repo = myRepository; if (repo != null) { repo.getModelAccess().checkWriteAccess(); } } @NotNull @Override public final List<SModel> getModels() { assertCanRead(); return Collections.unmodifiableList(new ArrayList<>(myModels)); } /** * returns all models under the model root * if some model is already loaded and registered, it is recommended to return the loaded one instead of loading another time * @return a sequence of models */ @NotNull public abstract Iterable<SModel> loadModels(); @Override public boolean canCreateModels() { SModule module = getModule(); return module != null && !module.isReadOnly(); } public void attach() { if (myModule == null) { throw new IllegalStateException("Module is null"); } myRepository = myModule.getRepository(); myModule.addModuleListener(myModuleListener); update(); } public void dispose() { if (myModule != null) { for (SModel model : getModels()) { myModule.unregisterModel((SModelBase) model); } } if (isRegistered()) { assert myModule != null; myModule.removeModuleListener(myModuleListener); } assert myModels.isEmpty(); myRepository = null; } void checkNotRegistered() { if (isRegistered()) { throw new IllegalStateException("cannot change properties of the registered root"); } } public final boolean isRegistered() { return myRepository != null; } /** * @param model is the model to be registered here as well as in the enclosing module. */ protected final void registerModel(@NotNull SModel model) { SModuleBase module = (SModuleBase) getModule(); assert module != null; assert module.getModel(model.getModelId()) == null; if (model instanceof SModelBase) { module.registerModel((SModelBase) model); ((SModelBase) model).setModelRoot(this); } myModels.add(model); } /** * note that the model will be removed from our models collection eventually * since we subscribed to our model removing events via {@link SyncModuleListener}. * * FIXME Faulty code is written here, we must not listen to the module events rather invoke this method right in the module class */ private void unregisterModel(@NotNull SModel model) { SModuleBase module = (SModuleBase) getModule(); assert module != null; assert module.getModel(model.getModelId()) != null; assert myModels.contains(model); if (model instanceof SModelBase) { ((SModelBase) model).setModelRoot(null); } if (model instanceof EditableSModelBase && ((EditableSModelBase) model).isChanged()) { ((EditableSModelBase) model).resolveDiskConflict(); } else { if (model instanceof SModelBase) { module.unregisterModel((SModelBase) model); } } } /** * IMPORTANT API METHOD * * Tricky logic which is forced onto all of subclasses. * This method represents a caching mechanism which does not reload the models which are already loaded * but looks only at the difference between what we had and what we get now * * Strangely enough this logic is not in API (not added to the API #loadModels implementation) so * the client of this class (and its subclasses) has to cast his <code>ModelRoot</code> to <code>ModelRootBase</code> * every time he wants to reload the models from their data sources. * * TODO the right thing */ public void update() { assertCanChange(); SModuleBase module = (SModuleBase) getModule(); assert module != null; Set<SModelId> loaded = new HashSet<>(); Iterable<SModel> allModels = loadModels(); for (SModel model : allModels) { SModel oldModel = module.getModel(model.getModelId()); if (oldModel == model) { //do nothing } else if (oldModel != null && oldModel.getModelRoot() != model.getModelRoot()) { LOG.warn("Trying to load model `" + model + "' which is already loaded by another model root"); } else if (loaded.contains(model.getModelId())) { LOG.warn("loadModels() returned model `" + model + "' twice"); } else { if (oldModel != null) { LOG.warn("loadModels() loaded model `" + model + "' which id is already present."); unregisterModel(oldModel); } registerModel(model); } loaded.add(model.getModelId()); } for (SModel model : getModels()) { if (!loaded.contains(model.getModelId())) { unregisterModel(model); } } } @Override public String toString() { return "(" + getType() + ") " + getPresentation(); } private final class SyncModuleListener extends SModuleListenerBase { @Override public void beforeModelRemoved(@NotNull SModule module, @NotNull SModel model) { assert myModule == module; myModels.remove(model); } } }