/**
* 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
*
* Contributors:
* Marcel Bruch - initial API and implementation.
*/
package org.eclipse.recommenders.internal.types.rcp;
import static org.apache.commons.io.FileUtils.deleteQuietly;
import static org.apache.lucene.document.Field.Index.NOT_ANALYZED;
import static org.apache.lucene.search.NumericRangeQuery.newLongRange;
import static org.eclipse.jdt.core.IJavaElement.PACKAGE_FRAGMENT_ROOT;
import static org.eclipse.recommenders.internal.types.rcp.l10n.LogMessages.ERROR_ACCESSING_SEARCHINDEX_FAILED;
import static org.eclipse.recommenders.jdt.JavaElementsFinder.*;
import static org.eclipse.recommenders.utils.Logs.log;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.List;
import java.util.Objects;
import java.util.Set;
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.document.NumericField;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
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.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.recommenders.internal.types.rcp.l10n.LogMessages;
import org.eclipse.recommenders.internal.types.rcp.l10n.Messages;
import org.eclipse.recommenders.jdt.JavaElementsFinder;
import org.eclipse.recommenders.utils.Logs;
import org.eclipse.recommenders.utils.names.ITypeName;
import org.eclipse.recommenders.utils.names.Names;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.AbstractFuture;
import com.google.common.util.concurrent.AbstractIdleService;
public class ProjectTypesIndex extends AbstractIdleService implements IProjectTypesIndex {
private static final String F_PACAKGE_FRAGEMENT_ROOT_TYPE = "pfrType"; //$NON-NLS-1$
private static final String F_NAME = "name"; //$NON-NLS-1$
private static final String F_LAST_MODIFIED = "lastModified"; //$NON-NLS-1$
private static final String F_LOCATION = "location"; //$NON-NLS-1$
private static final String F_INSTANCEOF = "instanceof"; //$NON-NLS-1$
private static final String V_JAVA_LANG_OBJECT = "java.lang.Object"; //$NON-NLS-1$
private static final String V_ARCHIVE = "archive"; //$NON-NLS-1$
private static final TermQuery TERM_QUERY_PACKAGE_FRAGMENT_ROOT_TYPE = new TermQuery(
new Term(F_PACAKGE_FRAGEMENT_ROOT_TYPE, V_ARCHIVE));
private final IJavaProject project;
private final File indexDir;
private Directory directory;
private IndexWriter writer;
private JobFuture activeRebuild = null;
private boolean rebuildAfterNextAccess;
private final File onlyIndexedJar;
private SearcherManager searchManager;
public ProjectTypesIndex(IJavaProject project, File indexDir) {
this(project, indexDir, null);
}
@VisibleForTesting
ProjectTypesIndex(IJavaProject project, File indexDir, File onlyIndexedJar) {
this.project = project;
this.indexDir = indexDir;
this.onlyIndexedJar = onlyIndexedJar;
if (onlyIndexedJar == null) {
startAsync();
}
}
@Override
protected void startUp() throws Exception {
initialize();
if (needsRebuild()) {
rebuild();
}
}
@VisibleForTesting
void initialize() throws IOException {
directory = FSDirectory.open(indexDir);
if (IndexWriter.isLocked(directory)) {
IndexWriter.unlock(directory);
}
IndexWriterConfig conf = new IndexWriterConfig(Version.LUCENE_35, new KeywordAnalyzer());
writer = new IndexWriter(directory, conf);
writer.commit();
searchManager = new SearcherManager(directory, null, null);
}
@VisibleForTesting
boolean needsRebuild() {
List<IPackageFragmentRoot> roots = findArchivePackageFragmentRoots();
StringBuilder sb = new StringBuilder();
try {
Set<File> indexedRoots = getIndexedRoots();
for (IPackageFragmentRoot root : roots) {
File location = JavaElementsFinder.findLocation(root).orNull();
if (location == null) {
continue;
}
if (!indexedRoots.remove(location)) {
// this root was unknown:
sb.append(" [+] ").append(location).append('\n'); //$NON-NLS-1$
} else if (!isCurrent(location)) {
// this root's timestamp is different to what we indexed before:
sb.append(" [*] ").append(location).append('\n'); //$NON-NLS-1$
}
}
if (!indexedRoots.isEmpty()) {
// there is a root that we did not index before:
for (File file : indexedRoots) {
sb.append(" [-] ").append(file.getAbsolutePath()).append('\n'); //$NON-NLS-1$
}
}
} catch (IOException e) {
Logs.log(LogMessages.ERROR_ACCESSING_SEARCHINDEX_FAILED, e);
}
if (sb.length() > 0) {
Logs.log(LogMessages.INFO_REINDEXING_REQUIRED, sb.toString());
return true;
}
return false;
}
private List<IPackageFragmentRoot> findArchivePackageFragmentRoots() {
Iterable<IPackageFragmentRoot> filtered = Iterables.filter(JavaElementsFinder.findPackageFragmentRoots(project),
new ArchiveFragmentRootsOnlyPredicate());
Iterable<IPackageFragmentRoot> result;
result = Iterables.filter(filtered, new Predicate<IPackageFragmentRoot>() {
@Override
public boolean apply(IPackageFragmentRoot input) {
return onlyIndexedJar == null || input.getPath().toFile().equals(onlyIndexedJar);
}
});
return Ordering.usingToString().sortedCopy(result);
}
private Set<File> getIndexedRoots() throws IOException {
Set<File> res = Sets.newHashSet();
IndexSearcher searcher = searchManager.acquire();
try {
TopDocs topDocs = searcher.search(TERM_QUERY_PACKAGE_FRAGMENT_ROOT_TYPE, Integer.MAX_VALUE);
for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
Document doc = searcher.doc(scoreDoc.doc);
File location = new File(doc.get(F_LOCATION));
res.add(location);
}
} finally {
releaseQuietly(searcher);
}
return res;
}
private boolean isCurrent(File rootLocation) throws IOException {
BooleanQuery query = new BooleanQuery();
query.add(new TermQuery(termLocation(rootLocation)), Occur.MUST);
query.add(newLongRange(F_LAST_MODIFIED, rootLocation.lastModified(), rootLocation.lastModified(), true, true),
Occur.MUST);
IndexSearcher searcher = searchManager.acquire();
try {
return searcher.search(query, 1).totalHits > 0;
} finally {
releaseQuietly(searcher);
}
}
private Term termLocation(File rootLocation) {
return new Term(F_LOCATION, rootLocation.getAbsolutePath());
}
@Override
public void close() throws IOException {
stopAsync();
awaitTerminated();
}
@Override
protected void shutDown() throws Exception {
cancelRebuild();
IOUtils.close(writer, directory);
searchManager.close();
}
@Override
public ImmutableSet<String> subtypes(ITypeName expected) {
if (!isRunning()) {
return ImmutableSet.of();
}
try {
return doSubtypes(expected);
} catch (Exception e) {
// temporary workaround for
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=464925
log(LogMessages.ERROR_ACCESSING_SEARCHINDEX_FAILED, e);
return ImmutableSet.of();
}
}
@VisibleForTesting
protected ImmutableSet<String> doSubtypes(ITypeName expected) {
if (expected == null) {
return ImmutableSet.of();
}
String type = Names.vm2srcQualifiedType(expected);
ImmutableSet.Builder<String> b = ImmutableSet.builder();
IndexSearcher searcher = searchManager.acquire();
try {
Query query = new TermQuery(new Term(F_INSTANCEOF, type));
TopDocs search = searcher.search(query, Integer.MAX_VALUE);
for (ScoreDoc sdoc : search.scoreDocs) {
Document doc = searcher.doc(sdoc.doc);
String name = doc.get(F_NAME);
b.add(name);
}
} catch (Exception e) {
log(LogMessages.ERROR_ACCESSING_SEARCHINDEX_FAILED, e);
} finally {
releaseQuietly(searcher);
}
// check whether the index was flagged as 'needs a rebuild':
if (isRebuildAfterNextAccess()) {
setRebuildAfterNextAccess(false);
rebuild();
}
return b.build();
}
private void releaseQuietly(IndexSearcher searcher) {
try {
searchManager.release(searcher);
} catch (IOException e) {
log(LogMessages.ERROR_ACCESSING_SEARCHINDEX_FAILED, e);
}
}
private void clear() {
try {
writer.deleteAll();
} catch (Exception e) {
log(ERROR_ACCESSING_SEARCHINDEX_FAILED, e);
}
}
private void rebuild() {
cancelRebuild();
final JobFuture res = new JobFuture();
activeRebuild = res;
Job job = new Job(MessageFormat.format(Messages.JOB_NAME_INDEXING, project.getElementName())) {
@Override
protected IStatus run(IProgressMonitor monitor) {
Thread thread = Thread.currentThread();
int priority = thread.getPriority();
try {
thread.setPriority(Thread.MIN_PRIORITY);
return doRun(monitor);
} finally {
thread.setPriority(priority);
}
}
private IStatus doRun(IProgressMonitor monitor) {
try {
clear();
rebuild(monitor);
commit();
} catch (OperationCanceledException e) {
res.setException(e);
res.setResult(Status.CANCEL_STATUS);
} catch (Exception e) {
res.setException(e);
res.setResult(new Status(IStatus.ERROR, Constants.BUNDLE_ID, e.getMessage(), e));
} finally {
monitor.done();
}
res.setResult(Status.OK_STATUS);
return Status.OK_STATUS;
}
};
res.setJob(job);
job.schedule(2000);
}
@VisibleForTesting
synchronized void rebuild(IProgressMonitor monitor) {
List<IPackageFragmentRoot> roots = findArchivePackageFragmentRoots();
SubMonitor progress = SubMonitor.convert(monitor, Messages.MONITOR_NAME_INDEXING, roots.size());
for (IPackageFragmentRoot root : roots) {
rebuildRoot(root, progress.newChild(1));
}
}
private void rebuildRoot(IPackageFragmentRoot root, SubMonitor monitor) {
ImmutableList<IType> types = findTypes(root);
SubMonitor progress = SubMonitor.convert(monitor, types.size());
progress.subTask(root.getElementName());
for (IType type : types) {
if (progress.isCanceled()) {
setRebuildAfterNextAccess(true);
throw new OperationCanceledException();
}
indexType(type, progress.newChild(1));
}
File location = JavaElementsFinder.findLocation(root).orNull();
if (location != null) {
registerArchivePackageFragmentRoot(location);
}
commit();
}
private void cancelRebuild() {
if (!(activeRebuild == null || activeRebuild.isDone() || activeRebuild.isCancelled())) {
activeRebuild.cancel(true);
}
}
private void registerArchivePackageFragmentRoot(File location) {
Document doc = new Document();
doc.add(new Field(F_PACAKGE_FRAGEMENT_ROOT_TYPE, V_ARCHIVE, Store.NO, Index.NOT_ANALYZED));
doc.add(new Field(F_LOCATION, location.getAbsolutePath(), Store.YES, Index.NOT_ANALYZED));
doc.add(new NumericField(F_LAST_MODIFIED, Store.YES, true).setLongValue(location.lastModified()));
try {
writer.addDocument(doc);
} catch (Exception e) {
Logs.log(ERROR_ACCESSING_SEARCHINDEX_FAILED, e);
}
}
private void commit() {
try {
writer.commit();
searchManager.maybeReopen();
} catch (Exception e) {
log(ERROR_ACCESSING_SEARCHINDEX_FAILED, e);
}
}
private void indexType(IType type, SubMonitor monitor) {
Document doc = new Document();
// name:
doc.add(new Field(F_NAME, type.getFullyQualifiedName(), Store.YES, NOT_ANALYZED));
// location:
File location = findPackageFragmentRoot(type).orNull();
if (location != null) {
doc.add(new Field(F_LOCATION, location.getAbsolutePath(), Store.NO, Index.NOT_ANALYZED));
}
// extends:
doc.add(new Field(F_INSTANCEOF, type.getFullyQualifiedName(), Store.NO, NOT_ANALYZED));
try {
ITypeHierarchy h = type.newSupertypeHierarchy(null);
for (IType supertypes : h.getAllSupertypes(type)) {
String fullyQualifiedName = supertypes.getFullyQualifiedName();
if (Objects.equals(V_JAVA_LANG_OBJECT, fullyQualifiedName)) {
continue;
}
doc.add(new Field(F_INSTANCEOF, fullyQualifiedName, Store.NO, NOT_ANALYZED));
}
} catch (Exception e) {
log(LogMessages.ERROR_ACCESSING_TYPE_HIERARCHY, e, type);
}
addDocument(doc, monitor);
}
private void addDocument(Document doc, IProgressMonitor monitor) {
SubMonitor progress = SubMonitor.convert(monitor, 1);
try {
if (!monitor.isCanceled()) {
writer.addDocument(doc);
}
} catch (Exception e) {
log(ERROR_ACCESSING_SEARCHINDEX_FAILED, e);
} finally {
progress.worked(1);
}
}
private Optional<File> findPackageFragmentRoot(IType type) {
IPackageFragmentRoot ancestor = (IPackageFragmentRoot) type.getAncestor(PACKAGE_FRAGMENT_ROOT);
return findLocation(ancestor);
}
@Override
public void suggestRebuild() {
setRebuildAfterNextAccess(needsRebuild());
}
private void setRebuildAfterNextAccess(boolean value) {
rebuildAfterNextAccess = value;
}
private boolean isRebuildAfterNextAccess() {
return rebuildAfterNextAccess;
}
@Override
public void delete() {
stopAsync();
awaitTerminated();
deleteQuietly(indexDir);
}
private static final class ArchiveFragmentRootsOnlyPredicate implements Predicate<IPackageFragmentRoot> {
@Override
public boolean apply(IPackageFragmentRoot input) {
if (input == null) {
return false;
}
if (!input.isArchive()) {
return false;
}
File location = JavaElementsFinder.findLocation(input).orNull();
if (location == null) {
return false;
}
return true;
}
}
private static final class JobFuture extends AbstractFuture<IStatus> {
private Job job;
public void setJob(Job job) {
this.job = job;
}
public boolean setResult(IStatus value) {
return super.set(value);
}
@Override
public boolean setException(Throwable throwable) {
return super.setException(throwable);
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
if (job != null) {
return job.cancel();
}
return false;
}
}
}