/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.jackrabbit.commons; import java.io.InputStream; import java.util.Calendar; import javax.jcr.Item; import javax.jcr.ItemNotFoundException; import javax.jcr.ItemVisitor; import javax.jcr.Node; import javax.jcr.PathNotFoundException; import javax.jcr.Property; import javax.jcr.PropertyIterator; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.UnsupportedRepositoryOperationException; import javax.jcr.Value; import javax.jcr.ValueFactory; import javax.jcr.lock.LockException; import javax.jcr.nodetype.NodeType; import javax.jcr.nodetype.NodeTypeManager; import javax.jcr.version.Version; import javax.jcr.version.VersionHistory; /** * Abstract base class for implementing the JCR {@link Node} interface. * <p> * {@link Item} methods <em>without</em> a default implementation: * <ul> * <li>{@link Item#accept(javax.jcr.ItemVisitor)}</li> * <li>{@link Item#getName()}</li> * <li>{@link Item#getParent()}</li> * <li>{@link Item#getSession()}</li> * <li>{@link Item#isModified()}</li> * <li>{@link Item#isNew()}</li> * <li>{@link Item#isSame(Item)}</li> * <li>{@link Item#refresh(boolean)}</li> * <li>{@link Item#remove()}</li> * <li>{@link Item#save()}</li> * </ul> * <p> * {@link Node} methods <em>without</em> a default implementation: * <ul> * <li>{@link Node#addMixin(String)}</li> * <li>{@link Node#addNode(String)}</li> * <li>{@link Node#addNode(String, String)}</li> * <li>{@link Node#canAddMixin(String)}</li> * <li>{@link Node#cancelMerge(Version)}</li> * <li>{@link Node#checkin()}</li> * <li>{@link Node#checkout()}</li> * <li>{@link Node#doneMerge(Version)}</li> * <li>{@link Node#getBaseVersion()}</li> * <li>{@link Node#getCorrespondingNodePath(String)}</li> * <li>{@link Node#getDefinition()}</li> * <li>{@link Node#getIndex()}</li> * <li>{@link Node#getLock()}</li> * <li>{@link Node#getNode(String)}</li> * <li>{@link Node#getNodes()}</li> * <li>{@link Node#getNodes(String)}</li> * <li>{@link Node#getPrimaryItem()}</li> * <li>{@link Node#getProperties()}</li> * <li>{@link Node#getProperties(String)}</li> * <li>{@link Node#getReferences()}</li> * <li>{@link Node#lock(boolean, boolean)}</li> * <li>{@link Node#merge(String, boolean)}</li> * <li>{@link Node#orderBefore(String, String)}</li> * <li>{@link Node#removeMixin(String)}</li> * <li>{@link Node#restore(Version, String, boolean)}</li> * <li>{@link Node#setProperty(String, Value)}</li> * <li>{@link Node#setProperty(String, Value[])}</li> * <li>{@link Node#unlock()}</li> * <li>{@link Node#update(String)}</li> * </ul> */ public abstract class AbstractNode extends AbstractItem implements Node { //----------------------------------------------------------------< Item > /** * Accepts the given item visitor. * <p> * The default implementation calls {@link ItemVisitor#visit(Node)} on * the given visitor with this node as the argument. * * @param visitor item visitor * @throws RepositoryException if an error occurs */ public void accept(ItemVisitor visitor) throws RepositoryException { visitor.visit(this); } /** * Returns the path of this node. * <p> * The default implementation recursively calls this method on the * parent node and appends the name and optionally the index of this * node to construct the full path. Returns "/" if the parent node is * not available (i.e. this is the root node). * * @return node path * @throws RepositoryException if an error occurs */ public String getPath() throws RepositoryException { try { StringBuffer buffer = new StringBuffer(getParent().getPath()); if (buffer.length() > 1) { buffer.append('/'); } buffer.append(getName()); int index = getIndex(); if (index != 1) { buffer.append('['); buffer.append(index); buffer.append(']'); } return buffer.toString(); } catch (ItemNotFoundException e) { return "/"; } } /** * Returns <code>true</code>. * * @return <code>true</code> */ public boolean isNode() { return true; } //----------------------------------------------------------------< Node > /** * Returns the declared mixin node types of this node. * <p> * The default implementation uses the values of the * <code>jcr:mixinTypes</code> property to look up the mixin node types * from the {@link NodeTypeManager} of the current workspace. * * @return mixin node types * @throws RepositoryException if an error occurs */ public NodeType[] getMixinNodeTypes() throws RepositoryException { try { NodeTypeManager manager = getSession().getWorkspace().getNodeTypeManager(); Property property = getProperty(getName("jcr:mixinTypes")); Value[] values = property.getValues(); NodeType[] types = new NodeType[values.length]; for (int i = 0; i < values.length; i++) { types[i] = manager.getNodeType(values[i].getString()); } return types; } catch (PathNotFoundException e) { // jcr:mixinTypes does not exist, i.e. no mixin types on this node return new NodeType[0]; } } /** * Returns the primary node type of this node. * <p> * The default implementation uses the value of the * <code>jcr:primaryType</code> property to look up the primary * node type from the {@link NodeTypeManager} of the current workspace. * * @return primary node type * @throws RepositoryException if an error occurs */ public NodeType getPrimaryNodeType() throws RepositoryException { NodeTypeManager manager = getSession().getWorkspace().getNodeTypeManager(); Property property = getProperty(getName("jcr:primaryType")); return manager.getNodeType(property.getString()); } /** * Returns the property at the given relative path from this node. * <p> * The default implementation looks up the parent node of the given * relative path and iterates through the properties of that node to * find and return the identified property. * * @param relPath relative path of the property * @return property * @throws PathNotFoundException if the property is not found * @throws RepositoryException if an error occurs */ public Property getProperty(String relPath) throws PathNotFoundException, RepositoryException { // Corner case, remove any "/." self references at the end of the path while (relPath.endsWith("/.")) { relPath = relPath.substring(0, relPath.length() - 2); } // Find the parent node of the identified property Node node = this; int slash = relPath.lastIndexOf('/'); if (slash == 0) { node = getSession().getRootNode(); relPath = relPath.substring(1); } else if (slash > 0) { node = getNode(relPath.substring(0, slash)); relPath = relPath.substring(slash + 1); } // Look for the named property. Must iterate and re-check for the name // since the client could have used an invalid path like "./a|b". PropertyIterator properties = node.getProperties(relPath); while (properties.hasNext()) { Property property = (Property) properties.next(); if (relPath.equals(property.getName())) { return property; } } throw new PathNotFoundException("Property not found: " + relPath); } /** * Returns the UUID of this node. * <p> * The default implementation checks if this node is referenceable (i.e. of * type <code>mix:referenceable</code>) and returns the contents of the * <code>jcr:uuid</code> property if it is. * * @return node UUID * @throws UnsupportedRepositoryOperationException * if this node is not referenceable * @throws RepositoryException if an error occurs */ public String getUUID() throws UnsupportedRepositoryOperationException, RepositoryException { if (isNodeType(getName("mix:referenceable"))) { return getProperty(getName("jcr:uuid")).getString(); } else { throw new UnsupportedRepositoryOperationException( "This node is not referenceable: " + getPath()); } } /** * Returns the version history of this node. * <p> * The default implementation returns the containing version history of * the base version of this node. * * @return version history * @throws RepositoryException if an error occurs */ public VersionHistory getVersionHistory() throws RepositoryException { return getBaseVersion().getContainingHistory(); } /** * Checks whether a node at the given relative path exists. * <p> * The default implementation looks up the node using * {@link Node#getNode(String)} and returns <code>true</code> if * a {@link PathNotFoundException} is not thrown. * * @param relPath relative path * @return <code>true</code> if a node exists at the given path, * <code>false</code> otherwise * @throws RepositoryException if an error occurs */ public boolean hasNode(String relPath) throws RepositoryException { try { getNode(relPath); return true; } catch (PathNotFoundException e) { return false; } } /** * Checks if this node has one or more properties. * <p> * The default implementation calls {@link Node#getNodes()} and returns * <code>true</code> iff returned iterator has at least one element. * * @return <code>true</code> if this node has child nodes, * <code>false</code> otherwise * @throws RepositoryException if an error occurs */ public boolean hasNodes() throws RepositoryException { return getNodes().hasNext(); } /** * Checks if this node has one or more properties. * <p> * The default implementation calls {@link Node#getProperties()} and * returns <code>true</code> iff returned iterator has at least one element. * <p> * Note that in normal circumstances (i.e. no weird access controls) this * method will always return <code>true</code> since all nodes always have * at least the <code>jcr:primaryType</code> property. * * @return <code>true</code> if this node has properties, * <code>false</code> otherwise * @throws RepositoryException if an error occurs */ public boolean hasProperties() throws RepositoryException { return getProperties().hasNext(); } /** * Checks whether a property at the given relative path exists. * <p> * The default implementation looks up the property using * {@link Node#getProperty(String)} and returns <code>true</code> if * a {@link PathNotFoundException} is not thrown. * * @param relPath relative path * @return <code>true</code> if a property exists at the given path, * <code>false</code> otherwise * @throws RepositoryException if an error occurs */ public boolean hasProperty(String relPath) throws RepositoryException { try { getProperty(relPath); return true; } catch (PathNotFoundException e) { return false; } } /** * Checks if this node holds a lock. * <p> * The default implementation calls {@link Node#getLock()} and returns * <code>true</code> iff the holding node of the lock is the same as this * node. * * @return <code>true</code> if this node holds a lock, * <code>false</code> otherwise * @throws RepositoryException if an error occurs */ public boolean holdsLock() throws RepositoryException { try { return isSame(getLock().getNode()); } catch (LockException e) { return false; } } /** * Checks whether this node is checked out. * <p> * The default implementation checks the <code>jcr:isCheckedOut</code> * property if this node is versionable, and recursively calls this method * on the parent node if this node is not versionable. A non-versionable * root node always returns <code>true</code> from this method. * * @return <code>true</code> if this node is checked out, * <code>false</code> otherwise * @throws RepositoryException if an error occurs */ public boolean isCheckedOut() throws RepositoryException { if (isNodeType(getName("jcr:versionable"))) { // This node is versionable, check the jcr:isCheckedOut property return getProperty(getName("jcr:isCheckedOut")).getBoolean(); } else { try { // This node is not versionable, is the parent checked out? return getParent().isCheckedOut(); } catch (ItemNotFoundException e) { // This node is the root node, always checked out return true; } } } /** * Checks if this node is locked. * <p> * The default implementation calls {@link Node#getLock()} and returns * <code>true</code> iff a {@link LockException} is not thrown. * * @return <code>true</code> if this node is locked, * <code>false</code> otherwise * @throws RepositoryException if an error occurs */ public boolean isLocked() throws RepositoryException { try { getLock(); return true; } catch (LockException e) { return false; } } /** * Checks whether this node is of the given type. * <p> * The default implementation iterates through the primary and mixin * types and all the supertypes of this node, returning <code>true</code> * if a type with the given name is encountered. Returns <code>false</code> * if none of the types matches. * * @param name type name * @return <code>true</code> if this node is of the given type, * <code>false</code> otherwise * @throws RepositoryException if an error occurs */ public boolean isNodeType(String name) throws RepositoryException { NodeType type = getPrimaryNodeType(); if (name.equals(type.getName())) { return true; } NodeType[] supertypes = type.getSupertypes(); for (int i = 0; i < supertypes.length; i++) { if (name.equals(supertypes[i].getName())) { return true; } } NodeType[] mixins = getMixinNodeTypes(); for (int i = 0; i < mixins.length; i++) { if (name.equals(mixins[i].getName())) { return true; } supertypes = mixins[i].getSupertypes(); for (int j = 0; j < supertypes.length; j++) { if (name.equals(supertypes[j].getName())) { return true; } } } return false; } /** * Restores this node to the version with the given name. * <p> * The default implement retrieves the named {@link Version} from the * associated {@link VersionHistory} and forwards the call to the * {@link Node#restore(Version, boolean)} method. * * @param versionName version name * @param removeExisting passed through * @throws RepositoryException if an error occurs */ public void restore(String versionName, boolean removeExisting) throws RepositoryException { restore(getVersionHistory().getVersion(versionName), removeExisting); } /** * Restores this node to the given version. * <p> * The default implementation forwards the call to the * {@link Node#restore(Version, String, boolean)} method using the * relative path ".". * * @param version passed through * @param removeExisting passed through * @throws RepositoryException if an error occurs */ public void restore(Version version, boolean removeExisting) throws RepositoryException { restore(version, ".", removeExisting); } /** * Restores this node to the version with the given label. * <p> * The default implement retrieves the labeled {@link Version} from the * associated {@link VersionHistory} and forwards the call to the * {@link Node#restore(Version, boolean)} method. * * @param versionLabel version label * @param removeExisting passed through * @throws RepositoryException if an error occurs */ public void restoreByLabel(String versionLabel, boolean removeExisting) throws RepositoryException { restore(getVersionHistory().getVersionByLabel(versionLabel), removeExisting); } /** * Sets the value of the named property. * <p> * The default implementation uses the {@link ValueFactory} of the * current {@link Session} to create a {@link Value} instances from * the given string values and forwards the call to the * {@link Node#setProperty(String, Value[])} method. * * @param name property name * @param strings string values * @return modified property * @throws RepositoryException if an error occurs */ public Property setProperty(String name, String[] strings) throws RepositoryException { ValueFactory factory = getSession().getValueFactory(); Value[] values = new Value[strings.length]; for (int i = 0; i < strings.length; i++) { values[i] = factory.createValue(strings[i]); } return setProperty(name, values); } /** * Sets the value of the named property. * <p> * The default implementation uses the {@link ValueFactory} of the * current {@link Session} to create a {@link Value} instance from * the given string value and forwards the call to the * {@link Node#setProperty(String, Value)} method. * * @param name property name * @param value string value * @return modified property * @throws RepositoryException if an error occurs */ public Property setProperty(String name, String value) throws RepositoryException { ValueFactory factory = getSession().getValueFactory(); return setProperty(name, factory.createValue(value)); } /** * Sets the value of the named property. * <p> * The default implementation uses the {@link ValueFactory} of the * current {@link Session} to create a {@link Value} instance from * the given binary value and forwards the call to the * {@link Node#setProperty(String, Value)} method. * * @param name property name * @param value binary value * @return modified property * @throws RepositoryException if an error occurs */ public Property setProperty(String name, InputStream value) throws RepositoryException { ValueFactory factory = getSession().getValueFactory(); return setProperty(name, factory.createValue(value)); } /** * Sets the value of the named property. * <p> * The default implementation uses the {@link ValueFactory} of the * current {@link Session} to create a {@link Value} instance from * the given boolean value and forwards the call to the * {@link Node#setProperty(String, Value)} method. * * @param name property name * @param value boolean value * @return modified property * @throws RepositoryException if an error occurs */ public Property setProperty(String name, boolean value) throws RepositoryException { ValueFactory factory = getSession().getValueFactory(); return setProperty(name, factory.createValue(value)); } /** * Sets the value of the named property. * <p> * The default implementation uses the {@link ValueFactory} of the * current {@link Session} to create a {@link Value} instance from * the given double value and forwards the call to the * {@link Node#setProperty(String, Value)} method. * * @param name property name * @param value double value * @return modified property * @throws RepositoryException if an error occurs */ public Property setProperty(String name, double value) throws RepositoryException { ValueFactory factory = getSession().getValueFactory(); return setProperty(name, factory.createValue(value)); } /** * Sets the value of the named property. * <p> * The default implementation uses the {@link ValueFactory} of the * current {@link Session} to create a {@link Value} instance from * the given long value and forwards the call to the * {@link Node#setProperty(String, Value)} method. * * @param name property name * @param value long value * @return modified property * @throws RepositoryException if an error occurs */ public Property setProperty(String name, long value) throws RepositoryException { ValueFactory factory = getSession().getValueFactory(); return setProperty(name, factory.createValue(value)); } /** * Sets the value of the named property. * <p> * The default implementation uses the {@link ValueFactory} of the * current {@link Session} to create a {@link Value} instance from * the given date value and forwards the call to the * {@link Node#setProperty(String, Value)} method. * * @param name property name * @param value date value * @return modified property * @throws RepositoryException if an error occurs */ public Property setProperty(String name, Calendar value) throws RepositoryException { ValueFactory factory = getSession().getValueFactory(); return setProperty(name, factory.createValue(value)); } /** * Sets the value of the named property. * <p> * The default implementation uses the {@link ValueFactory} of the * current {@link Session} to create a {@link Value} instance from * the given reference value and forwards the call to the * {@link Node#setProperty(String, Value)} method. * * @param name property name * @param value reference value * @return modified property * @throws RepositoryException if an error occurs */ public Property setProperty(String name, Node value) throws RepositoryException { ValueFactory factory = getSession().getValueFactory(); return setProperty(name, factory.createValue(value)); } /** * Sets the value of the named property. * <p> * The default implementation uses the {@link ValueFactory} of the * current {@link Session} to convert the given value to the given * type and forwards the call to the * {@link Node#setProperty(String, Value)} method. * * @param name property name * @param value property value * @param type property type * @return modified property * @throws RepositoryException if an error occurs */ public Property setProperty(String name, Value value, int type) throws RepositoryException { if (value.getType() != type) { ValueFactory factory = getSession().getValueFactory(); value = factory.createValue(value.getString(), type); } return setProperty(name, value); } /** * Sets the value of the named property. * <p> * The default implementation uses the {@link ValueFactory} of the * current {@link Session} to convert the given values to the given * type and forwards the call to the * {@link Node#setProperty(String, Value[])} method. * * @param name property name * @param values property values * @param type property type * @return modified property * @throws RepositoryException if an error occurs */ public Property setProperty(String name, Value[] values, int type) throws RepositoryException { ValueFactory factory = getSession().getValueFactory(); Value[] converted = new Value[values.length]; for (int i = 0; i < values.length; i++) { if (values[i].getType() != type) { converted[i] = factory.createValue(values[i].getString(), type); } else { converted[i] = values[i]; } } return setProperty(name, converted); } /** * Sets the value of the named property. * <p> * The default implementation uses the {@link ValueFactory} of the * current {@link Session} to create {@link Value} instances of the * given type from the given string values and forwards the call to the * {@link Node#setProperty(String, Value[])} method. * * @param name property name * @param strings string values * @param type property type * @return modified property * @throws RepositoryException if an error occurs */ public Property setProperty(String name, String[] strings, int type) throws RepositoryException { ValueFactory factory = getSession().getValueFactory(); Value[] values = new Value[strings.length]; for (int i = 0; i < strings.length; i++) { values[i] = factory.createValue(strings[i], type); } return setProperty(name, values); } /** * Sets the value of the named property. * <p> * The default implementation uses the {@link ValueFactory} of the * current {@link Session} to create a {@link Value} instance of the * given type from the given string value and forwards the call to the * {@link Node#setProperty(String, Value)} method. * * @param name property name * @param value string value * @param type property type * @return modified property * @throws RepositoryException if an error occurs */ public Property setProperty(String name, String value, int type) throws RepositoryException { ValueFactory factory = getSession().getValueFactory(); return setProperty(name, factory.createValue(value, type)); } //-------------------------------------------------------------< private > /** * Returns the prefixed JCR name for the namespace URI and local name * using the current namespace mappings. * * @param uri namespace URI * @param name namespace-local name * @return prefixed JCR name * @throws RepositoryException if an error occurs */ private String getName(String name) throws RepositoryException { return new NamespaceHelper(getSession()).getJcrName(name); } }