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, getXPath());
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 computeRemovedProperty(name);
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(getXPath());
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(getXPath());
219        }
220
221        if (getRoot().getClearComplexPropertyBeforeSet()) {
222            // completely clear this property before adding new values
223            for (Property child : children.values()) {
224                child.remove();
225            }
226            children.clear();
227        }
228
229        Map<String, Object> map = (Map<String, Object>) value;
230        for (Entry<String, Object> entry : map.entrySet()) {
231            Property property = get(entry.getKey());
232            if (property.isPhantom() && this.isNew()) {
233                // make sure complex list elements are rewritten
234                property.setForceDirty(true);
235            }
236            property.setValue(entry.getValue());
237        }
238        setValueDeprecation(value, false);
239    }
240
241    @Override
242    public Property addValue(Object value) {
243        throw new UnsupportedOperationException("add(value) operation not supported on map properties");
244    }
245
246    @Override
247    public Property addValue(int index, Object value) {
248        throw new UnsupportedOperationException("add(value, index) operation not supported on map properties");
249    }
250
251    @Override
252    public Property addEmpty() {
253        throw new UnsupportedOperationException("add() operation not supported on map properties");
254    }
255
256    public void visitChildren(PropertyVisitor visitor, Object arg) throws PropertyException {
257        boolean includePhantoms = visitor.acceptPhantoms();
258        if (includePhantoms) {
259            for (Property property : getChildren()) {
260                property.accept(visitor, arg);
261            }
262        } else {
263            for (Field field : getType().getFields()) {
264                Property property = getNonPhantomChild(field);
265                if (property == null) {
266                    continue; // a phantom property not yet initialized
267                } else if (property.isPhantom()) {
268                    continue; // a phantom property
269                } else {
270                    property.accept(visitor, arg);
271                }
272            }
273        }
274    }
275
276    /**
277     * Should be used by container properties. Non container props must overwrite this.
278     */
279    @Override
280    public boolean isSameAs(Property property) throws PropertyException {
281        if (!(property instanceof ComplexProperty)) {
282            return false;
283        }
284        ComplexProperty cp = (ComplexProperty) property;
285        if (isContainer()) {
286            if (!cp.isContainer()) {
287                return false;
288            }
289            Collection<Property> c1 = getNonPhantomChildren();
290            Collection<Property> c2 = cp.getNonPhantomChildren();
291            if (c1.size() != c2.size()) {
292                return false;
293            }
294            for (Property p : c1) {
295                Property child = cp.getNonPhantomChild(p.getField());
296                if (child == null) {
297                    return false;
298                }
299                if (!p.isSameAs(child)) {
300                    return false;
301                }
302            }
303        }
304        return true;
305    }
306
307    @Override
308    public Iterator<Property> getDirtyChildren() {
309        if (!isContainer()) {
310            throw new UnsupportedOperationException("Cannot iterate over children of scalar properties");
311        }
312        return new DirtyPropertyIterator(children.values().iterator());
313    }
314
315    /**
316     * Throws UnsupportedOperationException, added to implement List<Property> interface
317     */
318    @Override
319    public void clear() {
320        throw new UnsupportedOperationException();
321    }
322
323    /**
324     * Throws UnsupportedOperationException, added to implement List<Property> interface
325     */
326    @Override
327    public boolean containsKey(Object key) {
328        throw new UnsupportedOperationException();
329    }
330
331    /**
332     * Throws UnsupportedOperationException, added to implement List<Property> interface
333     */
334    @Override
335    public boolean containsValue(Object value) {
336        throw new UnsupportedOperationException();
337    }
338
339    @Override
340    public Set<Entry<String, Property>> entrySet() {
341        return children.entrySet();
342    }
343
344    @Override
345    public Property get(Object key) {
346        return children.get(key);
347    }
348
349    @Override
350    public boolean isEmpty() {
351        return children.isEmpty();
352    }
353
354    @Override
355    public Set<String> keySet() {
356        return children.keySet();
357    }
358
359    /**
360     * Throws UnsupportedOperationException, added to implement List<Property> interface
361     */
362    @Override
363    public Property put(String key, Property value) {
364        throw new UnsupportedOperationException();
365    }
366
367    /**
368     * Throws UnsupportedOperationException, added to implement List<Property> interface
369     */
370    @Override
371    public void putAll(Map<? extends String, ? extends Property> t) {
372        throw new UnsupportedOperationException();
373    }
374
375    /**
376     * Throws UnsupportedOperationException, added to implement List<Property> interface
377     */
378    @Override
379    public Property remove(Object key) {
380        throw new UnsupportedOperationException();
381    }
382
383    @Override
384    public Collection<Property> values() {
385        return children.values();
386    }
387
388    @Override
389    public void clearDirtyFlags() {
390        // even makes child properties not dirty
391        super.clearDirtyFlags();
392        for (Property child : children.values()) {
393            if (!child.isRemoved() && !child.isPhantom()) {
394                child.clearDirtyFlags();
395            }
396        }
397    }
398
399}