| /******************************************************************************* |
| * 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 |
| *******************************************************************************/ |
| package org.eclipse.core.internal.localstore; |
| |
| import java.io.*; |
| import java.util.Arrays; |
| import java.util.Comparator; |
| import org.eclipse.core.internal.utils.UniversalUniqueIdentifier; |
| import org.eclipse.core.runtime.IPath; |
| |
| public class HistoryBucket extends Bucket { |
| |
| /** |
| * A entry in the bucket index. Each entry has one path and a collection |
| * of states, which by their turn contain a (UUID, timestamp) pair. |
| * <p> |
| * This class is intended as a lightweight way of hiding the internal data structure. |
| * Objects of this class are supposed to be short-lived. No instances |
| * of this class are kept stored anywhere. The real stuff (the internal data structure) |
| * is. |
| * </p> |
| */ |
| public static final class HistoryEntry extends Bucket.Entry { |
| |
| final static Comparator<byte[]> COMPARATOR = (state1, state2) -> compareStates(state1, state2); |
| |
| // the length of each component of the data array |
| private final static byte[][] EMPTY_DATA = new byte[0][]; |
| // the length of a long in bytes |
| private final static int LONG_LENGTH = 8; |
| // the length of a UUID in bytes |
| private final static int UUID_LENGTH = UniversalUniqueIdentifier.BYTES_SIZE; |
| public final static int DATA_LENGTH = UUID_LENGTH + LONG_LENGTH; |
| |
| /** |
| * The history states. The first array dimension is the number of states. The |
| * second dimension is an encoding of the {UUID,timestamp} pair for that entry. |
| */ |
| private byte[][] data; |
| |
| /** |
| * Comparison logic for states in byte[] form. |
| * |
| * @see Comparator#compare(java.lang.Object, java.lang.Object) |
| */ |
| static int compareStates(byte[] state1, byte[] state2) { |
| long timestamp1 = getTimestamp(state1); |
| long timestamp2 = getTimestamp(state2); |
| if (timestamp1 == timestamp2) |
| return -UniversalUniqueIdentifier.compareTime(state1, state2); |
| return timestamp1 < timestamp2 ? +1 : -1; |
| } |
| |
| /** |
| * Returns the byte array representation of a (UUID, timestamp) pair. |
| */ |
| static byte[] getState(UniversalUniqueIdentifier uuid, long timestamp) { |
| byte[] uuidBytes = uuid.toBytes(); |
| byte[] state = new byte[DATA_LENGTH]; |
| System.arraycopy(uuidBytes, 0, state, 0, uuidBytes.length); |
| for (int j = 0; j < LONG_LENGTH; j++) { |
| state[UUID_LENGTH + j] = (byte) (0xFF & timestamp); |
| timestamp >>>= 8; |
| } |
| return state; |
| } |
| |
| private static long getTimestamp(byte[] state) { |
| long timestamp = 0; |
| for (int j = 0; j < LONG_LENGTH; j++) |
| timestamp += (state[UUID_LENGTH + j] & 0xFFL) << j * 8; |
| return timestamp; |
| } |
| |
| /** |
| * Inserts the given item into the given array at the right position. |
| * Returns the resulting array. Returns null if the item already exists. |
| */ |
| static byte[][] insert(byte[][] existing, byte[] toAdd) { |
| // look for the right spot where to insert the new guy |
| int index = search(existing, toAdd); |
| if (index >= 0) |
| // already there - nothing else to be done |
| return null; |
| // not found - insert |
| int insertPosition = -index - 1; |
| byte[][] newValue = new byte[existing.length + 1][]; |
| if (insertPosition > 0) |
| System.arraycopy(existing, 0, newValue, 0, insertPosition); |
| newValue[insertPosition] = toAdd; |
| if (insertPosition < existing.length) |
| System.arraycopy(existing, insertPosition, newValue, insertPosition + 1, existing.length - insertPosition); |
| return newValue; |
| } |
| |
| /** |
| * Merges two entries (are always sorted). Duplicates are discarded. |
| */ |
| static byte[][] merge(byte[][] base, byte[][] additions) { |
| int additionPointer = 0; |
| int basePointer = 0; |
| int added = 0; |
| byte[][] result = new byte[base.length + additions.length][]; |
| while (basePointer < base.length && additionPointer < additions.length) { |
| int comparison = compareStates(base[basePointer], additions[additionPointer]); |
| if (comparison == 0) { |
| result[added++] = base[basePointer++]; |
| // duplicate, ignore |
| additionPointer++; |
| } else if (comparison < 0) |
| result[added++] = base[basePointer++]; |
| else |
| result[added++] = additions[additionPointer++]; |
| } |
| // copy the remaining states from either additions or base arrays |
| byte[][] remaining = basePointer == base.length ? additions : base; |
| int remainingPointer = basePointer == base.length ? additionPointer : basePointer; |
| int remainingCount = remaining.length - remainingPointer; |
| System.arraycopy(remaining, remainingPointer, result, added, remainingCount); |
| added += remainingCount; |
| if (added == base.length + additions.length) |
| // no collisions |
| return result; |
| // there were collisions, need to compact |
| byte[][] finalResult = new byte[added][]; |
| System.arraycopy(result, 0, finalResult, 0, finalResult.length); |
| return finalResult; |
| } |
| |
| private static int search(byte[][] existing, byte[] element) { |
| return Arrays.binarySearch(existing, element, COMPARATOR); |
| } |
| |
| public HistoryEntry(IPath path, byte[][] data) { |
| super(path); |
| this.data = data; |
| } |
| |
| public HistoryEntry(IPath path, HistoryEntry base) { |
| super(path); |
| this.data = new byte[base.data.length][]; |
| System.arraycopy(base.data, 0, this.data, 0, this.data.length); |
| } |
| |
| /** |
| * Compacts the data array removing any null slots. If non-null slots |
| * are found, the entry is marked for removal. |
| */ |
| private void compact() { |
| if (!isDirty()) |
| return; |
| int occurrences = 0; |
| for (byte[] d : data) { |
| if (d != null) { |
| data[occurrences++] = d; |
| } |
| } |
| if (occurrences == data.length) |
| // no states deleted |
| return; |
| if (occurrences == 0) { |
| // no states remaining |
| data = EMPTY_DATA; |
| delete(); |
| return; |
| } |
| byte[][] result = new byte[occurrences][]; |
| System.arraycopy(data, 0, result, 0, occurrences); |
| data = result; |
| } |
| |
| public void deleteOccurrence(int i) { |
| markDirty(); |
| data[i] = null; |
| } |
| |
| byte[][] getData() { |
| return data; |
| } |
| |
| @Override |
| public int getOccurrences() { |
| return data.length; |
| } |
| |
| public long getTimestamp(int i) { |
| return getTimestamp(data[i]); |
| } |
| |
| public UniversalUniqueIdentifier getUUID(int i) { |
| return new UniversalUniqueIdentifier(data[i]); |
| } |
| |
| @Override |
| public Object getValue() { |
| return data; |
| } |
| |
| @Override |
| public boolean isEmpty() { |
| return data.length == 0; |
| } |
| |
| @Override |
| public void visited() { |
| compact(); |
| } |
| |
| } |
| |
| /** |
| * Version number for the current implementation file's format. |
| * <p> |
| * Version 2 (3.1 M5): |
| * </p> |
| * <pre> |
| * FILE ::= VERSION_ID ENTRY+ |
| * ENTRY ::= PATH STATE_COUNT STATE+ |
| * PATH ::= string (does not include project name) |
| * STATE_COUNT ::= int |
| * STATE ::= UUID LAST_MODIFIED |
| * UUID ::= byte[16] |
| * LAST_MODIFIED ::= byte[8] |
| * </pre> |
| * <p> |
| * Version 1 (3.1 M4): |
| * </p> |
| * <pre> |
| * FILE ::= VERSION_ID ENTRY+ |
| * ENTRY ::= PATH STATE_COUNT STATE+ |
| * PATH ::= string |
| * STATE_COUNT ::= int |
| * STATE ::= UUID LAST_MODIFIED |
| * UUID ::= byte[16] |
| * LAST_MODIFIED ::= byte[8] |
| * </pre> |
| */ |
| public final static byte VERSION = 2; |
| |
| public HistoryBucket() { |
| super(); |
| } |
| |
| public void addBlob(IPath path, UniversalUniqueIdentifier uuid, long lastModified) { |
| byte[] state = HistoryEntry.getState(uuid, lastModified); |
| String pathAsString = path.toString(); |
| byte[][] existing = (byte[][]) getEntryValue(pathAsString); |
| if (existing == null) { |
| setEntryValue(pathAsString, new byte[][] {state}); |
| return; |
| } |
| byte[][] newValue = HistoryEntry.insert(existing, state); |
| if (newValue == null) |
| return; |
| setEntryValue(pathAsString, newValue); |
| } |
| |
| public void addBlobs(HistoryEntry fileEntry) { |
| IPath path = fileEntry.getPath(); |
| byte[][] additions = fileEntry.getData(); |
| String pathAsString = path.toString(); |
| byte[][] existing = (byte[][]) getEntryValue(pathAsString); |
| if (existing == null) { |
| setEntryValue(pathAsString, additions); |
| return; |
| } |
| setEntryValue(pathAsString, HistoryEntry.merge(existing, additions)); |
| } |
| |
| @Override |
| protected Bucket.Entry createEntry(IPath path, Object value) { |
| return new HistoryEntry(path, (byte[][]) value); |
| } |
| |
| public HistoryEntry getEntry(IPath path) { |
| String pathAsString = path.toString(); |
| byte[][] existing = (byte[][]) getEntryValue(pathAsString); |
| if (existing == null) |
| return null; |
| return new HistoryEntry(path, existing); |
| } |
| |
| @Override |
| protected String getIndexFileName() { |
| return "history.index"; //$NON-NLS-1$ |
| } |
| |
| @Override |
| protected byte getVersion() { |
| return VERSION; |
| } |
| |
| @Override |
| protected String getVersionFileName() { |
| return "history.version"; //$NON-NLS-1$ |
| } |
| |
| @Override |
| protected Object readEntryValue(DataInputStream source) throws IOException { |
| int length = source.readUnsignedShort(); |
| byte[][] uuids = new byte[length][HistoryEntry.DATA_LENGTH]; |
| for (byte[] uuid : uuids) |
| source.read(uuid); |
| return uuids; |
| } |
| |
| @Override |
| protected void writeEntryValue(DataOutputStream destination, Object entryValue) throws IOException { |
| byte[][] uuids = (byte[][]) entryValue; |
| destination.writeShort(uuids.length); |
| for (byte[] uuid : uuids) |
| destination.write(uuid); |
| } |
| } |