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