/* * 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.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; 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.PropertyVisitor; import org.eclipse.ecr.core.api.model.ReadOnlyPropertyException; import org.eclipse.ecr.core.schema.types.ComplexType; import org.eclipse.ecr.core.schema.types.Field; /** * A scalar property that is linked to a schema field * * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> * */ public abstract class ComplexProperty extends AbstractProperty implements Map<String, Property> { private static final long serialVersionUID = -8189463982083623237L; protected transient Map<String, Property> children; protected ComplexProperty(Property parent) { super(parent); children = new HashMap<String, Property>(); } protected ComplexProperty(Property parent, int flags) { super(parent, flags); children = new HashMap<String, Property>(); } /** * Gets the property given its name. If the property was not set, returns * null. * <p> * This method will always be called using a valid property name (a property * specified by the schema). The returned property will be cached by its * parent so the next time it is needed, it will be reused from the cache. * That means this method servers as a initializer for properties - usually * you create a new property and return it - you don't need to cache created * properties. * <p> * If you want to change the way a property is fetched / stored, you must * override this method. * * @return the child. Cannot return null * @throws UnsupportedOperationException */ protected Property internalGetChild(Field field) { return null; // we don't store property that are not in the cache } @Override public abstract ComplexType getType(); @Override public boolean isNormalized(Object value) { return value == null || value instanceof Map; } @Override public Serializable normalize(Object value) throws PropertyConversionException { if (isNormalized(value)) { return (Serializable) value; } throw new PropertyConversionException(value.getClass(), Map.class, getPath()); } @Override public Property get(int index) { throw new UnsupportedOperationException( "accessing children by index is not allowed for complex properties"); } public final Property getNonPhantomChild(Field field) { String name = field.getName().getPrefixedName(); Property property = children.get(name); if (property == null) { property = internalGetChild(field); if (property == null) { return null; } children.put(name, property); } return property; } public final Property getChild(Field field) { Property property = getNonPhantomChild(field); if (property == null) { property = getRoot().createProperty(this, field, isValidating() ? IS_VALIDATING | IS_PHANTOM : IS_PHANTOM); children.put(property.getName(), property); // cache it } return property; } public final Collection<Property> getNonPhantomChildren() { ComplexType type = getType(); if (children.size() < type.getFieldsCount()) { // populate with // unloaded props only // if needed for (Field field : type.getFields()) { getNonPhantomChild(field); // force loading non phantom props } } return Collections.unmodifiableCollection(children.values()); } @Override public Collection<Property> getChildren() { ComplexType type = getType(); if (children.size() < type.getFieldsCount()) { // populate with // phantoms if needed for (Field field : type.getFields()) { getChild(field); // force loading all props including // phantoms } } return Collections.unmodifiableCollection(children.values()); } @Override public Property get(String name) throws PropertyNotFoundException { Field field = getType().getField(name); if (field == null) { throw new PropertyNotFoundException(name, ""); } return getChild(field); } @Override public Serializable internalGetValue() throws PropertyException { // noinspection CollectionDeclaredAsConcreteClass HashMap<String, Serializable> map = new HashMap<String, Serializable>(); for (Property property : getChildren()) { map.put(property.getName(), property.getValue()); } return map; } @Override public Serializable getValueForWrite() throws PropertyException { if (isPhantom() || isRemoved()) { return getDefaultValue(); } HashMap<String, Serializable> map = new HashMap<String, Serializable>(); for (Property property : getChildren()) { map.put(property.getName(), property.getValueForWrite()); } return map; } @Override @SuppressWarnings("unchecked") public void init(Serializable value) throws PropertyException { if (value == null) { // IGNORE null values - properties will be // considered PHANTOMS return; } Map<String, Serializable> map = (Map<String, Serializable>) value; for (Entry<String, Serializable> entry : map.entrySet()) { Property property = get(entry.getKey()); property.init(entry.getValue()); } removePhantomFlag(); } @Override protected Serializable getDefaultValue() { return new HashMap<String, Serializable>(); } @Override @SuppressWarnings("unchecked") public void setValue(Object value) throws PropertyException { if (!isContainer()) { // if not a container use default setValue() super.setValue(value); return; } if (isReadOnly()) { throw new ReadOnlyPropertyException(getPath()); } if (value == null) { remove(); return; // TODO how to treat nulls? } if (!(value instanceof Map)) { throw new InvalidPropertyValueException(getPath()); } Map<String, Object> map = (Map<String, Object>) value; for (Entry<String, Object> entry : map.entrySet()) { Property property = get(entry.getKey()); property.setValue(entry.getValue()); } } @Override public Property addValue(Object value) { throw new UnsupportedOperationException( "add(value) operation not supported on map properties"); } @Override public Property addValue(int index, Object value) { throw new UnsupportedOperationException( "add(value, index) operation not supported on map properties"); } @Override public Property addEmpty() { throw new UnsupportedOperationException( "add() operation not supported on map properties"); } protected void visitChildren(PropertyVisitor visitor, Object arg) throws PropertyException { boolean includePhantoms = visitor.acceptPhantoms(); if (includePhantoms) { for (Property property : getChildren()) { property.accept(visitor, arg); } } else { for (Field field : getType().getFields()) { Property property = getNonPhantomChild(field); if (property == null) { continue; // a phantom property not yet initialized } else if (property.isPhantom()) { continue; // a phantom property } else { property.accept(visitor, arg); } } } } /** * Should be used by container properties. Non container props must * overwrite this. */ @Override public boolean isSameAs(Property property) throws PropertyException { if (!(property instanceof ComplexProperty)) { return false; } ComplexProperty cp = (ComplexProperty) property; if (isContainer()) { if (!cp.isContainer()) { return false; } Collection<Property> c1 = getNonPhantomChildren(); Collection<Property> c2 = cp.getNonPhantomChildren(); if (c1.size() != c2.size()) { return false; } for (Property p : c1) { Property child = cp.getNonPhantomChild(p.getField()); if (child == null) { return false; } if (!p.isSameAs(child)) { return false; } } } return true; } @Override public Iterator<Property> getDirtyChildren() { if (!isContainer()) { throw new UnsupportedOperationException( "Cannot iterate over children of scalar properties"); } return new DirtyPropertyIterator(children.values().iterator()); } /** * Throws UnsupportedOperationException, added to implement List<Property> * interface */ @Override public void clear() { throw new UnsupportedOperationException(); } /** * Throws UnsupportedOperationException, added to implement List<Property> * interface */ @Override public boolean containsKey(Object key) { throw new UnsupportedOperationException(); } /** * Throws UnsupportedOperationException, added to implement List<Property> * interface */ @Override public boolean containsValue(Object value) { throw new UnsupportedOperationException(); } @Override public Set<Entry<String, Property>> entrySet() { return children.entrySet(); } @Override public Property get(Object key) { return children.get(key); } @Override public boolean isEmpty() { return children.isEmpty(); } @Override public Set<String> keySet() { return children.keySet(); } /** * Throws UnsupportedOperationException, added to implement List<Property> * interface */ @Override public Property put(String key, Property value) { throw new UnsupportedOperationException(); } /** * Throws UnsupportedOperationException, added to implement List<Property> * interface */ @Override public void putAll(Map<? extends String, ? extends Property> t) { throw new UnsupportedOperationException(); } /** * Throws UnsupportedOperationException, added to implement List<Property> * interface */ @Override public Property remove(Object key) { throw new UnsupportedOperationException(); } @Override public Collection<Property> values() { return children.values(); } }