blob: 8e840b640fd27cd55a07e46aa9ed1a5a73d0b0c1 [file] [log] [blame]
/**
* Copyright (c) 2015 Codetrails GmbH.
* 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
*/
package org.eclipse.epp.internal.logging.aeri.ide.server.mars;
import static com.google.common.base.Preconditions.*;
import static org.apache.commons.lang3.StringUtils.*;
import static org.eclipse.epp.internal.logging.aeri.ide.l10n.LogMessages.*;
import static org.eclipse.epp.logging.aeri.core.SendMode.NEVER;
import static org.eclipse.epp.logging.aeri.core.util.Logs.log;
import java.io.File;
import java.io.IOException;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.apache.commons.io.FileUtils;
import org.apache.http.HttpStatus;
import org.apache.lucene.analysis.KeywordAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Index;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.SearcherManager;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.IOUtils;
import org.apache.lucene.util.Version;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.epp.internal.logging.aeri.ide.l10n.LogMessages;
import org.eclipse.epp.internal.logging.aeri.ide.l10n.Messages;
import org.eclipse.epp.internal.logging.aeri.ide.utils.Formats;
import org.eclipse.epp.internal.logging.aeri.ide.utils.Zips;
import org.eclipse.epp.logging.aeri.core.ILink;
import org.eclipse.epp.logging.aeri.core.IModelFactory;
import org.eclipse.epp.logging.aeri.core.IProblemState;
import org.eclipse.epp.logging.aeri.core.ISystemSettings;
import org.eclipse.epp.logging.aeri.core.ProblemStatus;
import org.eclipse.epp.logging.aeri.core.ResetSendMode;
import org.eclipse.epp.logging.aeri.core.util.Links;
import org.eclipse.epp.logging.aeri.core.util.Logs;
import org.eclipse.epp.logging.aeri.core.util.Statuses;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.io.Files;
import com.google.common.util.concurrent.AbstractIdleService;
/**
* A history of remotely known problems that are worth knowing on the client side.
*/
public class ProblemsDatabaseProblemsHistory extends AbstractIdleService implements IProblemsHistory {
// values for problem databases
public static final String F_VERSION = "version"; //$NON-NLS-1$
public static final String VERSION = "0.6"; //$NON-NLS-1$
public static final String F_MESSAGE = "message"; //$NON-NLS-1$
public static final String F_ACTION = "action"; //$NON-NLS-1$
public static final String F_BUG_ID = "bugId"; //$NON-NLS-1$
public static final String F_BUG_URL = "bugUrl"; //$NON-NLS-1$
public static final String F_PROBLEM_URL = "problemUrl"; //$NON-NLS-1$
public static final String F_FINGERPRINT = "fingerprint"; //$NON-NLS-1$
public static final String F_NEEDINFOS = "needinfos"; //$NON-NLS-1$
private final File stateLocation;
private UpdateIndexJob updateIndexJob;
private Directory index;
private SearcherManager manager;
public ProblemsDatabaseProblemsHistory(File stateLocation) {
this.stateLocation = stateLocation;
}
@PostConstruct
private void e4PostConstruct() {
startAsync();
}
@Override
protected void startUp() throws Exception {
index = createIndexDirectory();
if (!IndexReader.indexExists(index)) {
createInitialIndex(index);
}
manager = new SearcherManager(index, null, null);
}
@VisibleForTesting
Directory createIndexDirectory() throws IOException {
stateLocation.mkdirs();
FSDirectory directory = FSDirectory.open(stateLocation);
return directory;
}
private void createInitialIndex(Directory directory) throws IOException {
IndexWriterConfig conf = new IndexWriterConfig(Version.LUCENE_35, new KeywordAnalyzer());
conf.setOpenMode(OpenMode.CREATE_OR_APPEND);
try (IndexWriter writer = new IndexWriter(directory, conf)) {
Document meta = new Document();
meta.add(new Field(F_VERSION, VERSION, Store.YES, Index.NO));
writer.addDocument(meta);
writer.commit();
}
}
@Override
public Optional<IProblemState> seen(IStatus status) {
checkNotNull(status);
checkState(isRunning());
String fingerprint = Statuses.traceIdentityHash(status);
return seen(new TermQuery(new Term(F_FINGERPRINT, fingerprint)));
}
private Optional<IProblemState> seen(Query q) {
IndexSearcher searcher = manager.acquire();
try {
TopDocs results = searcher.search(q, 1);
if (results.totalHits > 0) {
// HIT
int doc = results.scoreDocs[0].doc;
Document d = searcher.doc(doc);
IProblemState status = loadStatus(d);
return Optional.of(status);
}
} catch (Exception e) {
log(WARN_INDEX_NOT_AVAILABLE, e);
} finally {
try {
manager.release(searcher);
} catch (IOException e) {
log(WARN_INDEX_NOT_AVAILABLE, e);
}
}
return Optional.absent();
}
private IProblemState loadStatus(Document d) {
IProblemState state = IModelFactory.eINSTANCE.createProblemState();
String bugUrl = stripToNull(d.get(F_BUG_URL));
String bugId = stripToNull(d.get(F_BUG_ID));
ProblemStatus status = parseProblemStatus(d.get(F_ACTION));
String message = stripToNull(d.get(F_MESSAGE));
if (bugId != null) {
ILink bug = Links.createBugLink(bugUrl, Formats.format(Messages.LINK_TEXT_BUG, bugId));
state.getLinks().put("bug", bug); //$NON-NLS-1$
}
state.getNeedinfo().addAll(Arrays.asList(d.getValues(F_NEEDINFOS)));
state.setStatus(status);
state.setMessage(message);
return state;
}
private ProblemStatus parseProblemStatus(String string) {
switch (defaultString(string)) {
// It's not exactly clear which states a server knows - be conservative:
case "IGNORE": //$NON-NLS-1$
case "INVALID": //$NON-NLS-1$
return ProblemStatus.IGNORED;
case "NEEDINFO": //$NON-NLS-1$
return ProblemStatus.NEEDINFO;
case "FIXED": //$NON-NLS-1$
return ProblemStatus.FIXED;
case "WONTFIX": //$NON-NLS-1$
return ProblemStatus.WONTFIX;
case "NONE": //$NON-NLS-1$
return ProblemStatus.UNCONFIRMED;
case "": //$NON-NLS-1$
default:
Logs.log(LogMessages.DEBUG_UNKNOWN_SERVER_STATUS_IN_REMOTE_HISTORY, string);
return ProblemStatus.UNCONFIRMED;
}
}
public void replaceContent(File tempDir) throws IOException {
IndexWriterConfig conf = new IndexWriterConfig(Version.LUCENE_35, new KeywordAnalyzer());
conf.setOpenMode(OpenMode.CREATE_OR_APPEND);
try (IndexWriter writer = new IndexWriter(index, conf); FSDirectory newContent = FSDirectory.open(tempDir);) {
writer.deleteAll();
writer.addIndexes(newContent);
writer.commit();
indexChanged();
}
}
@VisibleForTesting
protected void indexChanged() throws IOException {
manager.maybeReopen();
}
@PreDestroy
private void e4PreDestroy() {
try {
stopAsync().awaitTerminated(2, TimeUnit.SECONDS);
} catch (Exception e) {
log(WARN_HISTORY_STOP_FAILED, e);
}
}
@Override
protected void shutDown() throws Exception {
IOUtils.close(index);
manager.close();
}
public static class UpdateIndexJob extends Job {
private IO io;
private ISystemSettings settings;
private ProblemsDatabaseProblemsHistory history;
public UpdateIndexJob(IO io, ISystemSettings settings, ProblemsDatabaseProblemsHistory history) {
super(Formats.format(Messages.JOB_NAME_UPDATE_INDEX, io.getConfiguration().getProblemsUrl()));
this.io = io;
this.settings = settings;
this.history = history;
}
@Override
protected IStatus run(IProgressMonitor monitor) {
SubMonitor progress = SubMonitor.convert(monitor, 3);
progress.beginTask(Messages.JOB_TASK_NAME_CHECKING_INDEX, 1000);
if (!io.isProblemsDatabaseOutdated()) {
return Status.OK_STATUS;
}
try {
progress.subTask(Messages.JOB_TASK_NAME_CHECKING_REMOTE_STATUS);
File tempRemoteIndexZip = File.createTempFile("problems-index", ".zip"); //$NON-NLS-1$ //$NON-NLS-2$
int downloadStatus = io.downloadDatabase(tempRemoteIndexZip, progress);
if (downloadStatus == HttpStatus.SC_NOT_MODIFIED) {
return Status.OK_STATUS;
} else if (downloadStatus != HttpStatus.SC_OK) {
// Could not access problems.zip for whatever reason; switch off error reporting until restart.
settings.setSendMode(NEVER);
settings.setResetSendMode(ResetSendMode.RESTART);
log(INFO_SERVER_NOT_AVAILABLE);
return Status.OK_STATUS;
}
progress.worked(1);
File tempDir = Files.createTempDir();
progress.subTask(Messages.JOB_TASK_NAME_REPLACING_LOCAL_DATABASE);
try {
Zips.unzip(tempRemoteIndexZip, tempDir);
history.replaceContent(tempDir);
io.getConfiguration().setProblemsZipLastDownloadTimestamp(System.currentTimeMillis());
io.saveConfiguration();
progress.worked(1);
} finally {
// cleanup files
tempRemoteIndexZip.delete();
FileUtils.deleteDirectory(tempDir);
progress.worked(1);
}
return Status.OK_STATUS;
} catch (CancellationException e) {
return Status.CANCEL_STATUS;
} catch (UnknownHostException e) {
Logs.debug("Failed to connect to server", e);
return Status.CANCEL_STATUS;
} catch (Exception e) {
log(WARN_INDEX_UPDATE_FAILED, io.getConfiguration().getProblemsUrl(), e);
return Status.OK_STATUS;
} finally {
monitor.done();
}
}
}
@Override
public void sync(IO io, ISystemSettings systemSettings) {
if (shouldUpdateProblemDatabase(io, systemSettings)) {
updateProblemDatabase(io, systemSettings);
}
}
private boolean shouldUpdateProblemDatabase(IO io, ISystemSettings systemSettings) {
return !isUpdating() && io.isProblemsDatabaseOutdated();
}
private boolean isUpdating() {
return updateIndexJob != null && updateIndexJob.getResult() == null;
}
@VisibleForTesting
protected void updateProblemDatabase(IO io, ISystemSettings systemSettings) {
updateIndexJob = new UpdateIndexJob(io, systemSettings, this);
updateIndexJob.schedule();
}
@Override
public void close() throws IOException {
}
}