/*
* 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.core;
import static javax.jcr.PropertyType.BINARY;
import static javax.jcr.PropertyType.NAME;
import static javax.jcr.PropertyType.PATH;
import static javax.jcr.PropertyType.REFERENCE;
import static javax.jcr.PropertyType.STRING;
import static javax.jcr.PropertyType.UNDEFINED;
import static javax.jcr.PropertyType.WEAKREFERENCE;
import static org.apache.jackrabbit.core.ItemValidator.CHECK_CHECKED_OUT;
import static org.apache.jackrabbit.core.ItemValidator.CHECK_CONSTRAINTS;
import static org.apache.jackrabbit.core.ItemValidator.CHECK_HOLD;
import static org.apache.jackrabbit.core.ItemValidator.CHECK_LOCK;
import static org.apache.jackrabbit.core.ItemValidator.CHECK_RETENTION;
import java.io.InputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import javax.jcr.Binary;
import javax.jcr.InvalidItemStateException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.ItemVisitor;
import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.Value;
import javax.jcr.ValueFormatException;
import javax.jcr.lock.LockException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.PropertyDefinition;
import javax.jcr.version.VersionException;
import org.apache.jackrabbit.core.id.PropertyId;
import org.apache.jackrabbit.core.security.authorization.Permission;
import org.apache.jackrabbit.core.session.SessionContext;
import org.apache.jackrabbit.core.state.ItemState;
import org.apache.jackrabbit.core.state.ItemStateException;
import org.apache.jackrabbit.core.state.PropertyState;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.QPropertyDefinition;
import org.apache.jackrabbit.spi.commons.value.ValueFormat;
import org.apache.jackrabbit.spi.commons.nodetype.PropertyDefinitionImpl;
import org.apache.jackrabbit.value.ValueHelper;
import org.apache.commons.io.input.AutoCloseInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <code>PropertyImpl</code> implements the <code>Property</code> interface.
*/
public class PropertyImpl extends ItemImpl implements Property {
private static Logger log = LoggerFactory.getLogger(PropertyImpl.class);
/** property data (avoids casting <code>ItemImpl.data</code>) */
private final PropertyData data;
/**
* Package private constructor.
*
* @param itemMgr the <code>ItemManager</code> that created this <code>Property</code>
* @param sessionContext the component context of the associated session
* @param data the property data
*/
PropertyImpl(
ItemManager itemMgr, SessionContext sessionContext,
PropertyData data) {
super(itemMgr, sessionContext, data);
this.data = data;
// value will be read on demand
}
/**
* Checks that this property is valid (session not closed, property not
* removed, etc.) and returns the underlying property state if all is OK.
*
* @return property state
* @throws RepositoryException if the property is not valid
*/
private PropertyState getPropertyState() throws RepositoryException {
// JCR-1272: Need to get the state reference now so it
// doesn't get invalidated after the sanity check
ItemState state = getItemState();
sanityCheck();
return (PropertyState) state;
}
protected synchronized ItemState getOrCreateTransientItemState()
throws RepositoryException {
synchronized (data) {
if (!isTransient()) {
// make transient (copy-on-write)
try {
PropertyState transientState =
stateMgr.createTransientPropertyState(
data.getPropertyState(), ItemState.STATUS_EXISTING_MODIFIED);
// swap persistent with transient state
data.setState(transientState);
} catch (ItemStateException ise) {
String msg = "failed to create transient state";
log.debug(msg);
throw new RepositoryException(msg, ise);
}
}
return getItemState();
}
}
protected void makePersistent() throws InvalidItemStateException {
if (!isTransient()) {
log.debug(this + " (" + id + "): there's no transient state to persist");
return;
}
PropertyState transientState = data.getPropertyState();
PropertyState persistentState = (PropertyState) transientState.getOverlayedState();
if (persistentState == null) {
// this property is 'new'
persistentState = stateMgr.createNew(transientState);
}
synchronized (persistentState) {
// check staleness of transient state first
if (transientState.isStale()) {
String msg =
this + ": the property cannot be saved because it has"
+ " been modified externally.";
log.debug(msg);
throw new InvalidItemStateException(msg);
}
// copy state from transient state
persistentState.setType(transientState.getType());
persistentState.setMultiValued(transientState.isMultiValued());
persistentState.setValues(transientState.getValues());
// make state persistent
stateMgr.store(persistentState);
}
// tell state manager to disconnect item state
stateMgr.disconnectTransientItemState(transientState);
// swap transient state with persistent state
data.setState(persistentState);
// reset status
data.setStatus(STATUS_NORMAL);
}
protected void restoreTransient(PropertyState transientState)
throws RepositoryException {
PropertyState thisState = null;
if (!isTransient()) {
thisState = (PropertyState) getOrCreateTransientItemState();
if (transientState.getStatus() == ItemState.STATUS_NEW
&& thisState.getStatus() != ItemState.STATUS_NEW) {
thisState.setStatus(ItemState.STATUS_NEW);
stateMgr.disconnectTransientItemState(thisState);
}
} else {
// JCR-2503: Re-create transient state in the state manager,
// because it was removed
synchronized (data) {
try {
thisState = stateMgr.createTransientPropertyState(
transientState.getParentId(),
transientState.getName(),
PropertyState.STATUS_NEW);
data.setState(thisState);
} catch (ItemStateException e) {
throw new RepositoryException(e);
}
}
}
// reapply transient changes
thisState.setType(transientState.getType());
thisState.setMultiValued(transientState.isMultiValued());
thisState.setValues(transientState.getValues());
}
protected void onRedefine(QPropertyDefinition def) throws RepositoryException {
PropertyDefinitionImpl newDef =
sessionContext.getNodeTypeManager().getPropertyDefinition(def);
data.setDefinition(newDef);
}
/**
* Determines the length of the given value.
*
* @param value value whose length should be determined
* @return the length of the given value
* @throws RepositoryException if an error occurs
* @see javax.jcr.Property#getLength()
* @see javax.jcr.Property#getLengths()
*/
protected long getLength(InternalValue value) throws RepositoryException {
long length;
switch (value.getType()) {
case NAME:
case PATH:
String str = ValueFormat.getJCRString(value, sessionContext);
length = str.length();
break;
default:
length = value.getLength();
break;
}
return length;
}
/**
* Checks various pre-conditions that are common to all
* <code>setValue()</code> methods. The checks performed are:
* <ul>
* <li>parent node must be checked-out</li>
* <li>property must not be protected</li>
* <li>parent node must not be locked by somebody else</li>
* <li>property must be multi-valued when set to an array of values
* (and vice versa)</li>
* </ul>
*
* @param multipleValues flag indicating whether the property is about to
* be set to an array of values
* @throws ValueFormatException if a single-valued property is set to an
* array of values (and vice versa)
* @throws VersionException if the parent node is not checked-out
* @throws LockException if the parent node is locked by somebody else
* @throws ConstraintViolationException if the property is protected
* @throws RepositoryException if another error occurs
* @see javax.jcr.Property#setValue
*/
protected void checkSetValue(boolean multipleValues)
throws ValueFormatException, VersionException,
LockException, ConstraintViolationException,
RepositoryException {
NodeImpl parent = (NodeImpl) getParent();
// check multi-value flag
if (multipleValues != isMultiple()) {
String msg = (multipleValues) ?
"Single-valued property can not be set to an array of values:" :
"Multivalued property can not be set to a single value (an array of length one is OK): ";
throw new ValueFormatException(msg + this);
}
// check protected flag and for retention/hold
sessionContext.getItemValidator().checkModify(
this, CHECK_CONSTRAINTS, Permission.NONE);
// make sure the parent is checked-out and neither locked nor under retention
sessionContext.getItemValidator().checkModify(
parent,
CHECK_CHECKED_OUT | CHECK_LOCK | CHECK_HOLD | CHECK_RETENTION,
Permission.NONE);
}
/**
* @param values
* @param type
* @throws ConstraintViolationException
* @throws RepositoryException
*/
protected void internalSetValue(InternalValue[] values, int type)
throws ConstraintViolationException, RepositoryException {
// check for null value
if (values == null) {
// setting a property to null removes it automatically
((NodeImpl) getParent()).removeChildProperty(((PropertyId) id).getName());
return;
}
ArrayList<InternalValue> list = new ArrayList<InternalValue>();
// compact array (purge null entries)
for (InternalValue v : values) {
if (v != null) {
list.add(v);
}
}
values = list.toArray(new InternalValue[list.size()]);
// modify the state of this property
PropertyState thisState = (PropertyState) getOrCreateTransientItemState();
// free old values as necessary
InternalValue[] oldValues = thisState.getValues();
if (oldValues != null) {
for (int i = 0; i < oldValues.length; i++) {
InternalValue old = oldValues[i];
if (old != null && old.getType() == BINARY) {
// make sure temporarily allocated data is discarded
// before overwriting it
old.discard();
}
}
}
// set new values
thisState.setValues(values);
// set type
if (type == UNDEFINED) {
// fallback to default type
type = STRING;
}
thisState.setType(type);
}
/**
* Same as <code>{@link Property#setValue(String)}</code> except that
* this method takes a <code>Name</code> instead of a <code>String</code>
* value.
*
* @param name
* @throws ValueFormatException
* @throws VersionException
* @throws LockException
* @throws ConstraintViolationException
* @throws RepositoryException
*/
public void setValue(Name name)
throws ValueFormatException, VersionException,
LockException, ConstraintViolationException,
RepositoryException {
// check state of this instance
sanityCheck();
// check pre-conditions for setting property value
checkSetValue(false);
// check type according to definition of this property
final PropertyDefinition definition = data.getPropertyDefinition();
int reqType = definition.getRequiredType();
if (reqType == UNDEFINED) {
reqType = NAME;
}
if (name == null) {
internalSetValue(null, reqType);
return;
}
InternalValue internalValue;
if (reqType != NAME) {
// type conversion required
Value targetValue = ValueHelper.convert(
ValueFormat.getJCRValue(InternalValue.create(name), sessionContext, getSession().getValueFactory()),
reqType, getSession().getValueFactory());
internalValue = InternalValue.create(
targetValue, sessionContext, sessionContext.getDataStore());
} else {
// no type conversion required
internalValue = InternalValue.create(name);
}
internalSetValue(new InternalValue[]{internalValue}, reqType);
}
/**
* Same as <code>{@link Property#setValue(String[])}</code> except that
* this method takes an array of <code>Name</code> instead of
* <code>String</code> values.
*
* @param names
* @throws ValueFormatException
* @throws VersionException
* @throws LockException
* @throws ConstraintViolationException
* @throws RepositoryException
*/
public void setValue(Name[] names)
throws ValueFormatException, VersionException,
LockException, ConstraintViolationException,
RepositoryException {
// check state of this instance
sanityCheck();
// check pre-conditions for setting property value
checkSetValue(true);
// check type according to definition of this property
final PropertyDefinition definition = data.getPropertyDefinition();
int reqType = definition.getRequiredType();
if (reqType == UNDEFINED) {
reqType = NAME;
}
InternalValue[] internalValues = null;
// convert to internal values of correct type
if (names != null) {
internalValues = new InternalValue[names.length];
for (int i = 0; i < names.length; i++) {
Name name = names[i];
InternalValue internalValue = null;
if (name != null) {
if (reqType != NAME) {
// type conversion required
Value targetValue = ValueHelper.convert(
ValueFormat.getJCRValue(InternalValue.create(name), sessionContext, getSession().getValueFactory()),
reqType, getSession().getValueFactory());
internalValue = InternalValue.create(
targetValue, sessionContext,
sessionContext.getDataStore());
} else {
// no type conversion required
internalValue = InternalValue.create(name);
}
}
internalValues[i] = internalValue;
}
}
internalSetValue(internalValues, reqType);
}
/**
* {@inheritDoc}
*/
public Name getQName() {
return ((PropertyId) id).getName();
}
/**
* Returns the internal values of a multi-valued property.
*
* @return array of values
* @throws ValueFormatException if this property is not multi-valued
* @throws RepositoryException
*/
public InternalValue[] internalGetValues() throws RepositoryException {
final PropertyDefinition definition = data.getPropertyDefinition();
if (isMultiple()) {
return getPropertyState().getValues();
} else {
throw new ValueFormatException(
this + " is a single-valued property,"
+ " so it's value can not be retrieved as an array");
}
}
/**
* Returns the internal value of a single-valued property.
*
* @return value
* @throws ValueFormatException if this property is not single-valued
* @throws RepositoryException
*/
public InternalValue internalGetValue() throws RepositoryException {
if (isMultiple()) {
throw new ValueFormatException(
this + " is a multi-valued property,"
+ " so it's values can only be retrieved as an array");
} else {
InternalValue[] values = getPropertyState().getValues();
if (values.length > 0) {
return values[0];
} else {
// should never be the case, but being a little paranoid can't hurt...
throw new RepositoryException(this + ": single-valued property with no value");
}
}
}
//-------------------------------------------------------------< Property >
public Value[] getValues() throws RepositoryException {
InternalValue[] internals = internalGetValues();
Value[] values = new Value[internals.length];
for (int i = 0; i < internals.length; i++) {
values[i] = ValueFormat.getJCRValue(internals[i], sessionContext, getSession().getValueFactory());
}
return values;
}
public Value getValue() throws RepositoryException {
try {
return ValueFormat.getJCRValue(internalGetValue(), sessionContext, getSession().getValueFactory());
} catch (RuntimeException e) {
String msg = "Internal error while retrieving value of " + this;
log.error(msg, e);
throw new RepositoryException(msg, e);
}
}
/** Wrapper around {@link #getValue()} */
public String getString() throws RepositoryException {
return getValue().getString();
}
/** Wrapper around {@link #getValue()} */
public InputStream getStream() throws RepositoryException {
final Binary binary = getValue().getBinary();
// make sure binary is disposed after stream had been consumed
return new AutoCloseInputStream(binary.getStream()) {
public void close() throws IOException {
super.close();
binary.dispose();
}
};
}
/** Wrapper around {@link #getValue()} */
public long getLong() throws RepositoryException {
return getValue().getLong();
}
/** Wrapper around {@link #getValue()} */
public double getDouble() throws RepositoryException {
return getValue().getDouble();
}
/** Wrapper around {@link #getValue()} */
public Calendar getDate() throws RepositoryException {
return getValue().getDate();
}
/** Wrapper around {@link #getValue()} */
public boolean getBoolean() throws RepositoryException {
return getValue().getBoolean();
}
public Node getNode() throws ValueFormatException, RepositoryException {
Session session = getSession();
Value value = getValue();
int type = value.getType();
switch (type) {
case REFERENCE:
case WEAKREFERENCE:
return session.getNodeByUUID(value.getString());
case PATH:
case NAME:
String path = value.getString();
Path p = sessionContext.getQPath(path);
boolean absolute = p.isAbsolute();
try {
return (absolute) ? session.getNode(path) : getParent().getNode(path);
} catch (PathNotFoundException e) {
throw new ItemNotFoundException(path);
}
case STRING:
try {
Value refValue = ValueHelper.convert(value, REFERENCE, session.getValueFactory());
return session.getNodeByUUID(refValue.getString());
} catch (RepositoryException e) {
// try if STRING value can be interpreted as PATH value
Value pathValue = ValueHelper.convert(value, PATH, session.getValueFactory());
p = sessionContext.getQPath(pathValue.getString());
absolute = p.isAbsolute();
try {
return (absolute) ? session.getNode(pathValue.getString()) : getParent().getNode(pathValue.getString());
} catch (PathNotFoundException e1) {
throw new ItemNotFoundException(pathValue.getString());
}
}
default:
throw new ValueFormatException("Property value cannot be converted to a PATH, REFERENCE or WEAKREFERENCE");
}
}
public Property getProperty() throws RepositoryException {
Value value = getValue();
Value pathValue = ValueHelper.convert(value, PATH, getSession().getValueFactory());
String path = pathValue.getString();
boolean absolute;
try {
Path p = sessionContext.getQPath(path);
absolute = p.isAbsolute();
} catch (RepositoryException e) {
throw new ValueFormatException("Property value cannot be converted to a PATH");
}
try {
return (absolute) ? getSession().getProperty(path) : getParent().getProperty(path);
} catch (PathNotFoundException e) {
throw new ItemNotFoundException(path);
}
}
/** Wrapper around {@link #getValue()} */
public BigDecimal getDecimal() throws RepositoryException {
return getValue().getDecimal();
}
/** Wrapper around {@link #setValue(Value)} */
public void setValue(BigDecimal value) throws RepositoryException {
if (value != null) {
setValue(getValueFactory().createValue(value));
} else {
setValue((Value) null);
}
}
/** Wrapper around {@link #getValue()} */
public Binary getBinary() throws RepositoryException {
return getValue().getBinary();
}
/** Wrapper around {@link #setValue(Value)} */
public void setValue(Binary value) throws RepositoryException {
if (value != null) {
setValue(getValueFactory().createValue(value));
} else {
setValue((Value) null);
}
}
/** Wrapper around {@link #setValue(Value)} */
public void setValue(Calendar value) throws RepositoryException {
if (value != null) {
try {
setValue(getSession().getValueFactory().createValue(value));
} catch (IllegalArgumentException e) {
throw new ValueFormatException(
"Value is not an ISO8601 date: " + value, e);
}
} else {
setValue((Value) null);
}
}
/** Wrapper around {@link #setValue(Value)} */
public void setValue(double value) throws RepositoryException {
setValue(getValueFactory().createValue(value));
}
/** Wrapper around {@link #setValue(Value)} */
public void setValue(InputStream value) throws RepositoryException {
if (value != null) {
Binary binary = getValueFactory().createBinary(value);
try {
setValue(getValueFactory().createValue(binary));
} finally {
binary.dispose();
}
} else {
setValue((Value) null);
}
}
/** Wrapper around {@link #setValue(Value)} */
public void setValue(String value) throws RepositoryException {
if (value != null) {
setValue(getValueFactory().createValue(value));
} else {
setValue((Value) null);
}
}
/** Wrapper around {@link #setValue(Value[])} */
public void setValue(String[] strings) throws RepositoryException {
if (strings != null) {
setValue(getValues(strings, STRING));
} else {
setValue((Value[]) null);
}
}
/** Wrapper around {@link #setValue(Value)} */
public void setValue(boolean value) throws RepositoryException {
setValue(getValueFactory().createValue(value));
}
/** Wrapper around {@link #setValue(Value)} */
public void setValue(Node value) throws RepositoryException {
if (value != null) {
try {
setValue(getValueFactory().createValue(value));
} catch (UnsupportedRepositoryOperationException e) {
throw new ValueFormatException(
"Node is not referenceable: " + value, e);
}
} else {
setValue((Value) null);
}
}
/** Wrapper around {@link #setValue(Value)} */
public void setValue(long value) throws RepositoryException {
setValue(getValueFactory().createValue(value));
}
public synchronized void setValue(Value value)
throws ValueFormatException, VersionException,
LockException, ConstraintViolationException,
RepositoryException {
// check state of this instance
sanityCheck();
// check pre-conditions for setting property value
checkSetValue(false);
// check type according to definition of this property
final PropertyDefinition definition = data.getPropertyDefinition();
int reqType = definition.getRequiredType();
if (reqType == UNDEFINED) {
if (value != null) {
reqType = value.getType();
} else {
reqType = STRING;
}
}
if (value == null) {
internalSetValue(null, reqType);
return;
}
InternalValue internalValue;
if (reqType != value.getType()) {
// type conversion required
Value targetVal = ValueHelper.convert(
value, reqType, getSession().getValueFactory());
internalValue = InternalValue.create(
targetVal, sessionContext, sessionContext.getDataStore());
} else {
// no type conversion required
internalValue = InternalValue.create(
value, sessionContext, sessionContext.getDataStore());
}
internalSetValue(new InternalValue[]{internalValue}, reqType);
}
/**
* {@inheritDoc}
*/
public void setValue(Value[] values) throws RepositoryException {
setValue(values, UNDEFINED);
}
/**
* Sets the values of this property.
*
* @param values property values (possibly <code>null</code>)
* @param valueType default value type if not set in the node type,
* may be {@link PropertyType#UNDEFINED}
* @throws RepositoryException if the property values could not be set
*/
public void setValue(Value[] values, int valueType)
throws RepositoryException {
// check state of this instance
sanityCheck();
// check pre-conditions for setting property value
checkSetValue(true);
if (values != null) {
// check type of values
int firstValueType = UNDEFINED;
for (int i = 0; i < values.length; i++) {
if (values[i] != null) {
if (firstValueType == UNDEFINED) {
firstValueType = values[i].getType();
} else if (firstValueType != values[i].getType()) {
throw new ValueFormatException(
"inhomogeneous type of values");
}
}
}
}
final PropertyDefinition definition = data.getPropertyDefinition();
int reqType = definition.getRequiredType();
if (reqType == UNDEFINED) {
reqType = valueType; // use the given type as property type
}
InternalValue[] internalValues = null;
// convert to internal values of correct type
if (values != null) {
internalValues = new InternalValue[values.length];
// check type of values
for (int i = 0; i < values.length; i++) {
Value value = values[i];
if (value != null) {
if (reqType == UNDEFINED) {
// Use the type of the fist value as the type
reqType = value.getType();
}
if (reqType != value.getType()) {
value = ValueHelper.convert(
value, reqType, getSession().getValueFactory());
}
internalValues[i] = InternalValue.create(
value, sessionContext, sessionContext.getDataStore());
} else {
internalValues[i] = null;
}
}
}
internalSetValue(internalValues, reqType);
}
/**
* {@inheritDoc}
*/
public long getLength() throws RepositoryException {
return getLength(internalGetValue());
}
/**
* {@inheritDoc}
*/
public long[] getLengths() throws RepositoryException {
InternalValue[] values = internalGetValues();
long[] lengths = new long[values.length];
for (int i = 0; i < values.length; i++) {
lengths[i] = getLength(values[i]);
}
return lengths;
}
/**
* {@inheritDoc}
*/
public PropertyDefinition getDefinition() throws RepositoryException {
// check state of this instance
sanityCheck();
return data.getPropertyDefinition();
}
/**
* {@inheritDoc}
*/
public int getType() throws RepositoryException {
return getPropertyState().getType();
}
/**
* {@inheritDoc}
*/
public boolean isMultiple() throws RepositoryException {
// check state of this instance
sanityCheck();
return getPropertyState().isMultiValued();
}
//-----------------------------------------------------------------< Item >
/**
* {@inheritDoc}
*/
public boolean isNode() {
return false;
}
/**
* {@inheritDoc}
*/
public String getName() throws RepositoryException {
// check state of this instance
sanityCheck();
return sessionContext.getJCRName(((PropertyId) id).getName());
}
/**
* {@inheritDoc}
*/
public void accept(ItemVisitor visitor) throws RepositoryException {
// check state of this instance
sanityCheck();
visitor.visit(this);
}
/**
* {@inheritDoc}
*/
public Node getParent() throws RepositoryException {
return (Node) itemMgr.getItem(getPropertyState().getParentId());
}
//--------------------------------------------------------------< Object >
/**
* Return a string representation of this property for diagnostic purposes.
*
* @return "property /path/to/item"
*/
public String toString() {
return "property " + super.toString();
}
}