Bug 496853 - Improve Lucene indexer locking, committing and recovery

Change-Id: I8fedd94903982f690f02e2bb8efb4d9237138633
Signed-off-by: Bartlomiej Laczkowski <bartlomiej.l@zend.com>
diff --git a/core/plugins/org.eclipse.dltk.core.index.lucene/META-INF/MANIFEST.MF b/core/plugins/org.eclipse.dltk.core.index.lucene/META-INF/MANIFEST.MF
index 33ff1c8..46a7b79 100644
--- a/core/plugins/org.eclipse.dltk.core.index.lucene/META-INF/MANIFEST.MF
+++ b/core/plugins/org.eclipse.dltk.core.index.lucene/META-INF/MANIFEST.MF
@@ -8,7 +8,8 @@
 Bundle-Vendor: %Bundle-Vendor
 Require-Bundle: org.eclipse.core.runtime,
  org.eclipse.core.resources,
- org.eclipse.dltk.core
+ org.eclipse.dltk.core,
+ org.eclipse.ui.workbench
 Bundle-RequiredExecutionEnvironment: JavaSE-1.7
 Bundle-ActivationPolicy: lazy
 Bundle-ClassPath: .,
diff --git a/core/plugins/org.eclipse.dltk.core.index.lucene/src/org/eclipse/dltk/internal/core/index/lucene/IndexContainer.java b/core/plugins/org.eclipse.dltk.core.index.lucene/src/org/eclipse/dltk/internal/core/index/lucene/IndexContainer.java
index da667a3..ab9ae3b 100644
--- a/core/plugins/org.eclipse.dltk.core.index.lucene/src/org/eclipse/dltk/internal/core/index/lucene/IndexContainer.java
+++ b/core/plugins/org.eclipse.dltk.core.index.lucene/src/org/eclipse/dltk/internal/core/index/lucene/IndexContainer.java
@@ -11,12 +11,9 @@
 package org.eclipse.dltk.internal.core.index.lucene;
 
 import java.io.IOException;
-import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -29,9 +26,11 @@
 import org.apache.lucene.search.SearcherFactory;
 import org.apache.lucene.search.SearcherManager;
 import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.SimpleFSLockFactory;
 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.dltk.core.index.lucene.LucenePlugin;
 
@@ -57,35 +56,32 @@
 
 		@Override
 		protected IStatus run(IProgressMonitor monitor) {
+			doClean();
+			return Status.OK_STATUS;
+		}
+
+		void clean(boolean fork) {
+			if (fork) {
+				schedule();
+			} else {
+				doClean();
+			}
+		}
+
+		private void doClean() {
 			close();
 			Path containerPath = Paths.get(fIndexRoot, getId());
 			try {
-				Files.walkFileTree(containerPath,
-						new SimpleFileVisitor<Path>() {
-							@Override
-							public FileVisitResult visitFile(Path file,
-									BasicFileAttributes attrs)
-									throws IOException {
-								Files.delete(file);
-								return FileVisitResult.CONTINUE;
-							}
-
-							@Override
-							public FileVisitResult postVisitDirectory(Path dir,
-									IOException exc) throws IOException {
-								Files.delete(dir);
-								return FileVisitResult.CONTINUE;
-							}
-						});
+				Utils.delete(containerPath);
 			} catch (IOException e) {
 				Logger.logException(e);
 			}
-			return Status.OK_STATUS;
 		}
 
 	}
 
 	private static final String TIMESTAMPS_DIR = "timestamps"; //$NON-NLS-1$
+	private static final long WRITE_LOCK_TIMEOUT = 3000;
 
 	private final String fIndexRoot;
 	private final String fContainerId;
@@ -113,19 +109,47 @@
 				new HashMap<Integer, SearcherManager>());
 	}
 
-	private IndexWriter createIndexWriter(Path path) {
+	private void purgeLocks(Path path) {
+		/*
+		 * Checks if any write locks exist (might be not removed if JVM crashed
+		 * or was terminated abnormally) and simply deletes them.
+		 */
+		Path writeLockPath = path.resolve(IndexWriter.WRITE_LOCK_NAME);
+		if (writeLockPath.toFile().exists()) {
+			try {
+				Files.delete(writeLockPath);
+			} catch (IOException e) {
+				Logger.logException(e);
+			}
+		}
+	}
+
+	private IndexWriter createWriter(Path path) throws IOException {
+		Directory indexDir = new IndexDirectory(path,
+				SimpleFSLockFactory.INSTANCE);
+		purgeLocks(path);
+		IndexWriterConfig config = new IndexWriterConfig(new SimpleAnalyzer());
+		ConcurrentMergeScheduler mergeScheduler = new ConcurrentMergeScheduler();
+		mergeScheduler.setDefaultMaxMergesAndThreads(true);
+		config.setMergeScheduler(mergeScheduler);
+		config.setOpenMode(OpenMode.CREATE_OR_APPEND);
+		config.setWriteLockTimeout(WRITE_LOCK_TIMEOUT);
+		config.setCommitOnClose(false);
+		return new IndexWriter(indexDir, config);
+	}
+
+	private IndexWriter getWriter(Path path) {
 		IndexWriter indexWriter = null;
 		try {
-			Directory indexDir = new IndexDirectory(path);
-			IndexWriterConfig config = new IndexWriterConfig(
-					new SimpleAnalyzer());
-			ConcurrentMergeScheduler mergeScheduler = new ConcurrentMergeScheduler();
-			mergeScheduler.setDefaultMaxMergesAndThreads(true);
-			config.setMergeScheduler(mergeScheduler);
-			config.setOpenMode(OpenMode.CREATE_OR_APPEND);
-			indexWriter = new IndexWriter(indexDir, config);
+			indexWriter = createWriter(path);
 		} catch (IOException e) {
-			Logger.logException(e);
+			// Try to recover possibly corrupted index
+			IndexRecovery.tryRecover(this, path, e);
+			try {
+				indexWriter = createWriter(path);
+			} catch (IOException ex) {
+				Logger.logException(ex);
+			}
 		}
 		return indexWriter;
 	}
@@ -138,7 +162,7 @@
 		if (fTimestampsWriter == null) {
 			Path writerPath = Paths.get(fIndexRoot, fContainerId,
 					TIMESTAMPS_DIR);
-			fTimestampsWriter = createIndexWriter(writerPath);
+			fTimestampsWriter = getWriter(writerPath);
 		}
 		return fTimestampsWriter;
 	}
@@ -163,7 +187,7 @@
 		if (writer == null) {
 			Path writerPath = Paths.get(fIndexRoot, fContainerId,
 					dataType.getDirectory(), String.valueOf(elementType));
-			writer = createIndexWriter(writerPath);
+			writer = getWriter(writerPath);
 			fIndexWriters.get(dataType).put(elementType, writer);
 		}
 		return writer;
@@ -205,9 +229,9 @@
 		}
 	}
 
-	public synchronized void delete() {
+	public synchronized void delete(boolean wait) {
 		// Delete container entry entirely
-		(new IndexCleaner()).schedule();
+		(new IndexCleaner()).clean(!wait);
 	}
 
 	public synchronized void close() {
@@ -238,4 +262,46 @@
 		}
 	}
 
+	synchronized boolean hasUncommittedChanges() {
+		for (Map<Integer, IndexWriter> dataWriters : fIndexWriters.values()) {
+			for (IndexWriter writer : dataWriters.values()) {
+				if (writer != null && writer.hasUncommittedChanges()) {
+					return true;
+				}
+			}
+			if (fTimestampsWriter != null) {
+				return fTimestampsWriter.hasUncommittedChanges();
+			}
+		}
+		return false;
+	}
+
+	synchronized void commit(IProgressMonitor monitor) {
+		int ticks = 1;
+		for (Map<?, ?> dataWriters : fIndexWriters.values()) {
+			ticks += dataWriters.size();
+		}
+		SubMonitor subMonitor = SubMonitor.convert(monitor, ticks);
+		try {
+			for (Map<Integer, IndexWriter> dataWriters : fIndexWriters
+					.values()) {
+				for (IndexWriter writer : dataWriters.values()) {
+					if (writer != null && !subMonitor.isCanceled()) {
+						writer.forceMergeDeletes(true);
+						writer.commit();
+						subMonitor.worked(1);
+					}
+				}
+			}
+			if (fTimestampsWriter != null && !subMonitor.isCanceled()) {
+				fTimestampsWriter.forceMergeDeletes(true);
+				fTimestampsWriter.commit();
+				subMonitor.worked(1);
+			}
+			subMonitor.done();
+		} catch (IOException e) {
+			Logger.logException(e);
+		}
+	}
+
 }
diff --git a/core/plugins/org.eclipse.dltk.core.index.lucene/src/org/eclipse/dltk/internal/core/index/lucene/IndexRecovery.java b/core/plugins/org.eclipse.dltk.core.index.lucene/src/org/eclipse/dltk/internal/core/index/lucene/IndexRecovery.java
new file mode 100644
index 0000000..77d6135
--- /dev/null
+++ b/core/plugins/org.eclipse.dltk.core.index.lucene/src/org/eclipse/dltk/internal/core/index/lucene/IndexRecovery.java
@@ -0,0 +1,72 @@
+/*******************************************************************************

+ * 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.IOException;

+import java.nio.file.Path;

+import java.text.MessageFormat;

+

+import org.apache.lucene.search.MatchAllDocsQuery;

+import org.eclipse.dltk.internal.core.search.ProjectIndexerManager;

+

+/**

+ * Lucene index container recovery class.

+ * 

+ * @author Bartlomiej Laczkowski

+ */

+@SuppressWarnings("restriction")

+public final class IndexRecovery {

+

+	private static final String RECOVERY_REASON = "Index writer could not be created, index data might be corrupted."; //$NON-NLS-1$

+	private static final String RECOVERY_STARTED = "Recovering index storage: {0}"; //$NON-NLS-1$

+	private static final String RECOVERY_FAILED = "Failed to recover index storage: {0}"; //$NON-NLS-1$

+

+	private IndexRecovery() {

+		// No instance

+	}

+

+	/**

+	 * <p>

+	 * Tries to recover possibly corrupted Lucene index. Recovery process will

+	 * try do do the following:

+	 * </p>

+	 * <ul>

+	 * <li>Remove problematic index directory.</li>

+	 * <li>Clean up time stamps data in corresponding index conatiner.</li>

+	 * <li>Trigger index rebuilding to fill empty index directory.</li>

+	 * </ul>

+	 * 

+	 * @param indexContainer

+	 * @param indexPath

+	 * @param exception

+	 */

+	static void tryRecover(IndexContainer indexContainer, Path indexPath,

+			IOException exception) {

+		Logger.logException(RECOVERY_REASON, exception);

+		Logger.log(Logger.INFO,

+				MessageFormat.format(RECOVERY_STARTED, indexPath.toString()));

+		try {

+			// Try to delete possibly corrupted index container

+			Utils.delete(indexPath);

+			// Clean time stamps to purge index state

+			indexContainer.getTimestampsWriter()

+					.deleteDocuments(new MatchAllDocsQuery());

+		} catch (IOException e) {

+			Logger.logException(

+					MessageFormat.format(RECOVERY_FAILED, indexPath.toString()),

+					e);

+			return;

+		}

+		// Re-triggering indexing will fill purged container indexes.

+		ProjectIndexerManager.startIndexing();

+	}

+

+}

diff --git a/core/plugins/org.eclipse.dltk.core.index.lucene/src/org/eclipse/dltk/internal/core/index/lucene/LuceneManager.java b/core/plugins/org.eclipse.dltk.core.index.lucene/src/org/eclipse/dltk/internal/core/index/lucene/LuceneManager.java
index 25c053d..5d36bf9 100644
--- a/core/plugins/org.eclipse.dltk.core.index.lucene/src/org/eclipse/dltk/internal/core/index/lucene/LuceneManager.java
+++ b/core/plugins/org.eclipse.dltk.core.index.lucene/src/org/eclipse/dltk/internal/core/index/lucene/LuceneManager.java
@@ -14,31 +14,41 @@
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.nio.file.FileVisitResult;
-import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
+import java.text.MessageFormat;
 import java.util.ArrayList;
-import java.util.HashMap;
 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>
@@ -69,13 +79,132 @@
 	 */
 	INSTANCE;
 
-	private static final class ShutdownListener implements IShutdownListener {
+	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$
@@ -90,7 +219,7 @@
 	private LuceneManager() {
 		fIndexProperties = new Properties();
 		fContainerMappings = new Properties();
-		fIndexContainers = new HashMap<>();
+		fIndexContainers = new ConcurrentHashMap<>();
 		fIndexRoot = Platform
 				.getStateLocation(LucenePlugin.getDefault().getBundle())
 				.append(INDEX_DIR).toOSString();
@@ -158,7 +287,7 @@
 	 * @param container
 	 */
 	public final void delete(final String container) {
-		deleteIndexContainer(container);
+		deleteIndexContainer(container, false);
 	}
 
 	/**
@@ -173,6 +302,52 @@
 		}
 	}
 
+	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;
@@ -195,9 +370,16 @@
 		}
 		loadMappings();
 		registerIndexContainers();
+		ShutdownListener shutdownListener = new ShutdownListener();
 		ModelManager.getModelManager().getIndexManager()
-				.addShutdownListener(new ShutdownListener());
-
+				.addShutdownListener(shutdownListener);
+		PlatformUI.getWorkbench().addWorkbenchListener(shutdownListener);
+		try {
+			ResourcesPlugin.getWorkspace().addSaveParticipant(LucenePlugin.ID,
+					shutdownListener);
+		} catch (CoreException e) {
+			Logger.logException(e);
+		}
 	}
 
 	private synchronized void shutdown() {
@@ -208,32 +390,6 @@
 		cleanup();
 	}
 
-	private 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);
-	}
-
-	private synchronized void deleteIndexContainer(String container) {
-		String containerId = (String) fContainerMappings.remove(container);
-		if (containerId != null) {
-			IndexContainer containerEntry = fIndexContainers
-					.remove(containerId);
-			saveMappings();
-			containerEntry.delete();
-		}
-	}
-
 	private void registerIndexContainers() {
 		for (String container : fContainerMappings.stringPropertyNames()) {
 			String containerId = fContainerMappings.getProperty(container);
@@ -302,6 +458,10 @@
 				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()) {
@@ -311,31 +471,36 @@
 		}
 		if (!toRemove.isEmpty()) {
 			for (String container : toRemove) {
-				deleteIndexContainer(container);
+				deleteIndexContainer(container, true);
 			}
-			// Save cleaned up container mappings
-			saveMappings();
+		}
+		/*
+		 * 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 {
-			Files.walkFileTree(indexRoot, new SimpleFileVisitor<Path>() {
-				@Override
-				public FileVisitResult visitFile(Path file,
-						BasicFileAttributes attrs) throws IOException {
-					Files.delete(file);
-					return FileVisitResult.CONTINUE;
-				}
-
-				@Override
-				public FileVisitResult postVisitDirectory(Path dir,
-						IOException exc) throws IOException {
-					Files.delete(dir);
-					return FileVisitResult.CONTINUE;
-				}
-			});
+			Utils.delete(indexRoot);
 		} catch (IOException e) {
 			Logger.logException(e);
 		}
diff --git a/core/plugins/org.eclipse.dltk.core.index.lucene/src/org/eclipse/dltk/internal/core/index/lucene/Messages.java b/core/plugins/org.eclipse.dltk.core.index.lucene/src/org/eclipse/dltk/internal/core/index/lucene/Messages.java
new file mode 100644
index 0000000..6087d68
--- /dev/null
+++ b/core/plugins/org.eclipse.dltk.core.index.lucene/src/org/eclipse/dltk/internal/core/index/lucene/Messages.java
@@ -0,0 +1,31 @@
+/*******************************************************************************

+ * 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 org.eclipse.osgi.util.NLS;

+

+/**

+ * Messages.

+ * 

+ * @author Bartlomiej Laczkowski

+ */

+public class Messages extends NLS {

+	private static final String BUNDLE_NAME = "org.eclipse.dltk.internal.core.index.lucene.messages"; //$NON-NLS-1$

+	public static String LuceneManager_Committer_flushing_index_data;

+	public static String LuceneManager_Committer_saving_indexes;

+	static {

+		// initialize resource bundle

+		NLS.initializeMessages(BUNDLE_NAME, Messages.class);

+	}

+

+	private Messages() {

+	}

+}

diff --git a/core/plugins/org.eclipse.dltk.core.index.lucene/src/org/eclipse/dltk/internal/core/index/lucene/Utils.java b/core/plugins/org.eclipse.dltk.core.index.lucene/src/org/eclipse/dltk/internal/core/index/lucene/Utils.java
new file mode 100644
index 0000000..64da887
--- /dev/null
+++ b/core/plugins/org.eclipse.dltk.core.index.lucene/src/org/eclipse/dltk/internal/core/index/lucene/Utils.java
@@ -0,0 +1,54 @@
+/*******************************************************************************

+ * 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.IOException;

+import java.nio.file.FileVisitResult;

+import java.nio.file.Files;

+import java.nio.file.Path;

+import java.nio.file.SimpleFileVisitor;

+import java.nio.file.attribute.BasicFileAttributes;

+

+/**

+ * Utility class for Lucene indexer.

+ * 

+ * @author Bartlomiej Laczkowski

+ */

+public final class Utils {

+

+	private Utils() {

+		// Utility class, should not be instantiated.

+	}

+

+	/**

+	 * Deletes directory under given path with all of its contents.

+	 * 

+	 * @param directory

+	 * @throws IOException

+	 */

+	public static void delete(Path directory) throws IOException {

+		Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {

+			@Override

+			public FileVisitResult visitFile(Path file,

+					BasicFileAttributes attrs) throws IOException {

+				Files.delete(file);

+				return FileVisitResult.CONTINUE;

+			}

+			@Override

+			public FileVisitResult postVisitDirectory(Path dir, IOException exc)

+					throws IOException {

+				Files.delete(dir);

+				return FileVisitResult.CONTINUE;

+			}

+		});

+	}

+

+}

diff --git a/core/plugins/org.eclipse.dltk.core.index.lucene/src/org/eclipse/dltk/internal/core/index/lucene/messages.properties b/core/plugins/org.eclipse.dltk.core.index.lucene/src/org/eclipse/dltk/internal/core/index/lucene/messages.properties
new file mode 100644
index 0000000..58d9c81
--- /dev/null
+++ b/core/plugins/org.eclipse.dltk.core.index.lucene/src/org/eclipse/dltk/internal/core/index/lucene/messages.properties
@@ -0,0 +1,13 @@
+###############################################################################

+# 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

+###############################################################################

+

+LuceneManager_Committer_flushing_index_data=Flushing index data to persistent storage: ({0} of {1})

+LuceneManager_Committer_saving_indexes=Saving indexes...
\ No newline at end of file