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