001/*
002 * Copyright (c) 2006-2014 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 *     Bogdan Stefanescu
011 *     Florent Guillaume
012 */
013package org.nuxeo.ecm.core.api.model.impl;
014
015import java.io.Serializable;
016import java.util.ArrayList;
017import java.util.Arrays;
018import java.util.Collection;
019import java.util.Collections;
020import java.util.Iterator;
021import java.util.List;
022import java.util.ListIterator;
023
024import org.nuxeo.common.collections.PrimitiveArrays;
025import org.nuxeo.ecm.core.api.ListDiff;
026import org.nuxeo.ecm.core.api.PropertyException;
027import org.nuxeo.ecm.core.api.model.InvalidPropertyValueException;
028import org.nuxeo.ecm.core.api.model.Property;
029import org.nuxeo.ecm.core.api.model.PropertyConversionException;
030import org.nuxeo.ecm.core.api.model.PropertyVisitor;
031import org.nuxeo.ecm.core.api.model.ReadOnlyPropertyException;
032import org.nuxeo.ecm.core.schema.types.Field;
033import org.nuxeo.ecm.core.schema.types.ListType;
034
035public class ListProperty extends AbstractProperty implements List<Property> {
036
037    private static final long serialVersionUID = 1L;
038
039    /**
040     * The corresponding field.
041     */
042    protected final Field field;
043
044    protected final List<Property> children;
045
046    public ListProperty(Property parent, Field field) {
047        super(parent);
048        this.field = field;
049        children = new ArrayList<Property>();
050    }
051
052    public ListProperty(Property parent, Field field, int flags) {
053        super(parent, flags);
054        this.field = field;
055        children = new ArrayList<Property>();
056    }
057
058    @Override
059    public void internalSetValue(Serializable value) throws PropertyException {
060    }
061
062    /**
063     * TODO FIXME XXX uncommented <code>return true;</code> see NXP-1653.
064     *
065     * @see DefaultPropertyFactory line 216
066     * @see {@link ListProperty#getValue}
067     * @see {@link ListProperty#accept}
068     */
069    @Override
070    public boolean isContainer() {
071        // return true; // - this can be uncommented when scalar list will be
072        // fixed
073        return !getType().isScalarList();
074    }
075
076    @Override
077    public Property addValue(int index, Object value) throws PropertyException {
078        Field lfield = getType().getField();
079        Property property = getRoot().createProperty(this, lfield, IS_NEW);
080        property.setValue(value);
081        children.add(index, property);
082        return property;
083    }
084
085    @Override
086    public Property addValue(Object value) throws PropertyException {
087        Field lfield = getType().getField();
088        Property property = getRoot().createProperty(this, lfield, IS_NEW);
089        property.setValue(value);
090        children.add(property);
091        return property;
092    }
093
094    @Override
095    public Property addEmpty() {
096        Field lfield = getType().getField();
097        Property property = getRoot().createProperty(this, lfield, 0);
098        children.add(property);
099        return property;
100    }
101
102    @Override
103    public Collection<Property> getChildren() {
104        return Collections.unmodifiableCollection(children);
105    }
106
107    @Override
108    public String getName() {
109        return field.getName().getPrefixedName();
110    }
111
112    @Override
113    public ListType getType() {
114        return (ListType) field.getType();
115    }
116
117    @Override
118    public Property get(String name) {
119        try {
120            return get(Integer.parseInt(name));
121        } catch (NumberFormatException e) {
122            return null;
123        }
124    }
125
126    @Override
127    public Property get(int index) {
128        try {
129            return children.get(index);
130        } catch (IndexOutOfBoundsException e) {
131            return null;
132        }
133    }
134
135    @Override
136    protected Serializable getDefaultValue() {
137        Serializable value = (Serializable) field.getDefaultValue();
138        if (value == null) {
139            value = new ArrayList<Serializable>();
140        }
141        return value;
142    }
143
144    @Override
145    public Serializable internalGetValue() throws PropertyException {
146        if (children.isEmpty()) {
147            return new ArrayList<String>();
148        }
149        // noinspection CollectionDeclaredAsConcreteClass
150        ArrayList<Object> list = new ArrayList<Object>(children.size());
151        for (Property property : children) {
152            list.add(property.getValue());
153        }
154        // TODO XXX FIXME for compatibility - this treats sclar lists as array
155        // see NXP-1653. remove this when will be fixed
156        // see also isContainer(), setValue() and accept()
157        // if (getType().isScalarList()) {
158        // if (list.isEmpty()) return null;
159        // Object o = list.get(0);
160        // Class<?> type = o.getClass();
161        // if (o == null) {
162        // // don't know the class of the element
163        // // try to use schema information
164        // type = JavaTypes.getPrimitiveClass(getType().getFieldType());
165        // if (type == null) { // this should be a bug
166        // throw new IllegalStateException("Scalar list type is not known - this
167        // should be a bug");
168        // }
169        // } else {
170        // type = o.getClass();
171        // }
172        // return list.toArray((Object[])Array.newInstance(type, list.size()));
173        // }
174        // end of compatibility code <--------
175        return list;
176    }
177
178    @Override
179    public Serializable getValueForWrite() throws PropertyException {
180        if (isPhantom() || isRemoved()) {
181            return getDefaultValue();
182        }
183        if (children.isEmpty()) {
184            return new ArrayList<String>();
185        }
186        // noinspection CollectionDeclaredAsConcreteClass
187        ArrayList<Object> list = new ArrayList<Object>(children.size());
188        for (Property property : children) {
189            list.add(property.getValueForWrite());
190        }
191        return list;
192    }
193
194    @Override
195    @SuppressWarnings("unchecked")
196    public void init(Serializable value) throws PropertyException {
197        if (value == null) { // IGNORE null values - properties will be
198                             // considered PHANTOMS
199            return;
200        }
201        List<Serializable> list;
202        if (value.getClass().isArray()) { // accept also arrays
203            list = (List<Serializable>) PrimitiveArrays.toList(value);
204        } else {
205            list = (List<Serializable>) value;
206        }
207        children.clear(); // do not use clear() method since it is marking the
208                          // list as dirty
209        Field lfield = getType().getField();
210        for (Serializable obj : list) {
211            Property property = getRoot().createProperty(this, lfield, 0);
212            property.init(obj);
213            children.add(property);
214        }
215        removePhantomFlag();
216    }
217
218    @Override
219    public void setValue(Object value) throws PropertyException {
220        if (isReadOnly()) {
221            throw new ReadOnlyPropertyException(getPath());
222        }
223        if (value == null) {
224            List<Property> temp = new ArrayList<Property>(children);
225            for (Property p : temp) { // remove all children
226                p.remove();
227            }
228            return;
229        }
230        Collection<?> col;
231        Class<?> klass = value.getClass();
232        if (klass == ListDiff.class) { // listdiff support for compatibility
233            applyListDiff((ListDiff) value);
234            return;
235        } else if (klass.isArray()) { // array support
236            col = arrayToList(value);
237        } else if (value instanceof Collection) { // collection support
238            col = (Collection<?>) value;
239        } else {
240            throw new InvalidPropertyValueException(getPath());
241        }
242        clear();
243        Field lfield = getType().getField();
244        for (Object obj : col) {
245            Property property = getRoot().createProperty(this, lfield, IS_NEW);
246            property.setValue(obj);
247            children.add(property);
248        }
249    }
250
251    @Override
252    public void clear() {
253        children.clear();
254        setIsModified();
255    }
256
257    @Override
258    public Field getField() {
259        return field;
260    }
261
262    public boolean remove(Property property) {
263        if (!children.remove(property)) { // physically remove the property
264            return false; // no such item
265        }
266        setIsModified();
267        return true;
268    }
269
270    @Override
271    public Property remove(int index) {
272        Property property = children.remove(index);
273        setIsModified();
274        return property;
275    }
276
277    @Override
278    public Object clone() throws CloneNotSupportedException {
279        ListProperty clone = (ListProperty) super.clone();
280        return clone;
281    }
282
283    @Override
284    public void accept(PropertyVisitor visitor, Object arg) throws PropertyException {
285        arg = visitor.visit(this, arg);
286        if (arg != null && isContainer()) {
287            for (Property property : children) {
288                property.accept(visitor, arg);
289            }
290        }
291    }
292
293    /* ---------------------------- type conversion ------------------------ */
294
295    @Override
296    public boolean isNormalized(Object value) {
297        return value == null || (value instanceof Collection && value instanceof Serializable);
298    }
299
300    @Override
301    public Serializable normalize(Object value) throws PropertyConversionException {
302        if (isNormalized(value)) {
303            return (Serializable) value;
304        }
305        if (value.getClass().isArray()) {
306            return arrayToList(value);
307        }
308        throw new PropertyConversionException(value.getClass(), List.class);
309    }
310
311    @SuppressWarnings("unchecked")
312    @Override
313    public <T> T convertTo(Serializable value, Class<T> toType) throws PropertyConversionException {
314        if (value == null) {
315            return null;
316        } else if (toType.isAssignableFrom(value.getClass())) {
317            return toType.cast(value);
318        }
319        if (toType.isArray()) {
320            return (T) ((Collection<?>) value).toArray();
321        } else if (toType == List.class || toType == Collection.class) {
322            // TODO we need this for compatibility with scalar lists
323            if (value.getClass().isArray()) {
324                if (value.getClass().isPrimitive()) {
325                    return (T) PrimitiveArrays.toList(value);
326                } else {
327                    return (T) Arrays.asList((Object[]) value);
328                }
329            }
330        }
331        return super.convertTo(value, toType);
332    }
333
334    // Must return ArrayList
335    public static ArrayList<?> arrayToList(Object obj) {
336        Object[] ar = PrimitiveArrays.toObjectArray(obj);
337        ArrayList<Object> list = new ArrayList<Object>(ar.length);
338        list.addAll(Arrays.asList(ar));
339        return list;
340    }
341
342    /**
343     * Supports ListDiff for compatibility.
344     *
345     * @param ld
346     */
347    public void applyListDiff(ListDiff ld) throws PropertyException {
348        for (ListDiff.Entry entry : ld.diff()) {
349            switch (entry.type) {
350            case ListDiff.ADD:
351                addValue(entry.value);
352                break;
353            case ListDiff.INSERT:
354                addValue(entry.index, entry.value);
355                break;
356            case ListDiff.REMOVE:
357                remove(entry.index);
358                break;
359            case ListDiff.CLEAR:
360                clear();
361                break;
362            case ListDiff.MODIFY:
363                get(entry.index).setValue(entry.value);
364                break;
365            case ListDiff.MOVE:
366                int toIndex = (Integer) entry.value;
367                int fromIndex = entry.index;
368                Property src = children.get(fromIndex);
369                src.moveTo(toIndex);
370                break;
371            }
372        }
373    }
374
375    @Override
376    public boolean isSameAs(Property property) throws PropertyException {
377        if (!(property instanceof ListProperty)) {
378            return false;
379        }
380        ListProperty lp = (ListProperty) property;
381        List<Property> c1 = children;
382        List<Property> c2 = lp.children;
383        if (c1.size() != c2.size()) {
384            return false;
385        }
386        for (int i = 0, size = c1.size(); i < size; i++) {
387            Property p1 = c1.get(i);
388            Property p2 = c2.get(i);
389            if (!p1.isSameAs(p2)) {
390                return false;
391            }
392        }
393        return true;
394    }
395
396    @Override
397    public Iterator<Property> getDirtyChildren() {
398        if (!isContainer()) {
399            throw new UnsupportedOperationException("Cannot iterate over children of scalar properties");
400        }
401        return new DirtyPropertyIterator(children.iterator());
402    }
403
404    public int indexOf(Property property) {
405        for (int i = 0, size = children.size(); i < size; i++) {
406            Property p = children.get(i);
407            if (p == property) {
408                return i;
409            }
410        }
411        return -1;
412    }
413
414    boolean moveTo(Property property, int index) {
415        if (index < 0 || index > children.size()) {
416            throw new IndexOutOfBoundsException("Index out of bounds: " + index + ". Bounds are: 0 - "
417                    + children.size());
418        }
419        int i = indexOf(property);
420        if (i == -1) {
421            throw new UnsupportedOperationException("You are trying to move a property that is not part of a list");
422        }
423        if (i == index) {
424            return false;
425        }
426        if (i < index) {
427            children.add(index + 1, property);
428            children.remove(i);
429        } else {
430            children.add(index, property);
431            children.remove(i + 1);
432        }
433        // new property must be dirty
434        for (int j = Math.min(index, i); j < children.size(); j++) {
435            ((AbstractProperty) children.get(j)).setIsModified();
436        }
437        return true;
438    }
439
440    /**
441     * Throws UnsupportedOperationException, added to implement List<Property> interface
442     */
443    @Override
444    public void add(int index, Property element) {
445        throw new UnsupportedOperationException();
446    }
447
448    /**
449     * Throws UnsupportedOperationException, added to implement List<Property> interface
450     */
451    @Override
452    public boolean add(Property o) {
453        throw new UnsupportedOperationException();
454    }
455
456    /**
457     * Throws UnsupportedOperationException, added to implement List<Property> interface
458     */
459    @Override
460    public boolean addAll(Collection<? extends Property> c) {
461        throw new UnsupportedOperationException();
462    }
463
464    /**
465     * Throws UnsupportedOperationException, added to implement List<Property> interface
466     */
467    @Override
468    public boolean addAll(int index, Collection<? extends Property> c) {
469        throw new UnsupportedOperationException();
470    }
471
472    /**
473     * Throws UnsupportedOperationException, added to implement List<Property> interface
474     */
475    @Override
476    public boolean contains(Object o) {
477        throw new UnsupportedOperationException();
478    }
479
480    /**
481     * Throws UnsupportedOperationException, added to implement List<Property> interface
482     */
483    @Override
484    public boolean containsAll(Collection<?> c) {
485        throw new UnsupportedOperationException();
486    }
487
488    /**
489     * Throws UnsupportedOperationException, added to implement List<Property> interface
490     */
491    @Override
492    public int indexOf(Object o) {
493        throw new UnsupportedOperationException();
494    }
495
496    @Override
497    public boolean isEmpty() {
498        return children.isEmpty();
499    }
500
501    /**
502     * Throws UnsupportedOperationException, added to implement List<Property> interface
503     */
504    @Override
505    public int lastIndexOf(Object o) {
506        throw new UnsupportedOperationException();
507    }
508
509    /**
510     * Throws UnsupportedOperationException, added to implement List<Property> interface
511     */
512    @Override
513    public ListIterator<Property> listIterator() {
514        return children.listIterator();
515    }
516
517    /**
518     * Throws UnsupportedOperationException, added to implement List<Property> interface
519     */
520    @Override
521    public ListIterator<Property> listIterator(int index) {
522        return children.listIterator(index);
523    }
524
525    /**
526     * Throws UnsupportedOperationException, added to implement List<Property> interface
527     */
528    @Override
529    public boolean remove(Object o) {
530        throw new UnsupportedOperationException();
531    }
532
533    /**
534     * Throws UnsupportedOperationException, added to implement List<Property> interface
535     */
536    @Override
537    public boolean removeAll(Collection<?> c) {
538        throw new UnsupportedOperationException();
539    }
540
541    /**
542     * Throws UnsupportedOperationException, added to implement List<Property> interface
543     */
544    @Override
545    public boolean retainAll(Collection<?> c) {
546        throw new UnsupportedOperationException();
547    }
548
549    /**
550     * Throws UnsupportedOperationException, added to implement List<Property> interface
551     */
552    @Override
553    public Property set(int index, Property element) {
554        throw new UnsupportedOperationException();
555    }
556
557    /**
558     * Throws UnsupportedOperationException, added to implement List<Property> interface
559     */
560    @Override
561    public List<Property> subList(int fromIndex, int toIndex) {
562        throw new UnsupportedOperationException();
563    }
564
565    @Override
566    public Object[] toArray() {
567        return children.toArray();
568    }
569
570    @Override
571    public <T> T[] toArray(T[] a) {
572        return children.toArray(a);
573    }
574
575    @Override
576    public void clearDirtyFlags() {
577        // even makes child properties not dirty
578        super.clearDirtyFlags();
579        for (Property child : children) {
580            child.clearDirtyFlags();
581        }
582    }
583
584}