| /******************************************************************************* |
| * Copyright (c) 2004, 2015 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 |
| * Lars Vogel <Lars.Vogel@vogella.com> - Bug 473427 |
| *******************************************************************************/ |
| 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 = (o1, 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; |
| } |
| |
| @Override |
| 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]; |
| } |
| |
| @Override |
| public Object getValue() { |
| return value; |
| } |
| |
| @Override |
| 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<>(); |
| |
| public PropertyBucket() { |
| super(); |
| } |
| |
| @Override |
| 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); |
| } |
| |
| @Override |
| 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); |
| } |
| |
| @Override |
| protected byte getVersion() { |
| return VERSION; |
| } |
| |
| @Override |
| protected String getVersionFileName() { |
| return "properties.version"; //$NON-NLS-1$ |
| } |
| |
| @Override |
| public void load(String newProjectName, File baseLocation, boolean force) throws CoreException { |
| qualifierIndex.clear(); |
| super.load(newProjectName, baseLocation, force); |
| } |
| |
| @Override |
| 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; |
| } |
| |
| @Override |
| 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); |
| } |
| |
| @Override |
| protected void writeEntryValue(DataOutputStream destination, Object entryValue) throws IOException { |
| String[][] properties = (String[][]) entryValue; |
| destination.writeShort(properties.length); |
| for (String[] propertie : properties) { |
| // writes the property key qualifier |
| int index = qualifierIndex.indexOf(propertie[0]); |
| if (index == -1) { |
| destination.writeByte(QNAME); |
| destination.writeUTF(propertie[0]); |
| qualifierIndex.add(propertie[0]); |
| } else { |
| destination.writeByte(INDEX); |
| destination.writeInt(index); |
| } |
| // then the local name |
| destination.writeUTF(propertie[1]); |
| // then the property value |
| destination.writeUTF(propertie[2]); |
| } |
| } |
| } |