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