/**
* Copyright (c) 2010, 2013 Darmstadt University of Technology.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Marcel Bruch - initial API and implementation.
* Olav Lenz - multi index support.
*/
package org.eclipse.recommenders.internal.models.rcp;
import static com.google.common.base.Optional.*;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.eclipse.recommenders.internal.models.rcp.ModelsRcpModule.INDEX_BASEDIR;
import static org.eclipse.recommenders.internal.models.rcp.l10n.LogMessages.*;
import static org.eclipse.recommenders.models.ModelCoordinate.HINT_REPOSITORY_URL;
import static org.eclipse.recommenders.utils.Logs.log;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.io.FileUtils;
import org.eclipse.recommenders.coordinates.ProjectCoordinate;
import org.eclipse.recommenders.internal.models.rcp.l10n.LogMessages;
import org.eclipse.recommenders.models.IModelIndex;
import org.eclipse.recommenders.models.IModelRepository;
import org.eclipse.recommenders.models.ModelCoordinate;
import org.eclipse.recommenders.models.ModelIndex;
import org.eclipse.recommenders.models.rcp.ModelEvents.ModelArchiveDownloadedEvent;
import org.eclipse.recommenders.models.rcp.ModelEvents.ModelIndexOpenedEvent;
import org.eclipse.recommenders.models.rcp.ModelEvents.ModelRepositoryClosedEvent;
import org.eclipse.recommenders.models.rcp.ModelEvents.ModelRepositoryOpenedEvent;
import org.eclipse.recommenders.rcp.IRcpService;
import org.eclipse.recommenders.utils.Checks;
import org.eclipse.recommenders.utils.Logs;
import org.eclipse.recommenders.utils.Pair;
import org.eclipse.recommenders.utils.Uris;
import org.eclipse.recommenders.utils.Zips;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.google.common.io.Files;
import com.google.common.util.concurrent.AbstractIdleService;
/**
* The Eclipse RCP wrapper around an IModelIndex that responds to (@link ModelRepositoryChangedEvent)s by closing the
* underlying, downloading the new index if required and reopening the index.
*/
public class EclipseModelIndex extends AbstractIdleService implements IModelIndex, IRcpService {
private static final int CACHE_SIZE = 10;
private final File basedir;
private final ModelsRcpPreferences prefs;
private final IModelRepository repository;
private final EventBus bus;
/*
* Contains only open indices: An IModelIndex will only be added after it is opened and removed before it is closed.
* For all read access openDelegates provides a consistent version.
*/
private volatile ImmutableMap<String, Pair<File, IModelIndex>> openDelegates;
private final Cache<Pair<ProjectCoordinate, String>, Optional<ModelCoordinate>> cache = CacheBuilder.newBuilder()
.maximumSize(CACHE_SIZE).concurrencyLevel(1).build();
@Inject
public EclipseModelIndex(@Named(INDEX_BASEDIR) File basedir, ModelsRcpPreferences prefs,
IModelRepository repository, EventBus bus) {
this.basedir = basedir;
this.prefs = prefs;
this.repository = repository;
this.bus = bus;
}
@PreDestroy
@Override
public void close() throws IOException {
stopAsync();
try {
awaitTerminated(5, SECONDS);
} catch (TimeoutException e) {
log(ERROR_CLOSING_MODEL_INDEX_SERVICE, e);
}
}
@Override
protected void shutDown() throws Exception {
cache.invalidateAll();
for (Pair<File, IModelIndex> delegate : openDelegates.values()) {
removeDelegate(delegate);
delegate.getSecond().close();
}
}
@PostConstruct
@Override
public void open() throws IOException {
startAsync();
}
@Override
protected void startUp() throws Exception {
Checks.ensureNoDuplicates(prefs.remotes);
clearDelegates();
basedir.mkdir();
for (String remoteUri : prefs.remotes) {
File file = createIndexLocation(remoteUri);
if (indexAlreadyDownloaded(file)) {
openDelegate(remoteUri, file);
}
triggerIndexDownload(remoteUri);
}
}
@VisibleForTesting
public void openForTesting() throws IOException {
Checks.ensureNoDuplicates(prefs.remotes);
clearDelegates();
for (String remoteUri : prefs.remotes) {
File file = createIndexLocation(remoteUri);
openDelegate(remoteUri, file);
}
}
private synchronized void storeDelegate(String remoteUrl, Pair<File, IModelIndex> pair) {
openDelegates = new ImmutableMap.Builder<String, Pair<File, IModelIndex>>().putAll(openDelegates)
.put(remoteUrl, pair).build();
}
private synchronized void removeDelegate(Pair<File, IModelIndex> delegate) {
HashMap<String, Pair<File, IModelIndex>> delegates = Maps.newHashMap(openDelegates);
delegates.values().remove(delegate);
openDelegates = new ImmutableMap.Builder<String, Pair<File, IModelIndex>>().putAll(delegates).build();
}
private synchronized void clearDelegates() {
openDelegates = ImmutableMap.of();
}
private void openDelegate(String remoteUrl, File indexLocation) throws IOException {
IModelIndex modelIndex = createModelIndex(indexLocation);
modelIndex.open();
storeDelegate(remoteUrl, Pair.newPair(indexLocation, modelIndex));
bus.post(new ModelIndexOpenedEvent());
}
private File createIndexLocation(String remoteUri) {
return new File(basedir, Uris.mangle(Uris.toUri(remoteUri)));
}
@VisibleForTesting
protected IModelIndex createModelIndex(File indexLocation) {
return new ModelIndex(indexLocation);
}
private void triggerIndexDownload(String remoteUrl) {
ModelCoordinate mc = createIndexCoordinateWithRemoteUrlHint(remoteUrl);
new DownloadModelArchiveJob(repository, mc, true, bus).schedule(300);
}
private ModelCoordinate createIndexCoordinateWithRemoteUrlHint(String remoteUrl) {
ModelCoordinate mc = new ModelCoordinate(INDEX.getGroupId(), INDEX.getArtifactId(), INDEX.getClassifier(),
INDEX.getExtension(), INDEX.getVersion());
return createCopyWithRepositoryUrlHint(mc, remoteUrl);
}
private boolean indexAlreadyDownloaded(File location) {
if (!location.exists()) {
return false;
}
File[] files = location.listFiles();
if (files == null) {
return false;
}
if (files.length <= 1) {
// If this folder contains an index, there must be more than one file...
// TODO However, on Mac OS, we often have hidden files in the folder. This is just simple heuristic.
return false;
}
return true;
}
/**
* This implementation caches the previous results
*/
@Override
public Optional<ModelCoordinate> suggest(final ProjectCoordinate pc, final String modelType) {
if (!isRunning()) {
log(INFO_SERVICE_NOT_RUNNING);
return absent();
}
Pair<ProjectCoordinate, String> key = Pair.newPair(pc, modelType);
try {
return cache.get(key, new Callable<Optional<ModelCoordinate>>() {
@Override
public Optional<ModelCoordinate> call() {
for (String remote : prefs.remotes) {
Pair<File, IModelIndex> pair = openDelegates.get(remote);
if (pair == null) {
continue; // Index not (yet) available; try next remote repository
}
IModelIndex index = pair.getSecond();
Optional<ModelCoordinate> suggest = index.suggest(pc, modelType);
if (suggest.isPresent()) {
return of(createCopyWithRepositoryUrlHint(suggest.get(), remote));
}
}
return absent();
}
});
} catch (ExecutionException e) {
Logs.log(LogMessages.ERROR_FAILED_TO_ACCESS_MODEL_COORDINATES_CACHE, e);
return absent();
}
}
@Override
public ImmutableSet<ModelCoordinate> suggestCandidates(ProjectCoordinate pc, String modelType) {
if (!isRunning()) {
log(INFO_SERVICE_NOT_RUNNING);
return ImmutableSet.of();
}
Set<ModelCoordinate> candidates = Sets.newHashSet();
for (Entry<String, Pair<File, IModelIndex>> entry : openDelegates.entrySet()) {
IModelIndex index = entry.getValue().getSecond();
candidates.addAll(addRepositoryUrlHint(index.suggestCandidates(pc, modelType), entry.getKey()));
}
return ImmutableSet.copyOf(candidates);
}
@Override
public ImmutableSet<ModelCoordinate> getKnownModels(String modelType) {
if (!isRunning()) {
log(INFO_SERVICE_NOT_RUNNING);
return ImmutableSet.of();
}
Set<ModelCoordinate> models = Sets.newHashSet();
for (Entry<String, Pair<File, IModelIndex>> entry : openDelegates.entrySet()) {
IModelIndex index = entry.getValue().getSecond();
models.addAll(addRepositoryUrlHint(index.getKnownModels(modelType), entry.getKey()));
}
return ImmutableSet.copyOf(models);
}
@Override
public Optional<ProjectCoordinate> suggestProjectCoordinateByArtifactId(String artifactId) {
if (!isRunning()) {
log(INFO_SERVICE_NOT_RUNNING);
return absent();
}
for (Pair<File, IModelIndex> delegate : openDelegates.values()) {
IModelIndex index = delegate.getSecond();
Optional<ProjectCoordinate> suggest = index.suggestProjectCoordinateByArtifactId(artifactId);
if (suggest.isPresent()) {
return suggest;
}
}
return absent();
}
@Override
public Optional<ProjectCoordinate> suggestProjectCoordinateByFingerprint(String fingerprint) {
if (!isRunning()) {
log(INFO_SERVICE_NOT_RUNNING);
return absent();
}
for (Pair<File, IModelIndex> delegate : openDelegates.values()) {
IModelIndex index = delegate.getSecond();
Optional<ProjectCoordinate> suggest = index.suggestProjectCoordinateByFingerprint(fingerprint);
if (suggest.isPresent()) {
return suggest;
}
}
return absent();
}
@Subscribe
public void onEvent(ModelRepositoryOpenedEvent e) throws Exception {
if (!isRunning()) {
// Log this to see whether my expectations are correct
log(INFO_SERVICE_NOT_RUNNING);
}
startUp();
}
@Subscribe
public void onEvent(ModelIndexOpenedEvent e) {
// We don't check whether the service is running here, because this event is fired during opening.
// When the model index is finally opened, invalidate the cache, as we may have cached a "not found" for
// something that can be found in the newly opened index.
cache.invalidateAll();
}
@Subscribe
public void onEvent(ModelRepositoryClosedEvent e) throws Exception {
if (!isRunning()) {
// Log this to see whether my expectations are correct
log(INFO_SERVICE_NOT_RUNNING);
}
// XXX: this is closing the repo but not setting the service state to stopped.
shutDown();
}
@Subscribe
public void onEvent(ModelArchiveDownloadedEvent e) throws IOException {
if (!isRunning()) {
log(INFO_SERVICE_NOT_RUNNING);
return;
}
if (isIndex(e.model)) {
File location = repository.getLocation(e.model, false).orNull();
String remoteUri = e.model.getHint(HINT_REPOSITORY_URL).orNull();
if (remoteUri != null) {
Pair<File, IModelIndex> pair = openDelegates.get(remoteUri);
if (pair == null) {
File folder = createIndexLocation(remoteUri);
folder.mkdir();
Zips.unzip(location, folder);
openDelegate(remoteUri, folder);
} else {
File folder = Files.createTempDir();
Zips.unzip(location, folder);
IModelIndex modelIndex = pair.getSecond();
modelIndex.updateIndex(folder);
bus.post(new ModelIndexOpenedEvent());
FileUtils.deleteDirectory(folder);
}
}
}
}
private boolean isIndex(ModelCoordinate model) {
return model.getGroupId().equals(INDEX.getGroupId()) && model.getArtifactId().equals(INDEX.getArtifactId())
&& model.getExtension().equals(INDEX.getExtension()) && model.getVersion().equals(INDEX.getVersion());
}
@Override
public void updateIndex(File index) throws IOException {
throw new UnsupportedOperationException();
}
public static Set<ModelCoordinate> addRepositoryUrlHint(Set<ModelCoordinate> modelCoordinates, String url) {
Set<ModelCoordinate> modelCoordinatesWithUrlHint = Sets.newHashSet();
for (ModelCoordinate modelCoordinate : modelCoordinates) {
modelCoordinatesWithUrlHint.add(createCopyWithRepositoryUrlHint(modelCoordinate, url));
}
return modelCoordinatesWithUrlHint;
}
private static ModelCoordinate createCopyWithRepositoryUrlHint(ModelCoordinate mc, String url) {
Map<String, String> hints = Maps.newHashMap(mc.getHints());
hints.put(ModelCoordinate.HINT_REPOSITORY_URL, url);
ModelCoordinate copy = new ModelCoordinate(mc.getGroupId(), mc.getArtifactId(), mc.getClassifier(),
mc.getExtension(), mc.getVersion(), hints);
return copy;
}
}