blob: 5d36bf9549cb20d638750b07a8be7cb4f95df9a2 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016 Zend Technologies and others.
* 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:
* Zend Technologies - initial API and implementation
*******************************************************************************/
package org.eclipse.dltk.internal.core.index.lucene;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.search.SearcherManager;
import org.eclipse.core.resources.ISaveContext;
import org.eclipse.core.resources.ISaveParticipant;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.dltk.core.DLTKLanguageManager;
import org.eclipse.dltk.core.IDLTKLanguageToolkit;
import org.eclipse.dltk.core.IShutdownListener;
import org.eclipse.dltk.core.index.lucene.LucenePlugin;
import org.eclipse.dltk.internal.core.ModelManager;
import org.eclipse.dltk.internal.core.search.DLTKWorkspaceScope;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchListener;
import org.eclipse.ui.PlatformUI;
/**
* <p>
* Apache Lucene indexes manager responsible for managing indexes model.
* </p>
* <p>
* Indexes are stored in hierarchical directory structure as follows:
* <code><pre>
* index_root
* |_container_id
* |_declarations
* |_model_element_type_id (index data)
* ...
* |_references
* |_model_element_type_id (index data)
* ...
* |_timestamps (index data)
* </pre></code>
* </p>
*
* @author Bartlomiej Laczkowski
*/
@SuppressWarnings("restriction")
public enum LuceneManager {
/**
* Manager Instance.
*/
INSTANCE;
private final class ShutdownListener
implements IShutdownListener, IWorkbenchListener, ISaveParticipant {
private final class Committer extends Job {
private List<IndexContainer> fContainers;
public Committer() {
super(Messages.LuceneManager_Committer_saving_indexes);
setUser(false);
setSystem(false);
}
@Override
public IStatus run(IProgressMonitor monitor) {
int containersNumber = fContainers.size();
monitor.beginTask("", containersNumber); //$NON-NLS-1$
SubMonitor subMonitor = SubMonitor.convert(monitor,
containersNumber);
int counter = 0;
try {
for (IndexContainer indexContainer : fContainers) {
if (!monitor.isCanceled()) {
counter++;
monitor.subTask(MessageFormat.format(
Messages.LuceneManager_Committer_flushing_index_data,
counter, containersNumber));
// Commit index data to file system storage
indexContainer.commit(subMonitor.newChild(1));
}
}
monitor.done();
} catch (Exception e) {
Logger.logException(e);
} finally {
// Whatever happens semaphore must be released.
fSemaphore.release();
}
return Status.OK_STATUS;
}
@Override
public boolean belongsTo(Object family) {
return family == LucenePlugin.LUCENE_JOB_FAMILY;
}
void committ() {
fContainers = getUncommittedContainers();
schedule();
}
}
private final Committer fCommitter = new Committer();
private final Semaphore fSemaphore = new Semaphore(0);
@Override
public boolean preShutdown(IWorkbench workbench, boolean forced) {
// Check if there is anything to commit first
if (getUncommittedContainers().isEmpty())
return true;
/*
* Trigger this hidden job that will use workspace root as
* scheduling rule just to show the committer job progress in the
* details pane of "Saving Workspace" dialog.
*/
Job job = new Job("") { //$NON-NLS-1$
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
fSemaphore.acquire();
} catch (InterruptedException e) {
// ignore
}
return Status.OK_STATUS;
}
};
job.setRule(ResourcesPlugin.getWorkspace().getRoot());
job.setSystem(true);
job.setUser(false);
job.schedule();
// Trigger committer job
fCommitter.committ();
return true;
}
@Override
public void saving(ISaveContext context) throws CoreException {
joinCommitter();
}
@Override
public void shutdown() {
joinCommitter();
// Shutdown manager and close all the writers and searchers
LuceneManager.INSTANCE.shutdown();
}
@Override
public void postShutdown(IWorkbench workbench) {
// ignore
}
@Override
public void doneSaving(ISaveContext context) {
// ignore
}
@Override
public void prepareToSave(ISaveContext context) throws CoreException {
// ignore
}
@Override
public void rollback(ISaveContext context) {
// ignore
}
private void joinCommitter() {
try {
fCommitter.join();
} catch (InterruptedException e) {
// ignore
}
}
}
private static final String INDEX_DIR = "index"; //$NON-NLS-1$
private static final String PROPERTIES_FILE = ".properties"; //$NON-NLS-1$
private static final String MAPPINGS_FILE = ".mappings"; //$NON-NLS-1$
private final String fIndexRoot;
private final Properties fIndexProperties;
private final Properties fContainerMappings;
private final Map<String, IndexContainer> fIndexContainers;
private LuceneManager() {
fIndexProperties = new Properties();
fContainerMappings = new Properties();
fIndexContainers = new ConcurrentHashMap<>();
fIndexRoot = Platform
.getStateLocation(LucenePlugin.getDefault().getBundle())
.append(INDEX_DIR).toOSString();
File indexRootDirectory = new File(fIndexRoot);
if (!indexRootDirectory.exists()) {
indexRootDirectory.mkdirs();
}
startup();
}
/**
* Finds and returns index writer for given container, data type and model
* element.
*
* @param container
* @param dataType
* @param elementType
* @return index writer
*/
public final IndexWriter findIndexWriter(String container,
IndexType dataType, int elementType) {
return getIndexContainer(container).getIndexWriter(dataType,
elementType);
}
/**
* Finds and returns index searcher for given container, data type and model
* element.
*
* @param container
* @param dataType
* @param elementType
* @return index searcher
*/
public final SearcherManager findIndexSearcher(String container,
IndexType dataType, int elementType) {
return getIndexContainer(container).getIndexSearcher(dataType,
elementType);
}
/**
* Finds and returns time stamps index writer for given container.
*
* @param container
* @return time stamps index writer
*/
public final IndexWriter findTimestampsWriter(String container) {
return getIndexContainer(container).getTimestampsWriter();
}
/**
* Finds and returns time stamps index searcher for given container.
*
* @param container
* @return time stamps index searcher
*/
public final SearcherManager findTimestampsSearcher(String container) {
return getIndexContainer(container).getTimestampsSearcher();
}
/**
* Deletes related container index entry (container entry is removed
* completely).
*
* @param container
*/
public final void delete(final String container) {
deleteIndexContainer(container, false);
}
/**
* Deletes given container's source module index data.
*
* @param container
* @param sourceModule
*/
public final void delete(String container, String sourceModule) {
if (fContainerMappings.getProperty(container) != null) {
getIndexContainer(container).delete(sourceModule);
}
}
synchronized String getContainerPath(String containerId) {
for (Object key : fContainerMappings.keySet()) {
String container = (String) key;
if (containerId.equals(fContainerMappings.getProperty(container))) {
return container;
}
}
return null;
}
synchronized List<IndexContainer> getUncommittedContainers() {
List<IndexContainer> uncommittedContainers = new ArrayList<>();
for (IndexContainer indexContainer : fIndexContainers.values()) {
if (indexContainer.hasUncommittedChanges()) {
uncommittedContainers.add(indexContainer);
}
}
return uncommittedContainers;
}
synchronized IndexContainer getIndexContainer(String container) {
String containerId = fContainerMappings.getProperty(container);
if (containerId == null) {
do {
// Just to be sure that ID does not already exist
containerId = UUID.randomUUID().toString();
} while (fContainerMappings.containsValue(containerId));
fContainerMappings.put(container, containerId);
fIndexContainers.put(containerId,
new IndexContainer(fIndexRoot, containerId));
// Persist mapping
saveMappings();
}
return fIndexContainers.get(containerId);
}
synchronized void deleteIndexContainer(String container, boolean wait) {
String containerId = (String) fContainerMappings.remove(container);
if (containerId != null) {
IndexContainer containerEntry = fIndexContainers
.remove(containerId);
saveMappings();
containerEntry.delete(wait);
}
}
private synchronized void startup() {
loadProperties();
boolean purgeIndexRoot = false;
boolean resetProperties = false;
String modelVersion = fIndexProperties
.getProperty(IndexProperties.KEY_MODEL_VERSION);
String luceneVersion = fIndexProperties
.getProperty(IndexProperties.KEY_LUCENE_VERSION);
if (!IndexProperties.MODEL_VERSION.equals(modelVersion)
|| !IndexProperties.LUCENE_VERSION.equals(luceneVersion)) {
purgeIndexRoot = true;
resetProperties = true;
}
if (purgeIndexRoot) {
purge();
}
if (resetProperties) {
resetProperties();
saveProperties();
}
loadMappings();
registerIndexContainers();
ShutdownListener shutdownListener = new ShutdownListener();
ModelManager.getModelManager().getIndexManager()
.addShutdownListener(shutdownListener);
PlatformUI.getWorkbench().addWorkbenchListener(shutdownListener);
try {
ResourcesPlugin.getWorkspace().addSaveParticipant(LucenePlugin.ID,
shutdownListener);
} catch (CoreException e) {
Logger.logException(e);
}
}
private synchronized void shutdown() {
// Close all searchers & writers in all container entries
for (IndexContainer entry : fIndexContainers.values()) {
entry.close();
}
cleanup();
}
private void registerIndexContainers() {
for (String container : fContainerMappings.stringPropertyNames()) {
String containerId = fContainerMappings.getProperty(container);
fIndexContainers.put(containerId,
new IndexContainer(fIndexRoot, containerId));
}
}
private void loadProperties() {
File file = Paths.get(fIndexRoot, PROPERTIES_FILE).toFile();
if (!file.exists()) {
return;
}
try (FileInputStream fis = new FileInputStream(file)) {
fIndexProperties.load(fis);
} catch (IOException e) {
Logger.logException(e);
}
}
private void loadMappings() {
File file = Paths.get(fIndexRoot, MAPPINGS_FILE).toFile();
if (!file.exists()) {
return;
}
try (FileInputStream fis = new FileInputStream(file)) {
fContainerMappings.load(fis);
} catch (IOException e) {
Logger.logException(e);
}
}
private void saveProperties() {
File file = Paths.get(fIndexRoot, PROPERTIES_FILE).toFile();
try (FileOutputStream fos = new FileOutputStream(file)) {
fIndexProperties.store(fos, ""); //$NON-NLS-1$
} catch (IOException e) {
Logger.logException(e);
}
}
private void saveMappings() {
File file = Paths.get(fIndexRoot, MAPPINGS_FILE).toFile();
try (FileOutputStream fos = new FileOutputStream(file)) {
fContainerMappings.store(fos, ""); //$NON-NLS-1$
} catch (IOException e) {
Logger.logException(e);
}
}
private void resetProperties() {
fIndexProperties.clear();
fIndexProperties.put(IndexProperties.KEY_MODEL_VERSION,
IndexProperties.MODEL_VERSION);
fIndexProperties.put(IndexProperties.KEY_LUCENE_VERSION,
IndexProperties.LUCENE_VERSION);
}
private void cleanup() {
List<String> containers = new ArrayList<>();
for (IDLTKLanguageToolkit toolkit : DLTKLanguageManager
.getLanguageToolkits()) {
DLTKWorkspaceScope scope = ModelManager.getModelManager()
.getWorkspaceScope(toolkit);
for (IPath path : scope.enclosingProjectsAndZips()) {
containers.add(path.toString());
}
}
/*
* Some projects/libraries could be deleted outside the workspace, clean
* up the related mappings that might left.
*/
Set<String> toRemove = new HashSet<>();
for (String mappedContainer : fContainerMappings
.stringPropertyNames()) {
if (!containers.contains(mappedContainer)) {
toRemove.add(mappedContainer);
}
}
if (!toRemove.isEmpty()) {
for (String container : toRemove) {
deleteIndexContainer(container, true);
}
}
/*
* Some projects/libraries could be deleted outside the workspace,
* delete up the related index directories that might left.
*/
List<Path> toDelete = new ArrayList<>();
Path indexRoot = Paths.get(fIndexRoot);
for (File containerDir : indexRoot.toFile().listFiles()) {
if (containerDir.isDirectory() && !fContainerMappings
.containsValue(containerDir.getName())) {
toDelete.add(Paths.get(containerDir.getAbsolutePath()));
}
}
if (!toDelete.isEmpty()) {
for (Path containerDir : toDelete) {
try {
Utils.delete(containerDir);
} catch (IOException e) {
Logger.logException(e);
}
}
}
}
private void purge() {
Path indexRoot = Paths.get(fIndexRoot);
try {
Utils.delete(indexRoot);
} catch (IOException e) {
Logger.logException(e);
}
indexRoot.toFile().mkdir();
}
}