blob: 8ff451a0f982e7d45ea44668df8ec076be737644 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004, 2015 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* James Blackburn (Broadcom Corp.) - ongoing development
* Lars Vogel <Lars.Vogel@vogella.com> - Bug 473427
*******************************************************************************/
package org.eclipse.core.internal.localstore;
import java.io.*;
import java.util.*;
import org.eclipse.core.internal.resources.ResourceException;
import org.eclipse.core.internal.resources.ResourceStatus;
import org.eclipse.core.internal.utils.Messages;
import org.eclipse.core.resources.IResourceStatus;
import org.eclipse.core.runtime.*;
import org.eclipse.osgi.util.NLS;
/**
* A bucket is a persistent dictionary having paths as keys. Values are determined
* by subclasses.
*
* @since 3.1
*/
public abstract class Bucket {
public static abstract class Entry {
/**
* This entry has not been modified in any way so far.
*
* @see #state
*/
private final static int STATE_CLEAR = 0;
/**
* This entry has been requested for deletion.
*
* @see #state
*/
private final static int STATE_DELETED = 0x02;
/**
* This entry has been modified.
*
* @see #state
*/
private final static int STATE_DIRTY = 0x01;
/**
* Logical path of the object we are storing history for. This does not
* correspond to a file system path.
*/
private IPath path;
/**
* State for this entry. Possible values are STATE_CLEAR, STATE_DIRTY and STATE_DELETED.
*
* @see #STATE_CLEAR
* @see #STATE_DELETED
* @see #STATE_DIRTY
*/
private byte state = STATE_CLEAR;
protected Entry(IPath path) {
this.path = path;
}
public void delete() {
state = STATE_DELETED;
}
public abstract int getOccurrences();
public IPath getPath() {
return path;
}
public abstract Object getValue();
public boolean isDeleted() {
return state == STATE_DELETED;
}
public boolean isDirty() {
return state == STATE_DIRTY;
}
public boolean isEmpty() {
return getOccurrences() == 0;
}
public void markDirty() {
Assert.isTrue(state != STATE_DELETED);
state = STATE_DIRTY;
}
/**
* Called on the entry right after the visitor has visited it.
*/
public void visited() {
// does not do anything by default
}
}
/**
* A visitor for bucket entries.
*/
public static abstract class Visitor {
// should continue the traversal
public final static int CONTINUE = 0;
// should stop looking at any states immediately
public final static int STOP = 1;
// should stop looking at states for files in this container (or any of its children)
public final static int RETURN = 2;
/**
* Called after the bucket has been visited (and saved).
* @throws CoreException
*/
public void afterSaving(Bucket bucket) throws CoreException {
// empty implementation, subclasses to override
}
/**
* @throws CoreException
*/
public void beforeSaving(Bucket bucket) throws CoreException {
// empty implementation, subclasses to override
}
/**
* @return either STOP, CONTINUE or RETURN
*/
public abstract int visit(Entry entry);
}
/**
* The segment name for the root directory for index files.
*/
static final String INDEXES_DIR_NAME = ".indexes"; //$NON-NLS-1$
/**
* Map of the history entries in this bucket. Maps (String -&gt; byte[][] or String[][]),
* where the key is the path of the object we are storing history for, and
* the value is the history entry data (UUID,timestamp) pairs.
*/
private final Map<String, Object> entries;
/**
* The file system location of this bucket index file.
*/
private File location;
/**
* Whether the in-memory bucket is dirty and needs saving
*/
private boolean needSaving = false;
/**
* The project name for the bucket currently loaded. <code>null</code> if this is the root bucket.
*/
protected String projectName;
public Bucket() {
this.entries = new HashMap<>();
}
/**
* Applies the given visitor to this bucket index.
* @param visitor
* @param filter
* @param depth the number of trailing segments that can differ from the filter
* @return one of STOP, RETURN or CONTINUE constants
* @exception CoreException
*/
public final int accept(Visitor visitor, IPath filter, int depth) throws CoreException {
if (entries.isEmpty())
return Visitor.CONTINUE;
try {
for (Iterator<Map.Entry<String, Object>> i = entries.entrySet().iterator(); i.hasNext();) {
Map.Entry<String, Object> mapEntry = i.next();
IPath path = new Path(mapEntry.getKey());
// check whether the filter applies
int matchingSegments = filter.matchingFirstSegments(path);
if (!filter.isPrefixOf(path) || path.segmentCount() - matchingSegments > depth)
continue;
// apply visitor
Entry bucketEntry = createEntry(path, mapEntry.getValue());
// calls the visitor passing all uuids for the entry
int outcome = visitor.visit(bucketEntry);
// notify the entry it has been visited
bucketEntry.visited();
if (bucketEntry.isDeleted()) {
needSaving = true;
i.remove();
} else if (bucketEntry.isDirty()) {
needSaving = true;
mapEntry.setValue(bucketEntry.getValue());
}
if (outcome != Visitor.CONTINUE)
return outcome;
}
return Visitor.CONTINUE;
} finally {
visitor.beforeSaving(this);
save();
visitor.afterSaving(this);
}
}
/**
* Tries to delete as many empty levels as possible.
*/
private void cleanUp(File toDelete) {
if (!toDelete.delete())
// if deletion didn't go well, don't bother trying to delete the parent dir
return;
// don't try to delete beyond the root for bucket indexes
if (toDelete.getName().equals(INDEXES_DIR_NAME))
return;
// recurse to parent directory
cleanUp(toDelete.getParentFile());
}
/**
* Factory method for creating entries. Subclasses to override.
*/
protected abstract Entry createEntry(IPath path, Object value);
/**
* Flushes this bucket so it has no contents and is not associated to any
* location. Any uncommitted changes are lost.
*/
public void flush() {
projectName = null;
location = null;
entries.clear();
needSaving = false;
}
/**
* Returns how many entries there are in this bucket.
*/
public final int getEntryCount() {
return entries.size();
}
/**
* Returns the value for entry corresponding to the given path (null if none found).
*/
public final Object getEntryValue(String path) {
return entries.get(path);
}
/**
* Returns the file name used to persist the index for this bucket.
*/
protected abstract String getIndexFileName();
/**
* Returns the version number for the file format used to persist this bucket.
*/
protected abstract byte getVersion();
/**
* Returns the file name to be used to store bucket version information
*/
protected abstract String getVersionFileName();
/**
* Loads the contents from a file under the given directory.
*/
public void load(String newProjectName, File baseLocation) throws CoreException {
load(newProjectName, baseLocation, false);
}
/**
* Loads the contents from a file under the given directory. If <code>force</code> is
* <code>false</code>, if this bucket already contains the contents from the current location,
* avoids reloading.
*/
public void load(String newProjectName, File baseLocation, boolean force) throws CoreException {
try {
// avoid reloading
if (!force && this.location != null && baseLocation.equals(this.location.getParentFile()) && (projectName == null ? (newProjectName == null) : projectName.equals(newProjectName))) {
this.projectName = newProjectName;
return;
}
// previously loaded bucket may not have been saved... save before loading new one
save();
this.projectName = newProjectName;
this.location = new File(baseLocation, getIndexFileName());
this.entries.clear();
if (!this.location.isFile())
return;
try (DataInputStream source = new DataInputStream(new BufferedInputStream(new FileInputStream(location), 8192))) {
int version = source.readByte();
if (version != getVersion()) {
// unknown version
String message = NLS.bind(Messages.resources_readMetaWrongVersion, location.getAbsolutePath(), Integer.toString(version));
ResourceStatus status = new ResourceStatus(IResourceStatus.FAILED_READ_METADATA, message);
throw new ResourceException(status);
}
int entryCount = source.readInt();
for (int i = 0; i < entryCount; i++)
this.entries.put(readEntryKey(source), readEntryValue(source));
}
} catch (IOException ioe) {
String message = NLS.bind(Messages.resources_readMeta, location.getAbsolutePath());
ResourceStatus status = new ResourceStatus(IResourceStatus.FAILED_READ_METADATA, null, message, ioe);
throw new ResourceException(status);
}
}
private String readEntryKey(DataInputStream source) throws IOException {
if (projectName == null)
return source.readUTF();
return IPath.SEPARATOR + projectName + source.readUTF();
}
/**
* Defines how data for a given entry is to be read from a bucket file. To be implemented by subclasses.
*/
protected abstract Object readEntryValue(DataInputStream source) throws IOException, CoreException;
/**
* Saves this bucket's contents back to its location.
*/
public void save() throws CoreException {
if (!needSaving)
return;
try {
if (entries.isEmpty()) {
needSaving = false;
cleanUp(location);
return;
}
// ensure the parent location exists
File parent = location.getParentFile();
if (parent == null)
throw new IOException();//caught and rethrown below
parent.mkdirs();
try (DataOutputStream destination = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(location), 8192))) {
destination.write(getVersion());
destination.writeInt(entries.size());
for (java.util.Map.Entry<String, Object> entry : entries.entrySet()) {
writeEntryKey(destination, entry.getKey());
writeEntryValue(destination, entry.getValue());
}
}
needSaving = false;
} catch (IOException ioe) {
String message = NLS.bind(Messages.resources_writeMeta, location.getAbsolutePath());
ResourceStatus status = new ResourceStatus(IResourceStatus.FAILED_WRITE_METADATA, null, message, ioe);
throw new ResourceException(status);
}
}
/**
* Sets the value for the entry with the given path. If <code>value</code> is <code>null</code>,
* removes the entry.
*/
public final void setEntryValue(String path, Object value) {
if (value == null)
entries.remove(path);
else
entries.put(path, value);
needSaving = true;
}
private void writeEntryKey(DataOutputStream destination, String path) throws IOException {
if (projectName == null) {
destination.writeUTF(path);
return;
}
// omit the project name
int pathLength = path.length();
int projectLength = projectName.length();
String key = (pathLength == projectLength + 1) ? "" : path.substring(projectLength + 1); //$NON-NLS-1$
destination.writeUTF(key);
}
/**
* Defines how an entry is to be persisted to the bucket file.
*/
protected abstract void writeEntryValue(DataOutputStream destination, Object entryValue) throws IOException, CoreException;
}