/******************************************************************************* * Copyright (c) 2004, 2010 IBM Corporation 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: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.core.internal.properties; import java.io.*; import java.util.*; import org.eclipse.core.internal.localstore.Bucket; import org.eclipse.core.internal.resources.ResourceException; import org.eclipse.core.internal.utils.Messages; import org.eclipse.core.resources.IResourceStatus; import org.eclipse.core.runtime.*; import org.eclipse.osgi.util.NLS; public class PropertyBucket extends Bucket { public static class PropertyEntry extends Entry { private final static Comparator<String[]> COMPARATOR = new Comparator<String[]>() { public int compare(String[] o1, String[] o2) { int qualifierComparison = o1[0].compareTo(o2[0]); return qualifierComparison != 0 ? qualifierComparison : o1[1].compareTo(o2[1]); } }; private static final String[][] EMPTY_DATA = new String[0][]; /** * value is a String[][] of {{propertyKey.qualifier, propertyKey.localName, propertyValue}} */ private String[][] value; /** * Deletes the property with the given name, and returns the result array. Returns the original * array if the property to be deleted could not be found. Returns <code>null</code> if the property was found * and the original array had size 1 (instead of a zero-length array). */ static String[][] delete(String[][] existing, QualifiedName propertyName) { // a size-1 array is a special case if (existing.length == 1) return (existing[0][0].equals(propertyName.getQualifier()) && existing[0][1].equals(propertyName.getLocalName())) ? null : existing; // find the guy to delete int deletePosition = search(existing, propertyName); if (deletePosition < 0) // not found, nothing to delete return existing; String[][] newValue = new String[existing.length - 1][]; if (deletePosition > 0) // copy elements preceding the one to be removed System.arraycopy(existing, 0, newValue, 0, deletePosition); if (deletePosition < existing.length - 1) // copy elements succeeding the one to be removed System.arraycopy(existing, deletePosition + 1, newValue, deletePosition, newValue.length - deletePosition); return newValue; } static String[][] insert(String[][] existing, QualifiedName propertyName, String propertyValue) { // look for the right spot where to insert the new guy int index = search(existing, propertyName); if (index >= 0) { // found existing occurrence - just replace the value existing[index][2] = propertyValue; return existing; } // not found - insert int insertPosition = -index - 1; String[][] newValue = new String[existing.length + 1][]; if (insertPosition > 0) System.arraycopy(existing, 0, newValue, 0, insertPosition); newValue[insertPosition] = new String[] {propertyName.getQualifier(), propertyName.getLocalName(), propertyValue}; if (insertPosition < existing.length) System.arraycopy(existing, insertPosition, newValue, insertPosition + 1, existing.length - insertPosition); return newValue; } /** * Merges two entries (are always sorted). Duplicated additions replace existing ones. */ static Object merge(String[][] base, String[][] additions) { int additionPointer = 0; int basePointer = 0; int added = 0; String[][] result = new String[base.length + additions.length][]; while (basePointer < base.length && additionPointer < additions.length) { int comparison = COMPARATOR.compare(base[basePointer], additions[additionPointer]); if (comparison == 0) { result[added++] = additions[additionPointer++]; // duplicate, override basePointer++; } else if (comparison < 0) result[added++] = base[basePointer++]; else result[added++] = additions[additionPointer++]; } // copy the remaining states from either additions or base arrays String[][] 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 String[][] finalResult = new String[added][]; System.arraycopy(result, 0, finalResult, 0, finalResult.length); return finalResult; } private static int search(String[][] existing, QualifiedName propertyName) { return Arrays.binarySearch(existing, new String[] {propertyName.getQualifier(), propertyName.getLocalName(), null}, COMPARATOR); } public PropertyEntry(IPath path, PropertyEntry base) { super(path); //copy 2-dimensional array [x][y] int xLen = base.value.length; this.value = new String[xLen][]; for (int i = 0; i < xLen; i++) { int yLen = base.value[i].length; this.value[i] = new String[yLen]; System.arraycopy(base.value[i], 0, value[i], 0, yLen); } } /** * @param path * @param value is a String[][] {{propertyKey, propertyValue}} */ protected PropertyEntry(IPath path, String[][] value) { super(path); this.value = value; } /** * 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 (int i = 0; i < value.length; i++) if (value[i] != null) value[occurrences++] = value[i]; if (occurrences == value.length) // no states deleted return; if (occurrences == 0) { // no states remaining value = EMPTY_DATA; delete(); return; } String[][] result = new String[occurrences][]; System.arraycopy(value, 0, result, 0, occurrences); value = result; } public int getOccurrences() { return value == null ? 0 : value.length; } public String getProperty(QualifiedName name) { int index = search(value, name); return index < 0 ? null : value[index][2]; } public QualifiedName getPropertyName(int i) { return new QualifiedName(this.value[i][0], this.value[i][1]); } public String getPropertyValue(int i) { return this.value[i][2]; } public Object getValue() { return value; } public void visited() { compact(); } } public static final byte INDEX = 1; public static final byte QNAME = 2; /** Version number for the current implementation file's format. * <p> * Version 1: * <pre> * FILE ::= VERSION_ID ENTRY+ * ENTRY ::= PATH PROPERTY_COUNT PROPERTY+ * PATH ::= string (does not contain project name) * PROPERTY_COUNT ::= int * PROPERTY ::= QUALIFIER LOCAL_NAME VALUE * QUALIFIER ::= INDEX | QNAME * INDEX -> byte int * QNAME -> byte string * UUID ::= byte[16] * LAST_MODIFIED ::= byte[8] * </pre> * </p> */ private static final byte VERSION = 1; private final List<String> qualifierIndex = new ArrayList<String>(); public PropertyBucket() { super(); } protected Entry createEntry(IPath path, Object value) { return new PropertyEntry(path, (String[][]) value); } private PropertyEntry getEntry(IPath path) { String pathAsString = path.toString(); String[][] existing = (String[][]) getEntryValue(pathAsString); if (existing == null) return null; return new PropertyEntry(path, existing); } /* (non-Javadoc) * @see org.eclipse.core.internal.localstore.Bucket#getIndexFileName() */ protected String getIndexFileName() { return "properties.index"; //$NON-NLS-1$ } public String getProperty(IPath path, QualifiedName name) { PropertyEntry entry = getEntry(path); if (entry == null) return null; return entry.getProperty(name); } protected byte getVersion() { return VERSION; } /* (non-Javadoc) * @see org.eclipse.core.internal.localstore.Bucket#getVersionFileName() */ protected String getVersionFileName() { return "properties.version"; //$NON-NLS-1$ } public void load(String newProjectName, File baseLocation, boolean force) throws CoreException { qualifierIndex.clear(); super.load(newProjectName, baseLocation, force); } protected Object readEntryValue(DataInputStream source) throws IOException, CoreException { int length = source.readUnsignedShort(); String[][] properties = new String[length][3]; for (int j = 0; j < properties.length; j++) { // qualifier byte constant = source.readByte(); switch (constant) { case QNAME : properties[j][0] = source.readUTF(); qualifierIndex.add(properties[j][0]); break; case INDEX : properties[j][0] = qualifierIndex.get(source.readInt()); break; default : //if we get here the properties file is corrupt IPath resourcePath = projectName == null ? Path.ROOT : Path.ROOT.append(projectName); String msg = NLS.bind(Messages.properties_readProperties, resourcePath.toString()); throw new ResourceException(IResourceStatus.FAILED_READ_METADATA, null, msg, null); } // localName properties[j][1] = source.readUTF(); // propertyValue properties[j][2] = source.readUTF(); } return properties; } public void save() throws CoreException { qualifierIndex.clear(); super.save(); } public void setProperties(PropertyEntry entry) { IPath path = entry.getPath(); String[][] additions = (String[][]) entry.getValue(); String pathAsString = path.toString(); String[][] existing = (String[][]) getEntryValue(pathAsString); if (existing == null) { setEntryValue(pathAsString, additions); return; } setEntryValue(pathAsString, PropertyEntry.merge(existing, additions)); } public void setProperty(IPath path, QualifiedName name, String value) { String pathAsString = path.toString(); String[][] existing = (String[][]) getEntryValue(pathAsString); if (existing == null) { if (value != null) setEntryValue(pathAsString, new String[][] {{name.getQualifier(), name.getLocalName(), value}}); return; } String[][] newValue; if (value != null) newValue = PropertyEntry.insert(existing, name, value); else newValue = PropertyEntry.delete(existing, name); // even if newValue == existing we should mark as dirty (insert may just change the existing array) setEntryValue(pathAsString, newValue); } protected void writeEntryValue(DataOutputStream destination, Object entryValue) throws IOException { String[][] properties = (String[][]) entryValue; destination.writeShort(properties.length); for (int j = 0; j < properties.length; j++) { // writes the property key qualifier int index = qualifierIndex.indexOf(properties[j][0]); if (index == -1) { destination.writeByte(QNAME); destination.writeUTF(properties[j][0]); qualifierIndex.add(properties[j][0]); } else { destination.writeByte(INDEX); destination.writeInt(index); } // then the local name destination.writeUTF(properties[j][1]); // then the property value destination.writeUTF(properties[j][2]); } } }