/* * ModeShape (http://www.modeshape.org) * * Licensed 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.modeshape.jcr; import java.math.BigDecimal; import java.util.Calendar; import java.util.Date; import java.util.Iterator; import java.util.Set; import javax.jcr.AccessDeniedException; import javax.jcr.InvalidItemStateException; import javax.jcr.Item; import javax.jcr.ItemNotFoundException; import javax.jcr.ItemVisitor; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.PathNotFoundException; import javax.jcr.Property; import javax.jcr.RepositoryException; import javax.jcr.ValueFormatException; import javax.jcr.lock.Lock; import javax.jcr.lock.LockException; import javax.jcr.nodetype.ConstraintViolationException; import javax.jcr.version.OnParentVersionAction; import javax.jcr.version.VersionException; import org.modeshape.common.annotation.Immutable; import org.modeshape.common.annotation.NotThreadSafe; import org.modeshape.common.util.CheckArg; import org.modeshape.jcr.api.value.DateTime; import org.modeshape.jcr.cache.CachedNode; import org.modeshape.jcr.cache.MutableCachedNode; import org.modeshape.jcr.cache.NodeKey; import org.modeshape.jcr.cache.SessionCache; import org.modeshape.jcr.value.BinaryValue; import org.modeshape.jcr.value.Name; import org.modeshape.jcr.value.Path; import org.modeshape.jcr.value.PropertyFactory; import org.modeshape.jcr.value.Reference; import org.modeshape.jcr.value.ValueFactories; import org.modeshape.jcr.value.ValueFactory; import org.modeshape.jcr.value.basic.NodeKeyReference; /** * An abstract {@link Property JCR Property} implementation. */ @NotThreadSafe abstract class AbstractJcrProperty extends AbstractJcrItem implements org.modeshape.jcr.api.Property, Comparable<org.modeshape.jcr.api.Property> { @Immutable private final static class CachedDefinition { protected final PropertyDefinitionId propDefnId; protected final int nodeTypesVersion; protected CachedDefinition( PropertyDefinitionId propDefnId, int nodeTypesVersion ) { this.propDefnId = propDefnId; this.nodeTypesVersion = nodeTypesVersion; } } private final AbstractJcrNode node; private final Name name; private int propertyType; private volatile CachedDefinition cachedDefn; AbstractJcrProperty( AbstractJcrNode node, Name name, int propertyType ) { super(node.session()); assert node != null; assert name != null; this.node = node; this.name = name; this.propertyType = propertyType; } final void setPropertyDefinitionId( PropertyDefinitionId propDefnId, int nodeTypesVersion ) { this.cachedDefn = new CachedDefinition(propDefnId, nodeTypesVersion); } final void releasePropertyDefinitionId() { this.cachedDefn = null; } /** * Get the property definition ID. * * @return the cached property definition ID; never null * @throws ItemNotFoundException if the node that contains this property doesn't exist anymore * @throws ConstraintViolationException if no valid property definition could be found * @throws InvalidItemStateException if the node has been removed in this session's transient state */ final PropertyDefinitionId propertyDefinitionId() throws ItemNotFoundException, ConstraintViolationException, InvalidItemStateException { CachedDefinition defn = cachedDefn; NodeTypes nodeTypes = session.nodeTypes(); if (defn == null || nodeTypes.getVersion() > defn.nodeTypesVersion) { Name primaryType = node.getPrimaryTypeName(); Set<Name> mixinTypes = node.getMixinTypeNames(); PropertyDefinitionId id = node.propertyDefinitionFor(property(), primaryType, mixinTypes, nodeTypes).getId(); setPropertyDefinitionId(id, nodeTypes.getVersion()); return id; } return defn.propDefnId; } /** * Get the definition for this property. * * @return the cached property definition ID; never null * @throws ItemNotFoundException if the node that contains this property doesn't exist anymore * @throws ConstraintViolationException if no valid property definition could be found * @throws InvalidItemStateException if the node has been removed in this session's transient state */ final JcrPropertyDefinition propertyDefinition() throws ItemNotFoundException, ConstraintViolationException, InvalidItemStateException { CachedDefinition defn = cachedDefn; NodeTypes nodeTypes = session.nodeTypes(); if (defn == null || nodeTypes.getVersion() > defn.nodeTypesVersion) { Name primaryType = node.getPrimaryTypeName(); Set<Name> mixinTypes = node.getMixinTypeNames(); JcrPropertyDefinition propDefn = node.propertyDefinitionFor(property(), primaryType, mixinTypes, nodeTypes); PropertyDefinitionId id = propDefn.getId(); setPropertyDefinitionId(id, nodeTypes.getVersion()); return propDefn; } return nodeTypes.getPropertyDefinition(defn.propDefnId); } final CachedNode cachedNode() throws ItemNotFoundException, InvalidItemStateException { return node.node(); } final MutableCachedNode mutable() { return node.mutable(); } final SessionCache sessionCache() { return node.sessionCache(); } final PropertyFactory propertyFactory() { return node.session().propertyFactory(); } final org.modeshape.jcr.value.Property property() throws ItemNotFoundException, InvalidItemStateException { return cachedNode().getProperty(name, sessionCache()); } final JcrValue createValue( Object value ) { return new JcrValue(session().context().getValueFactories(), this.propertyType, value); } final JcrValue createValue( Object value, int propertyType ) throws ValueFormatException { try { return new JcrValue(session().context().getValueFactories(), propertyType, value); } catch (org.modeshape.jcr.value.ValueFormatException e) { throw new ValueFormatException(e); } } @Override public JcrSession getSession() { return node.getSession(); } /** * Checks that this property's parent node is not already locked by another session. If the parent node is not locked or the * parent node is locked but the lock is owned by this {@code Session}, this method completes silently. If the parent node is * locked (either directly or as part of a deep lock from an ancestor), this method throws a {@code LockException}. * * @throws LockException if the parent node of this property is locked (that is, if {@code getParent().isLocked() == true && * getParent().getLock().getLockToken() == null}. * @throws RepositoryException if any other error occurs * @see Node#isLocked() * @see Lock#getLockToken() */ protected final void checkForLock() throws LockException, RepositoryException { if (getParent().isLockedByAnotherSession()) { Lock parentLock = this.getParent().getLock(); if (parentLock != null && parentLock.getLockToken() == null) { throw new LockException(JcrI18n.lockTokenNotHeld.text(node.location())); } } } /** * Verifies that this node is either not versionable or that it is versionable but checked out. * * @throws VersionException if the node is versionable but is checked in and cannot be modified * @throws RepositoryException if there is an error accessing the repository */ protected final void checkForCheckedOut() throws VersionException, RepositoryException { if (!node.isCheckedOut()) { // Node is not checked out, so changing property is only allowed if OPV of property is 'ignore' ... JcrPropertyDefinition defn = getDefinition(); if (defn.getOnParentVersion() != OnParentVersionAction.IGNORE) { // Can't change this property ... String path = getParent().getPath(); throw new VersionException(JcrI18n.nodeIsCheckedIn.text(path)); } } } protected final void checkModifyPermission() throws AccessDeniedException { session.checkPermission(node, ModeShapePermissions.SET_PROPERTY); } @Override public final void accept( ItemVisitor visitor ) throws RepositoryException { CheckArg.isNotNull(visitor, "visitor"); checkSession(); visitor.visit(this); } final Name name() { return name; } @Override Path path() throws RepositoryException { return session().pathFactory().create(node.path(), name); } @Override public int getType() throws RepositoryException { checkSession(); return propertyType; } @Override public final JcrPropertyDefinition getDefinition() throws RepositoryException { checkSession(); return propertyDefinition(); } @Override public final String getName() { return name.getString(namespaces()); } @Override public final AbstractJcrNode getParent() { return node; } @Override public final String getPath() throws RepositoryException { return path().getString(namespaces()); } @Override public String getLocalName() { return name.getLocalName(); } @Override public String getNamespaceURI() { return name.getNamespaceUri(); } @Override public final boolean isModified() { try { checkSession(); CachedNode node = cachedNode(); return node instanceof MutableCachedNode && ((MutableCachedNode)node).isPropertyModified(sessionCache(), name); } catch (RepositoryException re) { throw new IllegalStateException(re); } } @Override public final boolean isNew() { try { checkSession(); CachedNode node = cachedNode(); return node instanceof MutableCachedNode && ((MutableCachedNode)node).isPropertyNew(sessionCache(), name); } catch (RepositoryException re) { throw new IllegalStateException(re); } } @Override public final boolean isNode() { return false; } @Override public final boolean isSame( Item otherItem ) throws RepositoryException { checkSession(); if (otherItem instanceof Property) { Property otherProperty = (Property)otherItem; // The nodes that own the properties must be the same ... if (!getParent().isSame(otherProperty.getParent())) return false; // The properties must have the same name ... return getName().equals(otherProperty.getName()); } return false; } @Override public void refresh( boolean keepChanges ) { throw new UnsupportedOperationException(); } @Override public void remove() throws VersionException, LockException, ConstraintViolationException, RepositoryException { checkSession(); checkNotProtected(); checkForLock(); checkForCheckedOut(); session.checkPermission(this, ModeShapePermissions.REMOVE); AbstractJcrNode parentNode = getParent(); if (parentNode.isLockedByAnotherSession()) { Lock parentLock = parentNode.getLock(); if (parentLock != null && !parentLock.isLockOwningSession()) { throw new LockException(JcrI18n.lockTokenNotHeld.text(getPath())); } } if (!parentNode.isCheckedOut()) { throw new VersionException(JcrI18n.nodeIsCheckedIn.text(getPath())); } node.removeProperty(this); } private void checkNotProtected() throws RepositoryException { if (this.getDefinition().isProtected()) { throw new ConstraintViolationException(JcrI18n.propertyIsProtected.text(getPath())); } } @Override public abstract JcrValue[] getValues() throws ValueFormatException, RepositoryException; @Override public abstract JcrValue getValue() throws ValueFormatException, RepositoryException; @SuppressWarnings( "deprecation" ) @Override public void save() throws RepositoryException { checkSession(); // This is not a correct implementation, but it's good enough to work around some TCK requirements for version tests // Plus, Item.save() has been removed from the JCR 2.0 spec (and deprecated in JCR 2.0's Java API). getParent().save(); } @Override public int compareTo( org.modeshape.jcr.api.Property that ) { if (that == this) return 0; try { return this.getName().compareTo(that.getName()); } catch (RepositoryException e) { throw new RuntimeException(e); } } protected Node valueToNode( Object value ) throws RepositoryException { ValueFactories factories = context().getValueFactories(); try { if (value instanceof Reference) { NodeKey key = null; if (value instanceof NodeKeyReference) { // REFERENCE and WEAKREFERENCE values are node keys ... key = ((NodeKeyReference)value).getNodeKey(); } else { throw new IllegalArgumentException("Unknown reference type: " + value.getClass().getSimpleName()); } return session().node(key, null); } else if (value instanceof String ) { String valueString = (String) value; // see MODE-2609 - some reference properties may have been incorrectly stored as strings, so try to resolve them if (NodeKey.isValidFormat(valueString)) { return session().node(new NodeKey(valueString), null); } } // STRING, PATH and NAME values will be convertable to a Path object ... Path path = factories.getPathFactory().create(value); return path.isAbsolute() ? session().node(path) : session().node(getParent().node(), path); } catch (org.modeshape.jcr.value.ValueFormatException e) { throw new ValueFormatException(e.getMessage(), e); } catch (PathNotFoundException pathNotFound) { //expected by the TCK throw new ItemNotFoundException(pathNotFound.getMessage(), pathNotFound); } } @Override public String toString() { ValueFactory<String> stringFactory = session().context().getValueFactories().getStringFactory(); StringBuilder sb = new StringBuilder(); try { org.modeshape.jcr.value.Property property = cachedNode().getProperty(name, sessionCache()); sb.append(getName()).append('='); if (isMultiple()) { sb.append('['); Iterator<?> iter = property.iterator(); while (iter.hasNext()) { Object value = iter.next(); appendValueToString(stringFactory, sb, value); if (iter.hasNext()) sb.append(','); } sb.append(']'); } else { Object value = property.getFirstValue(); appendValueToString(stringFactory, sb, value); } } catch (RepositoryException e) { // The node likely does not exist ... sb.append(" on deleted node ").append(node.key()); } return sb.toString(); } private void appendValueToString( ValueFactory<String> stringFactory, StringBuilder sb, Object value ) { if (value instanceof javax.jcr.Binary) { sb.append("**binary-value-not-shown**"); } else { sb.append(stringFactory.create(value)); } } @Override public <T> T getAs( Class<T> type, int index ) throws IndexOutOfBoundsException, ValueFormatException, RepositoryException { checkSession(); Object value = property().getValue(index); Object convertedValue = null; try { if (String.class.equals(type)) { convertedValue = context().getValueFactories().getStringFactory().create(value); } else if (Long.class.equals(type)) { convertedValue = context().getValueFactories().getLongFactory().create(value); } else if (Boolean.class.equals(type)) { convertedValue = context().getValueFactories().getBooleanFactory().create(value); } else if (Date.class.equals(type)) { Calendar calendar = context().getValueFactories().getDateFactory().create(value).toCalendar(); convertedValue = calendar.getTime(); } else if (Calendar.class.equals(type)) { convertedValue = context().getValueFactories().getDateFactory().create(value).toCalendar(); } else if (DateTime.class.equals(type)) { convertedValue = context().getValueFactories().getDateFactory().create(value); } else if (Double.class.equals(type)) { convertedValue = context().getValueFactories().getDoubleFactory().create(value); } else if (BigDecimal.class.equals(type)) { convertedValue = context().getValueFactories().getDecimalFactory().create(value); } else if (java.io.InputStream.class.equals(type)) { BinaryValue binary = context().getValueFactories().getBinaryFactory().create(value); convertedValue = binary.getStream(); } else if (javax.jcr.Binary.class.isAssignableFrom(type)) { convertedValue = context().getValueFactories().getBinaryFactory().create(value); } else if (Node.class.equals(type)) { convertedValue = valueToNode(value); } else if (NodeIterator.class.equals(type)) { convertedValue = new JcrSingleNodeIterator((AbstractJcrNode)getNode()); } else { throw new ValueFormatException(JcrI18n.unableToConvertPropertyValueAtIndexToType.text(getPath(), index, type)); } } catch (org.modeshape.jcr.value.ValueFormatException e) { throw new ValueFormatException(e); } return type.cast(convertedValue); } }