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<>();
056    }
057
058    protected ComplexProperty(Property parent, int flags) {
059        super(parent, flags);
060        children = new HashMap<>();
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     */
075    protected Property internalGetChild(Field field) {
076        return null; // we don't store property that are not in the cache
077    }
078
079    @Override
080    public abstract ComplexType getType();
081
082    @Override
083    public boolean isNormalized(Object value) {
084        return value == null || value instanceof Map;
085    }
086
087    @Override
088    public Serializable normalize(Object value) throws PropertyConversionException {
089        if (isNormalized(value)) {
090            return (Serializable) value;
091        }
092        throw new PropertyConversionException(value.getClass(), Map.class, getXPath());
093    }
094
095    @Override
096    public Property get(int index) {
097        throw new UnsupportedOperationException("accessing children by index is not allowed for complex properties");
098    }
099
100    public final Property getNonPhantomChild(Field field) {
101        String name = field.getName().getPrefixedName();
102        Property property = children.get(name);
103        if (property == null) {
104            property = internalGetChild(field);
105            if (property == null) {
106                return null;
107            }
108            children.put(name, property);
109        }
110        return property;
111    }
112
113    public final Property getChild(Field field) {
114        Property property = getNonPhantomChild(field);
115        if (property == null) {
116            property = getRoot().createProperty(this, field, IS_PHANTOM);
117            children.put(property.getName(), property); // cache it
118        }
119        return property;
120    }
121
122    public final Collection<Property> getNonPhantomChildren() {
123        ComplexType type = getType();
124        if (children.size() < type.getFieldsCount()) { // populate with
125                                                       // unloaded props only
126                                                       // if needed
127            for (Field field : type.getFields()) {
128                getNonPhantomChild(field); // force loading non phantom props
129            }
130        }
131        return Collections.unmodifiableCollection(children.values());
132    }
133
134    @Override
135    public Collection<Property> getChildren() {
136        ComplexType type = getType();
137        if (children.size() < type.getFieldsCount()) { // populate with
138                                                       // phantoms if needed
139            for (Field field : type.getFields()) {
140                getChild(field); // force loading all props including
141                                 // phantoms
142            }
143        }
144        return Collections.unmodifiableCollection(children.values());
145    }
146
147    @Override
148    public Property get(String name) throws PropertyNotFoundException {
149        Field field = getType().getField(name);
150        if (field == null) {
151            return computeRemovedProperty(name);
152        }
153        return getChild(field);
154    }
155
156    @Override
157    public void set(String name, Property property) throws PropertyNotFoundException {
158        Field field = getType().getField(name);
159        if (field == null) {
160            Property removedProperty = computeRemovedProperty(name);
161            if (removedProperty != null) {
162                removedProperty.set(name, property);
163            }
164            return;
165        }
166        children.put(property.getName(), property);
167        setIsModified();
168    }
169
170    @Override
171    public Serializable internalGetValue() throws PropertyException {
172        var map = new HashMap<String, Serializable>();
173        for (Property property : getChildren()) {
174            map.put(property.getName(), property.getValue());
175        }
176        return map;
177    }
178
179    @Override
180    public Serializable getValueForWrite() throws PropertyException {
181        if (isPhantom() || isRemoved()) {
182            return getDefaultValue();
183        }
184        HashMap<String, Serializable> map = new HashMap<>();
185        for (Property property : getChildren()) {
186            map.put(property.getName(), property.getValueForWrite());
187        }
188        return map;
189    }
190
191    @Override
192    @SuppressWarnings("unchecked")
193    public void init(Serializable value) throws PropertyException {
194        if (value == null) { // IGNORE null values - properties will be
195                             // considered PHANTOMS
196            return;
197        }
198        Map<String, Serializable> map = (Map<String, Serializable>) value;
199        for (Entry<String, Serializable> entry : map.entrySet()) {
200            Property property = get(entry.getKey());
201            property.init(entry.getValue());
202        }
203        removePhantomFlag();
204    }
205
206    @Override
207    protected Serializable getDefaultValue() {
208        return new HashMap<String, Serializable>();
209    }
210
211    @Override
212    @SuppressWarnings("unchecked")
213    public void setValue(Object value) throws PropertyException {
214        if (!isContainer()) { // if not a container use default setValue()
215            super.setValue(value);
216            return;
217        }
218        if (isReadOnly() || isSecuredForContext()) {
219            throw new ReadOnlyPropertyException(
220                    String.format("Cannot set the value of property: %s since it is readonly", getXPath()));
221        }
222        if (value == null) {
223            remove();
224            // completly clear this property
225            for (Property child : children.values()) {
226                child.remove();
227            }
228            return; // TODO how to treat nulls?
229        }
230        if (!(value instanceof Map)) {
231            throw new InvalidPropertyValueException(getXPath());
232        }
233
234        if (getRoot().getClearComplexPropertyBeforeSet()) {
235            // completely clear this property before adding new values
236            for (Property child : children.values()) {
237                child.remove();
238            }
239            children.clear();
240        }
241
242        Map<String, Object> map = (Map<String, Object>) value;
243        for (Entry<String, Object> entry : map.entrySet()) {
244            Property property = get(entry.getKey());
245            if (property.isPhantom() && this.isNew()) {
246                // make sure complex list elements are rewritten
247                property.setForceDirty(true);
248            }
249            property.setValue(entry.getValue());
250        }
251        setValueDeprecation(value, false);
252    }
253
254    @Override
255    public Property addValue(Object value) {
256        throw new UnsupportedOperationException("add(value) operation not supported on map properties");
257    }
258
259    @Override
260    public Property addValue(int index, Object value) {
261        throw new UnsupportedOperationException("add(value, index) operation not supported on map properties");
262    }
263
264    @Override
265    public Property addEmpty() {
266        throw new UnsupportedOperationException("add() operation not supported on map properties");
267    }
268
269    public void visitChildren(PropertyVisitor visitor, Object arg) throws PropertyException {
270        boolean includePhantoms = visitor.acceptPhantoms();
271        if (includePhantoms) {
272            for (Property property : getChildren()) {
273                property.accept(visitor, arg);
274            }
275        } else {
276            for (Field field : getType().getFields()) {
277                Property property = getNonPhantomChild(field);
278                if (property == null) {
279                    continue; // a phantom property not yet initialized
280                } else if (property.isPhantom()) {
281                    continue; // a phantom property
282                } else {
283                    property.accept(visitor, arg);
284                }
285            }
286        }
287    }
288
289    /**
290     * Should be used by container properties. Non container props must overwrite this.
291     */
292    @Override
293    public boolean isSameAs(Property property) throws PropertyException {
294        if (!(property instanceof ComplexProperty)) {
295            return false;
296        }
297        ComplexProperty cp = (ComplexProperty) property;
298        if (isContainer()) {
299            if (!cp.isContainer()) {
300                return false;
301            }
302            Collection<Property> c1 = getNonPhantomChildren();
303            Collection<Property> c2 = cp.getNonPhantomChildren();
304            if (c1.size() != c2.size()) {
305                return false;
306            }
307            for (Property p : c1) {
308                Property child = cp.getNonPhantomChild(p.getField());
309                if (child == null) {
310                    return false;
311                }
312                if (!p.isSameAs(child)) {
313                    return false;
314                }
315            }
316        }
317        return true;
318    }
319
320    @Override
321    public Iterator<Property> getDirtyChildren() {
322        if (!isContainer()) {
323            throw new UnsupportedOperationException("Cannot iterate over children of scalar properties");
324        }
325        return new DirtyPropertyIterator(children.values().iterator());
326    }
327
328    /**
329     * Throws UnsupportedOperationException, added to implement Map&lt;String, Property&gt; interface.
330     */
331    @Override
332    public void clear() {
333        throw new UnsupportedOperationException();
334    }
335
336    /**
337     * Throws UnsupportedOperationException, added to implement Map&lt;String, Property&gt; interface.
338     */
339    @Override
340    public boolean containsKey(Object key) {
341        throw new UnsupportedOperationException();
342    }
343
344    /**
345     * Throws UnsupportedOperationException, added to implement Map&lt;String, Property&gt; interface.
346     */
347    @Override
348    public boolean containsValue(Object value) {
349        throw new UnsupportedOperationException();
350    }
351
352    @Override
353    public Set<Entry<String, Property>> entrySet() {
354        return children.entrySet();
355    }
356
357    @Override
358    public Property get(Object key) {
359        return children.get(key);
360    }
361
362    @Override
363    public boolean isEmpty() {
364        return children.isEmpty();
365    }
366
367    @Override
368    public Set<String> keySet() {
369        return children.keySet();
370    }
371
372    /**
373     * Throws UnsupportedOperationException, added to implement Map&lt;String, Property&gt; interface.
374     */
375    @Override
376    public Property put(String key, Property value) {
377        throw new UnsupportedOperationException();
378    }
379
380    /**
381     * Throws UnsupportedOperationException, added to implement Map&lt;String, Property&gt; interface.
382     */
383    @Override
384    public void putAll(Map<? extends String, ? extends Property> t) {
385        throw new UnsupportedOperationException();
386    }
387
388    /**
389     * Throws UnsupportedOperationException, added to implement Map&lt;String, Property&gt; interface.
390     */
391    @Override
392    public Property remove(Object key) {
393        throw new UnsupportedOperationException();
394    }
395
396    @Override
397    public Collection<Property> values() {
398        return children.values();
399    }
400
401    @Override
402    public void clearDirtyFlags() {
403        // even makes child properties not dirty
404        super.clearDirtyFlags();
405        for (Property child : children.values()) {
406            if (!child.isRemoved() && !child.isPhantom()) {
407                child.clearDirtyFlags();
408            }
409        }
410    }
411
412}