| /******************************************************************************* |
| * Copyright (c) 2000, 2002 IBM Corporation and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Common Public License v0.5 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/cpl-v05.html |
| * |
| * Contributors: |
| * IBM - Initial API and implementation |
| ******************************************************************************/ |
| package org.eclipse.core.internal.properties; |
| |
| import java.util.*; |
| |
| import org.eclipse.core.internal.indexing.IndexCursor; |
| import org.eclipse.core.internal.indexing.ObjectID; |
| import org.eclipse.core.internal.resources.ResourceException; |
| import org.eclipse.core.internal.utils.Policy; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.resources.IResourceStatus; |
| import org.eclipse.core.runtime.*; |
| /** |
| * |
| */ |
| public class PropertyStore { |
| |
| // The indexed store will maintain the properties |
| protected IndexedStoreWrapper store = null; |
| |
| // Add directives |
| public static final int CREATE = 0; // must not exist |
| public static final int UPDATE = 1; // must exist |
| public static final int SET_UPDATE = 2; // create if doesn't exist, update if exists |
| public static final int SET_SKIP = 3; // create if doesn't exist, don't update if exists |
| |
| // Remove directives |
| public static final int IGNORE_MISSING = 0; |
| public static final int FAIL_MISSING = 1; |
| |
| public PropertyStore(IPath location) { |
| store = new IndexedStoreWrapper(location); |
| } |
| protected boolean basicExists(StoreKey searchKey) throws CoreException { |
| byte[] searchBytes = searchKey.toBytes(); |
| IndexCursor cursor = store.getCursor(); |
| try { |
| cursor.find(searchBytes); |
| boolean exists = cursor.keyEquals(searchBytes); |
| cursor.close(); |
| return exists; |
| } catch (Exception e) { |
| String message = Policy.bind("properties.couldNotReadProp", searchKey.getQualifier(), searchKey.getLocalName()); //$NON-NLS-1$ |
| throw new ResourceException(IResourceStatus.FAILED_READ_LOCAL, searchKey.getResourceName().getPath(), message, e); |
| } |
| } |
| /** |
| * The caller is responsible for ensuring that this will not produce |
| * duplicate keys in the index. |
| */ |
| protected void basicInsert(StoreKey key, String value) throws CoreException { |
| try { |
| ObjectID valueID = store.createObject(value); |
| store.getIndex().insert(key.toBytes(), valueID); |
| } catch (Exception e) { |
| String message = Policy.bind("properties.couldNotWriteProp", key.getQualifier(), key.getLocalName()); //$NON-NLS-1$ |
| throw new ResourceException(IResourceStatus.FAILED_WRITE_LOCAL, key.getResourceName().getPath(), message, e); |
| } |
| } |
| protected boolean basicRemove(ResourceName resourceName, QualifiedName propertyName) throws CoreException { |
| StoreKey key = new StoreKey(resourceName, propertyName); |
| byte[] keyBytes = key.toBytes(); |
| boolean wasFound = false; |
| IndexCursor cursor = store.getCursor(); |
| try { |
| cursor.find(keyBytes); |
| if (cursor.keyEquals(keyBytes)) { |
| wasFound = true; |
| ObjectID valueID = cursor.getValueAsObjectID(); |
| store.removeObject(valueID); |
| cursor.remove(); |
| } |
| cursor.close(); |
| } catch (Exception e) { |
| String message = Policy.bind("properties.couldNotDeleteProp", key.getQualifier(), key.getLocalName()); //$NON-NLS-1$ |
| throw new ResourceException(IResourceStatus.FAILED_DELETE_LOCAL, resourceName.getPath(), message, e); |
| } |
| return wasFound; |
| } |
| protected void basicUpdate(StoreKey key, String value) throws CoreException { |
| byte[] keyBytes = key.toBytes(); |
| IndexCursor cursor = store.getCursor(); |
| try { |
| cursor.find(keyBytes); |
| if (cursor.keyEquals(keyBytes)) { |
| ObjectID oldID = cursor.getValueAsObjectID(); |
| store.removeObject(oldID); |
| ObjectID newValueId = store.createObject(value); |
| cursor.updateValue(newValueId); |
| } |
| cursor.close(); |
| } catch (Exception e) { |
| String message = Policy.bind("properties.couldNotWriteProp", key.getQualifier(), key.getLocalName()); //$NON-NLS-1$ |
| throw new ResourceException(IResourceStatus.FAILED_WRITE_LOCAL, key.getResourceName().getPath(), message, e); |
| } |
| } |
| protected synchronized void commonSet(ResourceName resourceName, StoredProperty[] properties, int depth, int setMode, QueryResults failures) throws CoreException { |
| if (depth == IResource.DEPTH_ZERO) { |
| for (int i = 0; i < properties.length; i++) { |
| StoredProperty property = properties[i]; |
| StoreKey key = new StoreKey(resourceName, property.getName()); |
| boolean exists = basicExists(key); |
| if ((exists && (setMode == CREATE)) || (!exists && (setMode == UPDATE))) |
| failures.add(resourceName, property); |
| else |
| if (exists && (setMode != SET_SKIP)) |
| basicUpdate(key, property.getStringValue()); |
| else |
| basicInsert(key, property.getStringValue()); |
| } |
| } else { |
| Enumeration resourceNamesEnum = deepResourceNames(resourceName); |
| while (resourceNamesEnum.hasMoreElements()) |
| commonSet((ResourceName) resourceNamesEnum.nextElement(), properties, IResource.DEPTH_ZERO, setMode, failures); |
| } |
| } |
| /** |
| * Returns the names of all resources that are rooted at the given resource. |
| * <p> |
| * Answers an <code>Enumeration</code> of <code>IResourceName</code>. |
| * The enumerator will include (at least) <code>resourceName</code> if it |
| * exists. If <code>resourceName</code> does not exist returns an empty |
| * enumerator. |
| * |
| * @see IResourceName |
| * @param resourceName the name of the top most resource to match. |
| * @return an enumeration of matching resource names. |
| */ |
| public Enumeration deepResourceNames(ResourceName resourceName) throws CoreException { |
| final Set resultHolder = new HashSet(10); |
| IVisitor visitor = new IVisitor() { |
| public void visit(ResourceName resourceName, StoredProperty property, IndexCursor cursor) { |
| resultHolder.add(resourceName); |
| } |
| public boolean requiresValue(ResourceName resourceName, QualifiedName propertyName) { |
| return false; |
| } |
| }; |
| recordsDeepMatching(resourceName, visitor); |
| return Collections.enumeration(resultHolder); |
| } |
| /** |
| * Returns the named property for the given resource. |
| * <p> |
| * The retieval is performed to depth zero. Returns <code>null</code> |
| * if there is no such property defined on the resource. |
| * |
| * @param resourceName the resource name to match. |
| * @param propertyName the property name to match. |
| * @return the matching property, or <code>null</code> if no such property. |
| */ |
| public StoredProperty get(ResourceName resourceName, final QualifiedName propertyName) throws CoreException { |
| final Object[] resultHolder = new Object[1]; |
| IVisitor simpleVisitor = new IVisitor() { |
| public void visit(ResourceName resourceName, StoredProperty property, IndexCursor cursor) { |
| resultHolder[0] = property; |
| } |
| public boolean requiresValue(ResourceName resourceName, QualifiedName propertyName) { |
| return true; |
| } |
| }; |
| recordsMatching(resourceName, propertyName, simpleVisitor); |
| return (StoredProperty) resultHolder[0]; |
| } |
| /** |
| * Returns all the properties for a given resource. |
| * <p> |
| * Answer a <code>QueryResults</code> containing <code>StoredProperty</code>. |
| * If there are no matches returns an empty <code>QueryResults</code></p> |
| * <p> |
| * The depth parameter allows searching based on resource name path prefix.</p> |
| * |
| * @see QueryResults |
| * @param resourceName the resource name to match. |
| * @param depth the scope of the query |
| * @return a <code>QueryResults</code> with the matching properties. |
| */ |
| public QueryResults getAll(ResourceName resourceName, int depth) throws CoreException { |
| final QueryResults result = new QueryResults(); |
| IVisitor visitor = new IVisitor() { |
| public void visit(ResourceName resourceName, StoredProperty property, IndexCursor cursor) { |
| result.add(resourceName, property); |
| } |
| public boolean requiresValue(ResourceName resourceName, QualifiedName propertyName) { |
| return true; |
| } |
| }; |
| if (depth == IResource.DEPTH_ZERO) |
| recordsMatching(resourceName, visitor); |
| else |
| recordsDeepMatching(resourceName, visitor); |
| return result; |
| } |
| /** |
| * Returns all the property names for a given resource. |
| * <p> |
| * The result is a <code>QueryResults</code> containing <code>QualifiedName</code>. |
| * If the resource has no defined properties, the method returns |
| * an empty <code>QueryResults</code>.</p> |
| * <p> |
| * The depth parameter allows searching based on resource name path prefix.</p> |
| * |
| * @param resourceName the resource name to match. |
| * @param depth the depth to which the query runs. |
| * @return a <code>QueryResults</code> containing the property names. |
| */ |
| public QueryResults getNames(ResourceName resourceName, int depth) throws CoreException { |
| QueryResults results = new QueryResults(); |
| if (depth == IResource.DEPTH_ZERO) |
| recordsMatching(resourceName, propertyNameVisitor(results)); |
| else |
| recordsDeepMatching(resourceName, propertyNameVisitor(results)); |
| return results; |
| } |
| protected IVisitor propertyNameVisitor(final QueryResults results) { |
| return new IVisitor() { |
| public void visit(ResourceName resourceName, StoredProperty property, IndexCursor cursor) { |
| results.add(resourceName, property.getName()); |
| } |
| public boolean requiresValue(ResourceName resourceName, QualifiedName propertyName) { |
| return false; |
| } |
| }; |
| } |
| /** |
| * Matches all properties for a given resource. |
| */ |
| protected void recordsDeepMatching(ResourceName resourceName, IVisitor visitor) throws CoreException { |
| |
| // Build the partial 'search' key |
| StoreKey searchKey = new StoreKey(resourceName, true); |
| byte[] searchBytes = searchKey.toBytes(); |
| int probe = searchBytes.length; |
| // Position a cursor over the first matching key |
| IndexCursor cursor = store.getCursor(); |
| try { |
| cursor.find(searchBytes); |
| |
| // While we have a prefix match |
| while (cursor.keyMatches(searchBytes)) { |
| // Must check that the prefix is up to a valid path segment |
| // note that the matching bytes length is > search key length since |
| // properties MUST have a local name. |
| byte[] matchingBytes = cursor.getKey(); |
| if (probe == 1 || //empty path is a valid prefix for all paths |
| (matchingBytes[probe] == 0) || // a full path match |
| (matchingBytes[probe] == 47 /*IPath.SEPARATOR*/)) { |
| // a segment boundary match |
| visitPropertyAt(cursor, visitor); |
| } |
| // else the match is intra-segment and therefore invalid |
| cursor.next(); |
| } |
| cursor.close(); |
| } catch (Exception e) { |
| throw new ResourceException(IResourceStatus.FAILED_READ_LOCAL, resourceName.getPath(), Policy.bind("properties.storeProblem"), e); //$NON-NLS-1$ |
| } |
| } |
| /** |
| * Matches all properties for a given resource. |
| */ |
| protected void recordsMatching(ResourceName resourceName, IVisitor visitor) throws CoreException { |
| |
| // Build the partial 'search' key |
| StoreKey searchKey = new StoreKey(resourceName, false); |
| byte[] searchBytes = searchKey.toBytes(); |
| // Position a cursor over the first matching key |
| IndexCursor cursor = store.getCursor(); |
| try { |
| cursor.find(searchBytes); |
| |
| // While we have a prefix match, evaluate the visitor |
| while (cursor.keyMatches(searchBytes)) { |
| visitPropertyAt(cursor, visitor); |
| cursor.next(); |
| } |
| cursor.close(); |
| } catch (Exception e) { |
| store.reset(); |
| throw new ResourceException(IResourceStatus.FAILED_READ_LOCAL, resourceName.getPath(), Policy.bind("properties.storeProblem"), e); //$NON-NLS-1$ |
| } |
| } |
| /** |
| * Matches the given property for a given resource. |
| * Note that there should be only one. |
| */ |
| protected void recordsMatching(ResourceName resourceName, QualifiedName propertyName, IVisitor visitor) throws CoreException { |
| |
| // Build the full 'search' key |
| StoreKey searchKey = new StoreKey(resourceName, propertyName); |
| byte[] searchBytes = searchKey.toBytes(); |
| // Position a cursor over the first matching key |
| IndexCursor cursor = store.getCursor(); |
| try { |
| cursor.find(searchBytes); |
| |
| // If we have an exact match, evaluate the visitor |
| if (cursor.keyEquals(searchBytes)) |
| visitPropertyAt(cursor, visitor); |
| cursor.close(); |
| } catch (Exception e) { |
| store.reset(); |
| throw new ResourceException(IResourceStatus.FAILED_READ_LOCAL, resourceName.getPath(), Policy.bind("properties.storeProblem"), e); //$NON-NLS-1$ |
| } |
| } |
| /** |
| * Remove the given collection of named properties from the given resource. |
| * <p> |
| * All of the properties being removed must exist already on the |
| * resource based on the removeRule parameter. If the rule is |
| * MISSING_IGNORE then attempts to remove properties that do not exist |
| * are ignored, if the rule is MISSING_FAIL the method will throw |
| * a <code>PropertyNotFoundException</code> if the property does not exist. |
| * <p> |
| * If an exception is thrown, all properties that did previously |
| * exist will have been removed from the resource. To determine which |
| * properties caused the exception see the offenders result in the exception.</p> |
| * <p> |
| * The depth parameter allows matching based on resource name path prefix.</p> |
| * |
| * @param resourceName the resource containing the properties. |
| * @param propertyNames the property names to remove. |
| * @param depth the scope for matching the resource name. |
| * @param removeRule the behavior when removing non-existant properties. |
| * @exception PropertyNotFoundException |
| */ |
| public QueryResults remove(ResourceName resourceName, QualifiedName[] propertyNames, int depth, int removeRule) throws CoreException { |
| QueryResults failures = new QueryResults(); |
| if (depth == IResource.DEPTH_ZERO) { |
| for (int i = 0; i < propertyNames.length; i++) { |
| boolean found = basicRemove(resourceName, propertyNames[i]); |
| if (!found && (removeRule == FAIL_MISSING)) |
| failures.add(resourceName, propertyNames[i]); |
| } |
| } else { |
| Enumeration resourceNamesEnum = deepResourceNames(resourceName); |
| while (resourceNamesEnum.hasMoreElements()) { |
| ResourceName resName = (ResourceName) resourceNamesEnum.nextElement(); |
| for (int i = 0; i < propertyNames.length; i++) { |
| boolean found = basicRemove(resName, propertyNames[i]); |
| if (!found && (removeRule == FAIL_MISSING)) |
| failures.add(resName, propertyNames[i]); |
| } |
| } |
| } |
| return failures; |
| } |
| /** |
| * Remove the named property from the given resource. |
| * <p> |
| * If a matching property does not exist on this resource |
| * the method has no affect on the store. Removal is performed |
| * to depth zero.</p> |
| * <p> |
| * @param resourceName the resource containing the property. |
| * @param property the property to remove. |
| */ |
| public void remove(ResourceName resourceName, QualifiedName propertyName) throws CoreException { |
| remove(resourceName, new QualifiedName[] {propertyName}, IResource.DEPTH_ZERO, IGNORE_MISSING); |
| } |
| /** |
| * Remove all the properties from a given resource. |
| * <p> |
| * The depth parameter allows matching based on resource name path prefix.</p> |
| * |
| * @param resourceName the resource containing the properties. |
| * @param depth the scope for matching the resource name. |
| */ |
| public void removeAll(ResourceName resourceName, int depth) throws CoreException { |
| QueryResults namesSearch = getNames(resourceName, depth); |
| Enumeration resourceNamesEnum = namesSearch.getResourceNames(); |
| while (resourceNamesEnum.hasMoreElements()) { |
| ResourceName resName = (ResourceName) resourceNamesEnum.nextElement(); |
| Enumeration propertyNamesEnum = Collections.enumeration(namesSearch.getResults(resName)); |
| while (propertyNamesEnum.hasMoreElements()) { |
| QualifiedName propertyName = (QualifiedName) propertyNamesEnum.nextElement(); |
| basicRemove(resName, propertyName); |
| } |
| } |
| } |
| /** |
| * Sets the given collection of properties on the given resource. |
| * <p> |
| * The addRule determines whether the properties must already exist |
| * or not, and if they do whether they are updated by subsequent addition. |
| * Valid addRule values are defined in <code>IPropertyCollectionConstants</code>. |
| * <p> |
| * The depth parameter allows matching based on resource name path prefix.</p> |
| * <p> |
| * The <code>PropertyExistsException</code> is thrown if the matching resource |
| * already has a property of the same name, and the rule requires that |
| * it must not. If the exception is thrown, all successfull properties |
| * will have been set, and the failures are listed in the exception.</p> |
| * |
| * @param resourceName the resource to receive the properties. |
| * @param properties the properties to add. |
| * @param depth the depth at which to apply the add opertion. |
| * @param addRule the behavior of the add operation. |
| * @exception PropertyExistsException |
| */ |
| public QueryResults set(ResourceName resourceName, StoredProperty[] properties, int depth, int mode) throws CoreException { |
| QueryResults failures = new QueryResults(); |
| commonSet(resourceName, properties, depth, mode, failures); |
| return failures; |
| } |
| /** |
| * Sets the given property to the given resource. |
| * <p> |
| * The property is added to depth zero. If the resource already has |
| * a proprety with the same name, it's value is updated to the given |
| * value (i.e. SET_UPDATE add rule equivalent.)</p> |
| * |
| * @param resourceName the resource to receive the property. |
| * @param property the property to add. |
| */ |
| public void set(ResourceName resourceName, StoredProperty property) throws CoreException { |
| commonSet(resourceName, new StoredProperty[] {property}, IResource.DEPTH_ZERO, SET_UPDATE, null); |
| } |
| public void shutdown(IProgressMonitor monitor) { |
| if (store == null) |
| return; |
| store.close(); |
| } |
| public void startup(IProgressMonitor monitor) { |
| } |
| protected void visitPropertyAt(IndexCursor cursor, IVisitor visitor) throws CoreException { |
| try { |
| StoreKey key = new StoreKey(cursor.getKey()); |
| ResourceName resourceName = key.getResourceName(); |
| QualifiedName propertyName = key.getPropertyName(); |
| String propertyValue = null; |
| if (visitor.requiresValue(resourceName, propertyName)) |
| propertyValue = store.getObjectAsString(cursor.getValueAsObjectID()); |
| visitor.visit(resourceName, new StoredProperty(propertyName, propertyValue), cursor); |
| } catch (Exception e) { |
| throw new ResourceException(IResourceStatus.FAILED_READ_LOCAL, null, Policy.bind("properties.storeProblem"), e); //$NON-NLS-1$ |
| } |
| } |
| public void commit() throws CoreException { |
| store.commit(); |
| } |
| } |