/*
* Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Nuxeo - initial API and implementation
*
* $Id$
*/
package org.eclipse.ecr.core.api.model.impl;
import java.io.Serializable;
import java.util.Iterator;
import org.eclipse.ecr.core.api.model.DocumentPart;
import org.eclipse.ecr.core.api.model.InvalidPropertyValueException;
import org.eclipse.ecr.core.api.model.Property;
import org.eclipse.ecr.core.api.model.PropertyConversionException;
import org.eclipse.ecr.core.api.model.PropertyException;
import org.eclipse.ecr.core.api.model.PropertyNotFoundException;
import org.eclipse.ecr.core.api.model.ReadOnlyPropertyException;
import org.eclipse.ecr.core.schema.types.Schema;
import org.nuxeo.common.utils.Path;
/**
* @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
*
*/
public abstract class AbstractProperty implements Property {
private static final long serialVersionUID = -4689902294497020548L;
/**
* Whether or not this property is read only.
*/
public static final int IS_READONLY = 32;
/**
* Whether or not this property is validating values when they are set.
*/
public static final int IS_VALIDATING = 64;
/**
* Whether or not the data field contains keyed data.
*/
public static final int KEYED_DATA = 128;
protected final Property parent;
protected int flags;
protected Object data;
protected AbstractProperty(Property parent) {
this.parent = parent;
}
protected AbstractProperty(Property parent, int flags) {
this.parent = parent;
this.flags = flags;
}
/**
* Sets the given normalized value.
* <p>
* This applies only for nodes that physically store a value (that means non
* container nodes). Container nodes does nothing.
*
* @param value
*/
public abstract void internalSetValue(Serializable value)
throws PropertyException;
public abstract Serializable internalGetValue() throws PropertyException;
@Override
public void init(Serializable value) throws PropertyException {
if (value == null) { // IGNORE null values - properties will be
// considered PHANTOMS
return;
}
internalSetValue(value);
removePhantomFlag();
}
public void removePhantomFlag() {
flags &= ~IS_PHANTOM;
if (parent != null) {
((AbstractProperty) parent).removePhantomFlag();
}
}
@Override
public void setValue(int index, Object value) throws PropertyException {
Property property = get(index);
property.setValue(value);
}
@Override
public int size() {
return getChildren().size();
}
@Override
public Iterator<Property> iterator() {
return getChildren().iterator();
}
@Override
public Serializable remove() throws PropertyException {
Serializable value = getValue();
if (parent != null && parent.isList()) { // remove from list is
// handled separately
ListProperty list = (ListProperty) parent;
list.remove(this);
} else if (!isPhantom()) { // remove from map is easier -> mark the
// field as removed and remove the value
init(null);
setIsRemoved();
}
return value;
}
@Override
public Property getParent() {
return parent;
}
@Override
public String getPath() {
Path path = collectPath(new Path("/"));
return path.toString();
}
protected Path collectPath(Path path) {
String name = getName();
if (parent != null) {
if (parent.isList()) {
int i = ((ListProperty) parent).children.indexOf(this);
name = name + '[' + i + ']';
}
path = ((AbstractProperty) parent).collectPath(path);
}
return path.append(name);
}
@Override
public Schema getSchema() {
return getRoot().getSchema();
}
@Override
public boolean isList() {
return getType().isListType();
}
@Override
public boolean isComplex() {
return getType().isComplexType();
}
@Override
public boolean isScalar() {
return getType().isSimpleType();
}
@Override
public boolean isNew() {
return areFlagsSet(IS_NEW);
}
@Override
public boolean isRemoved() {
return areFlagsSet(IS_REMOVED);
}
@Override
public boolean isMoved() {
return areFlagsSet(IS_MOVED);
}
@Override
public boolean isModified() {
return areFlagsSet(IS_MODIFIED);
}
@Override
public boolean isPhantom() {
return areFlagsSet(IS_PHANTOM);
}
@Override
public final boolean isDirty() {
return (flags & IS_DIRTY) != 0;
}
protected final void setDirtyFlags(int dirtyFlags) {
flags = dirtyFlags & DIRTY_MASK | flags & ~DIRTY_MASK;
}
protected final void appendDirtyFlags(int dirtyFlags) {
flags |= (dirtyFlags & DIRTY_MASK);
}
@Override
public boolean isValidating() {
return areFlagsSet(IS_VALIDATING);
}
@Override
public boolean isReadOnly() {
return areFlagsSet(IS_READONLY);
}
@Override
public void setReadOnly(boolean value) {
if (value) {
setFlags(IS_READONLY);
} else {
clearFlags(IS_READONLY);
}
}
@Override
public void setValidating(boolean value) {
if (value) {
setFlags(IS_VALIDATING);
} else {
clearFlags(IS_VALIDATING);
}
}
public final boolean areFlagsSet(long flags) {
return (this.flags & flags) != 0;
}
public final void setFlags(long flags) {
this.flags |= flags;
}
public final void clearFlags(long flags) {
this.flags &= ~flags;
}
@Override
public int getDirtyFlags() {
return flags & DIRTY_MASK;
}
@Override
public void clearDirtyFlags() {
if ((flags & IS_REMOVED) != 0) {
// if is removed the property becomes a phantom
setDirtyFlags(IS_PHANTOM);
} else {
setDirtyFlags(NONE);
}
}
/**
* This method is public because of DataModelImpl which use it.
* <p>
* TODO after removing DataModelImpl make it protected.
*/
public void setIsModified() {
if ((flags & IS_MODIFIED) == 0) { // if not already modified
// clear dirty + phatom flag if any
flags |= IS_MODIFIED; // set the modified flag
flags &= ~IS_PHANTOM; // remove phantom flag if any
if (parent != null) {
((AbstractProperty) parent).setIsModified();
}
}
}
protected void setIsNew() {
if (isDirty()) {
throw new IllegalStateException(
"Cannot set IS_NEW flag on a dirty property");
}
// clear dirty + phantom flag if any
setDirtyFlags(IS_NEW); // this clear any dirty flag and set the new
// flag
if (parent != null) {
((AbstractProperty) parent).setIsModified();
}
}
protected void setIsRemoved() {
if (isPhantom() || parent == null || parent.isList()) {
throw new IllegalStateException(
"Cannot set IS_REMOVED on removed or properties that are not map elements");
}
if ((flags & IS_REMOVED) == 0) { // if not already removed
// clear dirty + phatom flag if any
setDirtyFlags(IS_REMOVED);
((AbstractProperty) parent).setIsModified();
}
}
protected void setIsMoved() {
if (parent == null || !parent.isList()) {
throw new IllegalStateException(
"Cannot set IS_MOVED on removed or properties that are not map elements");
}
if ((flags & IS_MOVED) == 0) {
flags |= IS_MOVED;
((AbstractProperty) parent).setIsModified();
}
}
@Override
public <T> T getValue(Class<T> type) throws PropertyException {
return convertTo(getValue(), type);
}
@Override
public void setValue(Object value) throws PropertyException {
// 1. check the read only flag
if (isReadOnly()) {
throw new ReadOnlyPropertyException(getPath());
}
// 1. normalize the value
Serializable normalizedValue = normalize(value);
// 2. validate if needed
if (areFlagsSet(IS_VALIDATING)) {
if (!validate(normalizedValue)) {
throw new InvalidPropertyValueException(
"validating failed for " + normalizedValue);
}
}
// 3. set the normalized value
internalSetValue(normalizedValue);
// internalSetValue((Serializable)value);
// 4. update flags
setIsModified();
}
@Override
public void setValue(String path, Object value) throws PropertyException {
resolvePath(path).setValue(value);
}
@Override
public <T> T getValue(Class<T> type, String path) throws PropertyException {
return resolvePath(path).getValue(type);
}
@Override
public Serializable getValue(String path) throws PropertyException {
return resolvePath(path).getValue();
}
@Override
public Serializable getValue() throws PropertyException {
if (isPhantom() || isRemoved()) {
return getDefaultValue();
}
return internalGetValue();
}
@Override
public Serializable getValueForWrite() throws PropertyException {
return getValue();
}
protected Serializable getDefaultValue() {
return (Serializable) getField().getDefaultValue();
}
@Override
public void moveTo(int index) {
if (parent == null || !parent.isList()) {
throw new UnsupportedOperationException("Not a list item property");
}
ListProperty list = (ListProperty) parent;
if (list.moveTo(this, index)) {
setIsMoved();
}
}
@Override
public DocumentPart getRoot() {
return parent == null ? (DocumentPart) this : parent.getRoot();
}
@Override
public Property resolvePath(String path) throws PropertyNotFoundException {
return resolvePath(new Path(path));
}
@Override
public Property resolvePath(Path path) throws PropertyNotFoundException {
// handle absolute paths -> resolve them relative to the root
if (path.isAbsolute()) {
return getRoot().resolvePath(path.makeRelative());
}
String[] segments = path.segments();
// handle ../../ paths
Property property = this;
int start = 0;
for (; start < segments.length; start++) {
if (segments[start].equals("..")) {
property = property.getParent();
} else {
break;
}
}
// now start resolving the path from 'start' depth relative to
// 'property'
for (int i = start; i < segments.length; i++) {
String segment = segments[i];
if (property.isScalar()) {
throw new PropertyNotFoundException(path.toString(), "segment "
+ segment + " points to a scalar property");
}
String index = null;
if (segment.endsWith("]")) {
int p = segment.lastIndexOf('[');
if (p == -1) {
throw new PropertyNotFoundException(path.toString(),
"Parse error: no matching '[' was found");
}
index = segment.substring(p + 1, segment.length() - 1);
segment = segment.substring(0, p);
}
if (index == null) {
property = property.get(segment);
if (property == null) {
throw new PropertyNotFoundException(path.toString(),
"segment " + segments[i] + " cannot be resolved");
}
} else {
property = property.get(index);
}
}
return property;
}
@Override
public Serializable normalize(Object value)
throws PropertyConversionException {
if (isNormalized(value)) {
return (Serializable) value;
}
throw new PropertyConversionException(value.getClass(),
Serializable.class, getPath());
}
@Override
public boolean isNormalized(Object value) {
return value == null || value instanceof Serializable;
}
@Override
public <T> T convertTo(Serializable value, Class<T> toType)
throws PropertyConversionException {
// TODO FIXME XXX make it abstract at this level
throw new UnsupportedOperationException("Not implemented");
}
@Override
public boolean validateType(Class<?> type) {
return true; // TODO XXX FIXME
}
@Override
public boolean validate(Serializable value) {
return true; // TODO XXX FIXME
}
@Override
public Object newInstance() {
return null; // TODO XXX FIXME
}
@Override
public String toString() {
return getClass().getSimpleName() + '(' + getPath() + ')';
}
// @Override
// public boolean equals(Object obj) {
// if (obj == this) return true;
// if (obj instanceof Property) {
// Property p = (Property)obj;
// return field.equals(p.getField()) && value.equals(p.value);
// }
// return false;
// }
/**
* application data impl. was copied from eclipse Widget class
*/
@Override
public Object getData() {
return (flags & KEYED_DATA) != 0 ? ((Object[]) data)[0] : data;
}
@Override
public Object getData(String key) {
if (key == null) {
throw new IllegalArgumentException("Data Key must not be null");
}
if ((flags & KEYED_DATA) != 0) {
Object[] table = (Object[]) data;
for (int i = 1; i < table.length; i += 2) {
if (key.equals(table[i])) {
return table[i + 1];
}
}
}
return null;
}
@Override
public void setData(Object value) {
if ((flags & KEYED_DATA) != 0) {
((Object[]) data)[0] = value;
} else {
data = value;
}
}
@Override
public void setData(String key, Object value) {
if (key == null) {
throw new IllegalArgumentException("Data Key must not be null");
}
int index = 1;
Object[] table = null;
if ((flags & KEYED_DATA) != 0) {
table = (Object[]) data;
while (index < table.length) {
if (key.equals(table[index])) {
break;
}
index += 2;
}
}
if (value != null) {
if ((flags & KEYED_DATA) != 0) {
if (index == table.length) {
Object[] newTable = new Object[table.length + 2];
System.arraycopy(table, 0, newTable, 0, table.length);
data = table = newTable;
}
} else {
table = new Object[3];
table[0] = data;
data = table;
flags |= KEYED_DATA;
}
table[index] = key;
table[index + 1] = value;
} else {
if ((flags & KEYED_DATA) != 0) {
if (index != table.length) {
int length = table.length - 2;
if (length == 1) {
data = table[0];
flags &= ~KEYED_DATA;
} else {
Object[] newTable = new Object[length];
System.arraycopy(table, 0, newTable, 0, index);
System.arraycopy(table, index + 2, newTable, index,
length - index);
data = newTable;
}
}
}
}
}
}