/* * (C) Copyright 2006-2014 Nuxeo SA (http://nuxeo.com/) and others. * * 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. * * Contributors: * Bogdan Stefanescu * Florent Guillaume */ package org.nuxeo.ecm.core.api.model.impl; import java.io.Serializable; import java.util.Iterator; import org.nuxeo.common.utils.Path; import org.nuxeo.ecm.core.api.PropertyException; import org.nuxeo.ecm.core.api.model.DocumentPart; import org.nuxeo.ecm.core.api.model.Property; import org.nuxeo.ecm.core.api.model.PropertyConversionException; import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; import org.nuxeo.ecm.core.api.model.ReadOnlyPropertyException; import org.nuxeo.ecm.core.api.model.resolver.PropertyObjectResolver; import org.nuxeo.ecm.core.api.model.resolver.PropertyObjectResolverImpl; import org.nuxeo.ecm.core.schema.types.Schema; import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolver; public abstract class AbstractProperty implements Property { private static final long serialVersionUID = 1L; /** * Whether or not this property is read only. */ public static final int IS_READONLY = 32; public final Property parent; /** * for SimpleDocumentModel uses */ public boolean forceDirty = false; protected int flags; 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 || (value instanceof Object[] && ((Object[]) value).length == 0)) { // ignore null or empty 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 // do not remove the field if the previous value was null, except if its a property from a SimpleDocumentModel (forceDirty mode) Serializable previous = internalGetValue(); init(null); if (previous != null || isForceDirty()) { setIsRemoved(); } } return value; } @Override public Property getParent() { return parent; } @Override public String getXPath() { StringBuilder buf = new StringBuilder(); getXPath(buf); return buf.toString(); } protected void getXPath(StringBuilder buf) { if (parent != null) { ((AbstractProperty) parent).getXPath(buf); if (parent.isList()) { buf.append('/'); int i = ((ListProperty) parent).children.indexOf(this); buf.append(i); } else { if (buf.length() != 0) { buf.append('/'); } buf.append(getName()); } } } @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 isReadOnly() { return areFlagsSet(IS_READONLY); } @Override public void setReadOnly(boolean value) { if (value) { setFlags(IS_READONLY); } else { clearFlags(IS_READONLY); } } 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(getXPath()); } // 1. normalize the value Serializable normalizedValue = normalize(value); // 2. backup the current Serializable current = internalGetValue(); // if its a phantom, no need to check for changes, set it dirty if (!isSameValue(normalizedValue, current) || isForceDirty()) { // 3. set the normalized value and internalSetValue(normalizedValue); // 4. update flags setIsModified(); } else { removePhantomFlag(); } } protected boolean isSameValue(Serializable value1, Serializable value2) { return ((value1 == null && value2 == null) || (value1 != null && value1.equals(value2))); } @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, getXPath()); } @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 Object newInstance() { return null; // TODO XXX FIXME } @Override public String toString() { return getClass().getSimpleName() + '(' + getXPath() + ')'; } @Override public PropertyObjectResolver getObjectResolver() { ObjectResolver resolver = getType().getObjectResolver(); if (resolver != null) { return new PropertyObjectResolverImpl(this, resolver); } return null; } @Override public boolean isForceDirty() { return forceDirty; } @Override public void setForceDirty(boolean forceDirty) { this.forceDirty = forceDirty; } }