blob: abacc332c585f5aad76d4ba584ed9570238e45bb [file] [log] [blame]
/*
* 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);
}
}