001/*
002 * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     Nuxeo - initial API and implementation
011 *
012 * $Id$
013 */
014
015package org.nuxeo.ecm.core.api.model.impl;
016
017import java.io.Serializable;
018import java.util.Collection;
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.Iterator;
022import java.util.Map;
023import java.util.Set;
024
025import org.nuxeo.ecm.core.api.PropertyException;
026import org.nuxeo.ecm.core.api.model.InvalidPropertyValueException;
027import org.nuxeo.ecm.core.api.model.Property;
028import org.nuxeo.ecm.core.api.model.PropertyConversionException;
029import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
030import org.nuxeo.ecm.core.api.model.PropertyVisitor;
031import org.nuxeo.ecm.core.api.model.ReadOnlyPropertyException;
032import org.nuxeo.ecm.core.schema.types.ComplexType;
033import org.nuxeo.ecm.core.schema.types.Field;
034
035/**
036 * A scalar property that is linked to a schema field
037 *
038 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
039 */
040public abstract class ComplexProperty extends AbstractProperty implements Map<String, Property> {
041
042    private static final long serialVersionUID = 1L;
043
044    protected Map<String, Property> children;
045
046    protected ComplexProperty(Property parent) {
047        super(parent);
048        children = new HashMap<String, Property>();
049    }
050
051    protected ComplexProperty(Property parent, int flags) {
052        super(parent, flags);
053        children = new HashMap<String, Property>();
054    }
055
056    /**
057     * Gets the property given its name. If the property was not set, returns null.
058     * <p>
059     * This method will always be called using a valid property name (a property specified by the schema). The returned
060     * property will be cached by its parent so the next time it is needed, it will be reused from the cache. That means
061     * this method servers as a initializer for properties - usually you create a new property and return it - you don't
062     * need to cache created properties.
063     * <p>
064     * If you want to change the way a property is fetched / stored, you must override this method.
065     *
066     * @return the child. Cannot return null
067     * @throws UnsupportedOperationException
068     */
069    protected Property internalGetChild(Field field) {
070        return null; // we don't store property that are not in the cache
071    }
072
073    @Override
074    public abstract ComplexType getType();
075
076    @Override
077    public boolean isNormalized(Object value) {
078        return value == null || value instanceof Map;
079    }
080
081    @Override
082    public Serializable normalize(Object value) throws PropertyConversionException {
083        if (isNormalized(value)) {
084            return (Serializable) value;
085        }
086        throw new PropertyConversionException(value.getClass(), Map.class, getPath());
087    }
088
089    @Override
090    public Property get(int index) {
091        throw new UnsupportedOperationException("accessing children by index is not allowed for complex properties");
092    }
093
094    public final Property getNonPhantomChild(Field field) {
095        String name = field.getName().getPrefixedName();
096        Property property = children.get(name);
097        if (property == null) {
098            property = internalGetChild(field);
099            if (property == null) {
100                return null;
101            }
102            children.put(name, property);
103        }
104        return property;
105    }
106
107    public final Property getChild(Field field) {
108        Property property = getNonPhantomChild(field);
109        if (property == null) {
110            property = getRoot().createProperty(this, field, IS_PHANTOM);
111            children.put(property.getName(), property); // cache it
112        }
113        return property;
114    }
115
116    public final Collection<Property> getNonPhantomChildren() {
117        ComplexType type = getType();
118        if (children.size() < type.getFieldsCount()) { // populate with
119                                                       // unloaded props only
120                                                       // if needed
121            for (Field field : type.getFields()) {
122                getNonPhantomChild(field); // force loading non phantom props
123            }
124        }
125        return Collections.unmodifiableCollection(children.values());
126    }
127
128    @Override
129    public Collection<Property> getChildren() {
130        ComplexType type = getType();
131        if (children.size() < type.getFieldsCount()) { // populate with
132                                                       // phantoms if needed
133            for (Field field : type.getFields()) {
134                getChild(field); // force loading all props including
135                                 // phantoms
136            }
137        }
138        return Collections.unmodifiableCollection(children.values());
139    }
140
141    @Override
142    public Property get(String name) throws PropertyNotFoundException {
143        Field field = getType().getField(name);
144        if (field == null) {
145            return null;
146        }
147        return getChild(field);
148    }
149
150    @Override
151    public Serializable internalGetValue() throws PropertyException {
152        // noinspection CollectionDeclaredAsConcreteClass
153        HashMap<String, Serializable> map = new HashMap<String, Serializable>();
154        for (Property property : getChildren()) {
155            map.put(property.getName(), property.getValue());
156        }
157        return map;
158    }
159
160    @Override
161    public Serializable getValueForWrite() throws PropertyException {
162        if (isPhantom() || isRemoved()) {
163            return getDefaultValue();
164        }
165        HashMap<String, Serializable> map = new HashMap<String, Serializable>();
166        for (Property property : getChildren()) {
167            map.put(property.getName(), property.getValueForWrite());
168        }
169        return map;
170    }
171
172    @Override
173    @SuppressWarnings("unchecked")
174    public void init(Serializable value) throws PropertyException {
175        if (value == null) { // IGNORE null values - properties will be
176                             // considered PHANTOMS
177            return;
178        }
179        Map<String, Serializable> map = (Map<String, Serializable>) value;
180        for (Entry<String, Serializable> entry : map.entrySet()) {
181            Property property = get(entry.getKey());
182            property.init(entry.getValue());
183        }
184        removePhantomFlag();
185    }
186
187    @Override
188    protected Serializable getDefaultValue() {
189        return new HashMap<String, Serializable>();
190    }
191
192    @Override
193    @SuppressWarnings("unchecked")
194    public void setValue(Object value) throws PropertyException {
195        if (!isContainer()) { // if not a container use default setValue()
196            super.setValue(value);
197            return;
198        }
199        if (isReadOnly()) {
200            throw new ReadOnlyPropertyException(getPath());
201        }
202        if (value == null) {
203            remove();
204            // completly clear this property
205            for (Property child : children.values()) {
206                child.remove();
207            }
208            return; // TODO how to treat nulls?
209        }
210        if (!(value instanceof Map)) {
211            throw new InvalidPropertyValueException(getPath());
212        }
213        Map<String, Object> map = (Map<String, Object>) value;
214        for (Entry<String, Object> entry : map.entrySet()) {
215            Property property = get(entry.getKey());
216            property.setValue(entry.getValue());
217        }
218    }
219
220    @Override
221    public Property addValue(Object value) {
222        throw new UnsupportedOperationException("add(value) operation not supported on map properties");
223    }
224
225    @Override
226    public Property addValue(int index, Object value) {
227        throw new UnsupportedOperationException("add(value, index) operation not supported on map properties");
228    }
229
230    @Override
231    public Property addEmpty() {
232        throw new UnsupportedOperationException("add() operation not supported on map properties");
233    }
234
235    public void visitChildren(PropertyVisitor visitor, Object arg) throws PropertyException {
236        boolean includePhantoms = visitor.acceptPhantoms();
237        if (includePhantoms) {
238            for (Property property : getChildren()) {
239                property.accept(visitor, arg);
240            }
241        } else {
242            for (Field field : getType().getFields()) {
243                Property property = getNonPhantomChild(field);
244                if (property == null) {
245                    continue; // a phantom property not yet initialized
246                } else if (property.isPhantom()) {
247                    continue; // a phantom property
248                } else {
249                    property.accept(visitor, arg);
250                }
251            }
252        }
253    }
254
255    /**
256     * Should be used by container properties. Non container props must overwrite this.
257     */
258    @Override
259    public boolean isSameAs(Property property) throws PropertyException {
260        if (!(property instanceof ComplexProperty)) {
261            return false;
262        }
263        ComplexProperty cp = (ComplexProperty) property;
264        if (isContainer()) {
265            if (!cp.isContainer()) {
266                return false;
267            }
268            Collection<Property> c1 = getNonPhantomChildren();
269            Collection<Property> c2 = cp.getNonPhantomChildren();
270            if (c1.size() != c2.size()) {
271                return false;
272            }
273            for (Property p : c1) {
274                Property child = cp.getNonPhantomChild(p.getField());
275                if (child == null) {
276                    return false;
277                }
278                if (!p.isSameAs(child)) {
279                    return false;
280                }
281            }
282        }
283        return true;
284    }
285
286    @Override
287    public Iterator<Property> getDirtyChildren() {
288        if (!isContainer()) {
289            throw new UnsupportedOperationException("Cannot iterate over children of scalar properties");
290        }
291        return new DirtyPropertyIterator(children.values().iterator());
292    }
293
294    /**
295     * Throws UnsupportedOperationException, added to implement List<Property> interface
296     */
297    @Override
298    public void clear() {
299        throw new UnsupportedOperationException();
300    }
301
302    /**
303     * Throws UnsupportedOperationException, added to implement List<Property> interface
304     */
305    @Override
306    public boolean containsKey(Object key) {
307        throw new UnsupportedOperationException();
308    }
309
310    /**
311     * Throws UnsupportedOperationException, added to implement List<Property> interface
312     */
313    @Override
314    public boolean containsValue(Object value) {
315        throw new UnsupportedOperationException();
316    }
317
318    @Override
319    public Set<Entry<String, Property>> entrySet() {
320        return children.entrySet();
321    }
322
323    @Override
324    public Property get(Object key) {
325        return children.get(key);
326    }
327
328    @Override
329    public boolean isEmpty() {
330        return children.isEmpty();
331    }
332
333    @Override
334    public Set<String> keySet() {
335        return children.keySet();
336    }
337
338    /**
339     * Throws UnsupportedOperationException, added to implement List<Property> interface
340     */
341    @Override
342    public Property put(String key, Property value) {
343        throw new UnsupportedOperationException();
344    }
345
346    /**
347     * Throws UnsupportedOperationException, added to implement List<Property> interface
348     */
349    @Override
350    public void putAll(Map<? extends String, ? extends Property> t) {
351        throw new UnsupportedOperationException();
352    }
353
354    /**
355     * Throws UnsupportedOperationException, added to implement List<Property> interface
356     */
357    @Override
358    public Property remove(Object key) {
359        throw new UnsupportedOperationException();
360    }
361
362    @Override
363    public Collection<Property> values() {
364        return children.values();
365    }
366
367    @Override
368    public void clearDirtyFlags() {
369        // even makes child properties not dirty
370        super.clearDirtyFlags();
371        for (Property child : children.values()) {
372            if (!child.isRemoved() && !child.isPhantom()) {
373                child.clearDirtyFlags();
374            }
375        }
376    }
377
378}