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