/******************************************************************************* * Copyright (c) 2012-2015 Codenvy, S.A. * 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: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.api.vfs.server.search; import org.eclipse.che.api.core.ForbiddenException; import org.eclipse.che.api.core.ServerException; import org.eclipse.che.api.vfs.server.LazyIterator; import org.eclipse.che.api.vfs.server.MountPoint; import org.eclipse.che.api.vfs.server.VirtualFile; import org.eclipse.che.api.vfs.server.VirtualFileFilter; import org.eclipse.che.api.vfs.server.util.MediaTypeFilter; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.core.SimpleAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.StringField; import org.apache.lucene.document.TextField; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.Term; import org.apache.lucene.queryparser.classic.ParseException; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.PrefixQuery; import org.apache.lucene.search.SearcherFactory; import org.apache.lucene.search.SearcherManager; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.WildcardQuery; import org.apache.lucene.store.Directory; import org.apache.lucene.util.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.util.LinkedList; import java.util.Set; /** * Lucene based searcher. * * @author andrew00x */ public abstract class LuceneSearcher implements Searcher { private static final Logger LOG = LoggerFactory.getLogger(LuceneSearcher.class); private static final int RESULT_LIMIT = 1000; private final VirtualFileFilter filter; private IndexWriter luceneIndexWriter; private SearcherManager searcherManager; private boolean closed; public LuceneSearcher(Set<String> indexedMediaTypes) { this(new MediaTypeFilter(indexedMediaTypes)); } public LuceneSearcher(VirtualFileFilter filter) { this.filter = filter; } protected Analyzer makeAnalyzer() { return new SimpleAnalyzer(); } protected abstract Directory makeDirectory() throws ServerException; /** * Init lucene index. Need call this method if index directory is clean. Scan all files in virtual filesystem and add to index. * * @param mountPoint * MountPoint * @throws ServerException * if any virtual filesystem error */ public void init(MountPoint mountPoint) throws ServerException { doInit(); addTree(mountPoint.getRoot()); } protected final synchronized void doInit() throws ServerException { try { luceneIndexWriter = new IndexWriter(makeDirectory(), new IndexWriterConfig(makeAnalyzer())); searcherManager = new SearcherManager(luceneIndexWriter, true, new SearcherFactory()); } catch (IOException e) { throw new ServerException(e); } } public synchronized void close() { if (!closed) { try { IOUtils.close(getIndexWriter(), getIndexWriter().getDirectory(), searcherManager); } catch (IOException e) { LOG.error(e.getMessage(), e); } closed = true; } } public synchronized IndexWriter getIndexWriter() { return luceneIndexWriter; } @Override public String[] search(QueryExpression query) throws ServerException { final BooleanQuery luceneQuery = new BooleanQuery(); final String name = query.getName(); final String path = query.getPath(); final String mediaType = query.getMediaType(); final String text = query.getText(); if (path != null) { luceneQuery.add(new PrefixQuery(new Term("path", path)), BooleanClause.Occur.MUST); } if (name != null) { luceneQuery.add(new WildcardQuery(new Term("name", name)), BooleanClause.Occur.MUST); } if (mediaType != null) { luceneQuery.add(new TermQuery(new Term("mediatype", mediaType)), BooleanClause.Occur.MUST); } if (text != null) { QueryParser qParser = new QueryParser("text", makeAnalyzer()); try { luceneQuery.add(qParser.parse(text), BooleanClause.Occur.MUST); } catch (ParseException e) { throw new ServerException(e.getMessage()); } } IndexSearcher luceneSearcher = null; try { searcherManager.maybeRefresh(); luceneSearcher = searcherManager.acquire(); final TopDocs topDocs = luceneSearcher.search(luceneQuery, RESULT_LIMIT); if (topDocs.totalHits > RESULT_LIMIT) { throw new ServerException(String.format("Too many (%d) matched results found. ", topDocs.totalHits)); } final String[] result = new String[topDocs.scoreDocs.length]; for (int i = 0, length = result.length; i < length; i++) { result[i] = luceneSearcher.doc(topDocs.scoreDocs[i].doc).getField("path").stringValue(); } return result; } catch (IOException e) { throw new ServerException(e.getMessage(), e); } finally { try { searcherManager.release(luceneSearcher); } catch (IOException e) { LOG.error(e.getMessage()); } } } @Override public final void add(VirtualFile virtualFile) throws ServerException { doAdd(virtualFile); } protected void doAdd(VirtualFile virtualFile) throws ServerException { if (virtualFile.isFolder()) { addTree(virtualFile); } else { addFile(virtualFile); } } protected void addTree(VirtualFile tree) throws ServerException { final long start = System.currentTimeMillis(); final LinkedList<VirtualFile> q = new LinkedList<>(); q.add(tree); int indexedFiles = 0; while (!q.isEmpty()) { final VirtualFile folder = q.pop(); if (folder.exists()) { LazyIterator<VirtualFile> children = folder.getChildren(VirtualFileFilter.ALL); while (children.hasNext()) { final VirtualFile child = children.next(); if (child.isFolder()) { q.push(child); } else { addFile(child); indexedFiles++; } } } } final long end = System.currentTimeMillis(); LOG.debug("Indexed {} files from {}, time: {} ms", indexedFiles, tree.getPath(), (end - start)); } protected void addFile(VirtualFile virtualFile) throws ServerException { if (virtualFile.exists()) { try (Reader fContentReader = filter.accept(virtualFile) ? new BufferedReader( new InputStreamReader(virtualFile.getContent().getStream())) : null) { getIndexWriter().updateDocument(new Term("path", virtualFile.getPath()), createDocument(virtualFile, fContentReader)); } catch (OutOfMemoryError oome) { close(); throw oome; } catch (IOException e) { throw new ServerException(e.getMessage(), e); } catch (ForbiddenException e) { throw new ServerException(e.getServiceError()); } } } @Override public final void delete(String path) throws ServerException { doDelete(new Term("path", path)); } protected void doDelete(Term deleteTerm) throws ServerException { try { getIndexWriter().deleteDocuments(new PrefixQuery(deleteTerm)); } catch (OutOfMemoryError oome) { close(); throw oome; } catch (IOException e) { throw new ServerException(e.getMessage(), e); } } @Override public final void update(VirtualFile virtualFile) throws ServerException { doUpdate(new Term("path", virtualFile.getPath()), virtualFile); } protected void doUpdate(Term deleteTerm, VirtualFile virtualFile) throws ServerException { try (Reader fContentReader = filter.accept(virtualFile) ? new BufferedReader( new InputStreamReader(virtualFile.getContent().getStream())) : null) { getIndexWriter().updateDocument(deleteTerm, createDocument(virtualFile, fContentReader)); } catch (OutOfMemoryError oome) { close(); throw oome; } catch (IOException e) { throw new ServerException(e.getMessage(), e); } catch (ForbiddenException e) { throw new ServerException(e.getServiceError()); } } protected Document createDocument(VirtualFile virtualFile, Reader inReader) throws ServerException { final Document doc = new Document(); doc.add(new StringField("path", virtualFile.getPath(), Field.Store.YES)); doc.add(new StringField("name", virtualFile.getName(), Field.Store.YES)); doc.add(new StringField("mediatype", getMediaType(virtualFile), Field.Store.YES)); if (inReader != null) { doc.add(new TextField("text", inReader)); } return doc; } /** Get virtual file media type. Any additional parameters (e.g. 'charset') are removed. */ private String getMediaType(VirtualFile virtualFile) throws ServerException { String mediaType = virtualFile.getMediaType(); final int paramStartIndex = mediaType.indexOf(';'); if (paramStartIndex != -1) { mediaType = mediaType.substring(0, paramStartIndex).trim(); } return mediaType; } }