/*
* Copyright 2003-2014 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.classloading;
import jetbrains.mps.classloading.GraphHolder.Graph;
import jetbrains.mps.module.ReloadableModule;
import jetbrains.mps.project.dependency.UsedModulesCollector;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.util.Condition;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class ModuleUpdater {
private static final Logger LOG = LogManager.getLogger(ModuleUpdater.class);
private static final Object LOCK = new Object();
private volatile boolean myChangedFlag = false;
private final Set<ReloadableModule> myModulesToAdd = new LinkedHashSet<ReloadableModule>();
private final Set<ReloadableModule> myModulesToReload = new LinkedHashSet<ReloadableModule>();
private final Set<SModuleReference> myModulesToRemove = new LinkedHashSet<SModuleReference>();
private final Condition<ReloadableModule> myWatchableCondition;
private final GraphHolder<SModuleReference> myDepGraph = new GraphHolder<SModuleReference>();
private final ReferenceStorage<ReloadableModule> myRefStorage;
private final SRepository myRepository;
private final Map<ReloadableModule, List<SearchError>> myModulesWithAbsentDeps = new HashMap<>();
public ModuleUpdater(SRepository repository, Condition<ReloadableModule> watchableCondition, ReferenceStorage<ReloadableModule> refStorage) {
myRepository = repository;
myWatchableCondition = watchableCondition;
myRefStorage = refStorage;
}
public void updateModules(@NotNull Collection<? extends ReloadableModule> modules) {
synchronized (LOCK) {
myChangedFlag = true;
for (ReloadableModule module : modules) {
if (myWatchableCondition.met(module)) {
myModulesToReload.add(module);
}
// need this call because we might get #addModules notification later than this one
myRefStorage.moduleAdded(module);
}
}
}
public void addModules(@NotNull Collection<? extends ReloadableModule> modules) {
synchronized (LOCK) {
myChangedFlag = true;
for (ReloadableModule module : modules) {
if (myWatchableCondition.met(module)) {
myModulesToAdd.add(module);
myModulesToRemove.add(module.getModuleReference());
}
myRefStorage.moduleAdded(module);
}
}
}
public void removeModules(@NotNull Collection<? extends SModuleReference> mRefs) {
synchronized (LOCK) {
for (SModuleReference mRef : mRefs) {
if (myRefStorage.moduleRemoved(mRef) != null) {
// need to clean up myModulesToLoad and myModulesToReload
removeMRefFromModules(mRef, myModulesToAdd);
removeMRefFromModules(mRef, myModulesToReload);
myModulesToRemove.add(mRef);
myChangedFlag = true;
}
}
}
}
public Collection<SModuleReference> getModules() {
synchronized (LOCK) {
return myDepGraph.getVertices();
}
}
private void removeMRefFromModules(SModuleReference mRef, Collection<ReloadableModule> modules) {
for (Iterator<ReloadableModule> iterator = modules.iterator(); iterator.hasNext();) {
ReloadableModule module = iterator.next();
SModuleReference ref = module.getModuleReference();
if (mRef.equals(ref)) iterator.remove();
}
}
/**
* @return if graph did change (some edges or vertices added/removed)
*/
public boolean refreshGraph() {
myRepository.getModelAccess().checkReadAccess();
synchronized (LOCK) {
final long beginTime = System.nanoTime();
LOG.debug(String.format("Refreshing classloading graph adding: %d, removing %d, updating %d", myModulesToAdd.size(),
myModulesToRemove.size(), myModulesToReload.size()));
try {
myChangedFlag = false;
UsedModulesCollector usedModulesCollector = new UsedModulesCollector();
myDepGraph.checkGraphsCorrectness();
int wasEdges = myDepGraph.getEdgesCount();
int wasVertices = myDepGraph.getVerticesCount();
myModulesWithAbsentDeps.clear();
boolean updated = !myModulesToAdd.isEmpty() || !myModulesToRemove.isEmpty();
updateRemoved(myModulesToRemove);
updateAdded(myModulesToAdd, usedModulesCollector);
updated |= updateReloaded(myModulesToReload, usedModulesCollector);
myModulesToRemove.clear();
myModulesToAdd.clear();
myModulesToReload.clear();
LOG.debug("Difference in the vertex count after validation " + (myDepGraph.getVerticesCount() - wasVertices));
LOG.debug("Difference in the edge count after validation " + (myDepGraph.getEdgesCount() - wasEdges));
return updated;
} finally {
LOG.info(String.format("Classloading refresh took %.3f s", (System.nanoTime() - beginTime) / 1e9));
}
}
}
public Map<ReloadableModule, List<SearchError>> getModulesWithAbsentDeps() {
return Collections.unmodifiableMap(myModulesWithAbsentDeps);
}
private void updateRemoved(Set<? extends SModuleReference> modulesToRemove) {
for (SModuleReference mRef : modulesToRemove) {
if (!myDepGraph.contains(mRef)) continue;
LOG.debug("Removing module " + mRef);
myDepGraph.remove(mRef);
}
}
private void updateAdded(final Set<? extends ReloadableModule> modulesToAdd, UsedModulesCollector usedModulesCollector) {
updateAddedVertices(modulesToAdd);
updateAllEdges(usedModulesCollector);
}
/**
* @return true if actual update happened
*/
private boolean updateReloaded(final Set<? extends ReloadableModule> modulesToReload, UsedModulesCollector usedModulesCollector) {
if (modulesToReload.isEmpty()) {
return false;
}
boolean updated = updateReloadedVertices(modulesToReload);
updated |= updateReloadedEdges(modulesToReload, usedModulesCollector);
return updated;
}
private void updateAddedVertices(Set<? extends ReloadableModule> modulesToAdd) {
for (ReloadableModule module : modulesToAdd) {
LOG.debug("Adding module " + module);
assert myWatchableCondition.met(module);
assert module.getRepository() != null;
myDepGraph.add(module.getModuleReference());
}
}
/**
* Here we are updating references from all the existing modules
* Also we are going through all the modules in the repository and checking that their dependencies do exist.
* It checks every module in the current graph and tracks whether it has some unresolved dependencies.
* If so it puts it to the map {@link #myModulesWithAbsentDeps}.
*/
private void updateAllEdges(UsedModulesCollector usedModulesCollector) {
myRepository.getModelAccess().checkReadAccess();
Collection<? extends SModuleReference> allRefs = myDepGraph.getVertices();
for (SModuleReference ref : allRefs) {
ReloadableModule module = myRefStorage.resolveRef(ref);
assert module != null;
Collection<? extends ReloadableModule> deps;
DepsWithErrors depsWithErrors = getDepsWithErrors(module, usedModulesCollector);
deps = depsWithErrors.deps;
if (!depsWithErrors.errors.isEmpty()) {
myModulesWithAbsentDeps.put(module, depsWithErrors.errors);
continue;
}
for (ReloadableModule dep : deps) {
if (allRefs.contains(dep.getModuleReference())) {
myDepGraph.addEdge(ref, dep.getModuleReference());
} else {
// valid if somebody calls reloadModule in moduleAdded() listener before us
LOG.warn("The dependent module " + dep + " of the " + module + " is not registered");
}
}
}
}
private boolean updateReloadedVertices(Set<? extends ReloadableModule> modulesToReload) {
boolean updated = false;
for (ReloadableModule module : modulesToReload) {
LOG.debug("Reloading module " + module);
assert myWatchableCondition.met(module);
assert module.getRepository() != null;
SModuleReference mRef = module.getModuleReference();
if (!myDepGraph.contains(mRef)) {
myDepGraph.add(mRef);
updated = true;
}
}
return updated;
}
/**
* calculates difference in the outgoing edges for each given module
*/
private boolean updateReloadedEdges(Set<? extends ReloadableModule> modulesToReload, UsedModulesCollector usedModulesCollector) {
boolean updated = false;
myRepository.getModelAccess().checkReadAccess();
Collection<? extends SModuleReference> allRefs = myDepGraph.getVertices();
for (ReloadableModule module : modulesToReload) {
SModuleReference mRef = module.getModuleReference();
Collection<? extends SModuleReference> currentDeps = new HashSet<SModuleReference>(myDepGraph.getOutgoingEdges(mRef));
DepsWithErrors depsWithErrors = getDepsWithErrors(module, usedModulesCollector);
if (!depsWithErrors.errors.isEmpty()) {
assert myModulesWithAbsentDeps.containsKey(module);
return true;
}
Collection<? extends ReloadableModule> newModuleDeps = depsWithErrors.deps;
for (ReloadableModule moduleDep : newModuleDeps) {
SModuleReference depRef = moduleDep.getModuleReference();
if (!currentDeps.contains(depRef)) {
if (allRefs.contains(depRef)) {
myDepGraph.addEdge(mRef, depRef);
updated = true;
}
} else {
currentDeps.remove(depRef);
}
}
for (SModuleReference curDep : currentDeps) {
myDepGraph.removeEdge(mRef, curDep);
updated = true;
}
}
return updated;
}
@NotNull
private DepsWithErrors getDepsWithErrors(@NotNull ReloadableModule module, UsedModulesCollector usedModulesCollector) {
myRepository.getModelAccess().checkReadAccess();
if (module.getRepository() == null) {
return DepsWithErrors.EMPTY;
}
ErrorContainer errorContainer = new ErrorContainer();
Collection<SModule> directlyUsedModules = usedModulesCollector.directlyUsedModules(module, errorContainer, true, true);
Set<ReloadableModule> deps = new LinkedHashSet<>();
for (SModule dep : directlyUsedModules) {
if (dep instanceof ReloadableModule) {
ReloadableModule reloadableModule = (ReloadableModule) dep;
if (myWatchableCondition.met(reloadableModule)) {
deps.add(reloadableModule);
}
}
}
List<SearchError> errors = new ArrayList<>(errorContainer.getErrors());
return new DepsWithErrors(deps, errors);
}
public Collection<SModuleReference> getDeps(Iterable<? extends SModuleReference> mRefs) {
synchronized (LOCK) {
final Collection<SModuleReference> result = new ArrayList<SModuleReference>();
Graph<SModuleReference> depGraph = myDepGraph.getGraph();
depGraph.dfs(mRefs, result::add);
return Collections.unmodifiableCollection(result);
}
}
public Collection<SModuleReference> getBackDeps(Iterable<? extends SModuleReference> mRefs) {
synchronized (LOCK) {
final Collection<SModuleReference> result = new LinkedHashSet<SModuleReference>();
Graph<SModuleReference> backDepGraph = myDepGraph.getConjugateGraph();
backDepGraph.dfs(mRefs, result::add);
return Collections.unmodifiableCollection(result);
}
}
public boolean isDirty() {
return myChangedFlag;
}
public boolean contains(SModuleReference mRef) {
synchronized (LOCK) {
return myDepGraph.contains(mRef);
}
}
private final static class DepsWithErrors {
public final Collection<ReloadableModule> deps;
public final List<SearchError> errors;
private DepsWithErrors(@NotNull Collection<ReloadableModule> deps, @NotNull List<SearchError> errors) {
this.deps = deps;
this.errors = errors;
}
public final static DepsWithErrors EMPTY = new DepsWithErrors(Collections.emptySet(), Collections.emptyList());
}
static class SearchError {
private final String myMsg;
SearchError(String msg) {
myMsg = msg;
}
@NotNull
public String getMsg() {
return myMsg;
}
public static SearchError of(@NotNull String msg) {
return new SearchError(msg);
}
@Override
public String toString() {
return "SearchError " + myMsg;
}
}
}