/*
* 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);
}
}
}