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 - Bug 473427
/******************************************************************************* * 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
/** * 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 Also:
  • state
/** * 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 Also:
  • state
/** * This entry has been requested for deletion. * * @see #state */
private final static int STATE_DELETED = 0x02;
This entry has been modified.
See Also:
  • state
/** * 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.
/** * 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 Also:
/** * 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.
/** * 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.
/** * 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 –
/** * 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 –
/** * @throws CoreException */
public void beforeSaving(Bucket bucket) throws CoreException { // empty implementation, subclasses to override }
Returns:either STOP, CONTINUE or RETURN
/** * @return either STOP, CONTINUE or RETURN */
public abstract int visit(Entry entry); }
The segment name for the root directory for index files.
/** * 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 -> 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.
/** * 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.
/** * The file system location of this bucket index file. */
private File location;
Whether the in-memory bucket is dirty and needs saving
/** * Whether the in-memory bucket is dirty and needs saving */
private boolean needSaving = false;
The project name for the bucket currently loaded. null if this is the root bucket.
/** * 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.
Params:
  • visitor –
  • filter –
  • depth – the number of trailing segments that can differ from the filter
Throws:
Returns:one of STOP, RETURN or CONTINUE constants
/** * 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.
/** * 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.
/** * 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.
/** * 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.
/** * 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).
/** * 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.
/** * 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.
/** * 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
/** * 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.
/** * 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 force is false, if this bucket already contains the contents from the current location, avoids reloading.
/** * 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.
/** * 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.
/** * 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 value is null, removes the entry.
/** * 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.
/** * Defines how an entry is to be persisted to the bucket file. */
protected abstract void writeEntryValue(DataOutputStream destination, Object entryValue) throws IOException, CoreException; }