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.Iterator;
017
018import org.nuxeo.common.utils.Path;
019import org.nuxeo.ecm.core.api.PropertyException;
020import org.nuxeo.ecm.core.api.model.DocumentPart;
021import org.nuxeo.ecm.core.api.model.Property;
022import org.nuxeo.ecm.core.api.model.PropertyConversionException;
023import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
024import org.nuxeo.ecm.core.api.model.ReadOnlyPropertyException;
025import org.nuxeo.ecm.core.api.model.resolver.PropertyObjectResolver;
026import org.nuxeo.ecm.core.api.model.resolver.PropertyObjectResolverImpl;
027import org.nuxeo.ecm.core.schema.types.Schema;
028import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolver;
029
030public abstract class AbstractProperty implements Property {
031
032    private static final long serialVersionUID = 1L;
033
034    /**
035     * Whether or not this property is read only.
036     */
037    public static final int IS_READONLY = 32;
038
039    public final Property parent;
040
041    /**
042     * for SimpleDocumentModel uses
043     */
044    public boolean forceDirty = false;
045
046    protected int flags;
047
048    protected AbstractProperty(Property parent) {
049        this.parent = parent;
050    }
051
052    protected AbstractProperty(Property parent, int flags) {
053        this.parent = parent;
054        this.flags = flags;
055    }
056
057    /**
058     * Sets the given normalized value.
059     * <p>
060     * This applies only for nodes that physically store a value (that means non container nodes). Container nodes does
061     * nothing.
062     *
063     * @param value
064     */
065    public abstract void internalSetValue(Serializable value) throws PropertyException;
066
067    public abstract Serializable internalGetValue() throws PropertyException;
068
069    @Override
070    public void init(Serializable value) throws PropertyException {
071        if (value == null) { // IGNORE null values - properties will be
072            // considered PHANTOMS
073            return;
074        }
075        internalSetValue(value);
076        removePhantomFlag();
077    }
078
079    public void removePhantomFlag() {
080        flags &= ~IS_PHANTOM;
081        if (parent != null) {
082            ((AbstractProperty) parent).removePhantomFlag();
083        }
084    }
085
086    @Override
087    public void setValue(int index, Object value) throws PropertyException {
088        Property property = get(index);
089        property.setValue(value);
090    }
091
092    @Override
093    public int size() {
094        return getChildren().size();
095    }
096
097    @Override
098    public Iterator<Property> iterator() {
099        return getChildren().iterator();
100    }
101
102    @Override
103    public Serializable remove() throws PropertyException {
104        Serializable value = getValue();
105        if (parent != null && parent.isList()) { // remove from list is
106            // handled separately
107            ListProperty list = (ListProperty) parent;
108            list.remove(this);
109        } else if (!isPhantom()) { // remove from map is easier -> mark the
110            // field as removed and remove the value
111            // do not remove the field if the previous value was null, except if its a property from a SimpleDocumentModel (forceDirty mode)
112            Serializable previous = internalGetValue();
113            init(null);
114            if (previous != null || isForceDirty()) {
115                setIsRemoved();
116            }
117        }
118        return value;
119    }
120
121    @Override
122    public Property getParent() {
123        return parent;
124    }
125
126    @Override
127    public String getPath() {
128        Path path = collectPath(new Path("/"));
129        return path.toString();
130    }
131
132    protected Path collectPath(Path path) {
133        String name = getName();
134        if (parent != null) {
135            if (parent.isList()) {
136                int i = ((ListProperty) parent).children.indexOf(this);
137                name = name + '[' + i + ']';
138            }
139            path = ((AbstractProperty) parent).collectPath(path);
140        }
141        return path.append(name);
142    }
143
144    @Override
145    public Schema getSchema() {
146        return getRoot().getSchema();
147    }
148
149    @Override
150    public boolean isList() {
151        return getType().isListType();
152    }
153
154    @Override
155    public boolean isComplex() {
156        return getType().isComplexType();
157    }
158
159    @Override
160    public boolean isScalar() {
161        return getType().isSimpleType();
162    }
163
164    @Override
165    public boolean isNew() {
166        return areFlagsSet(IS_NEW);
167    }
168
169    @Override
170    public boolean isRemoved() {
171        return areFlagsSet(IS_REMOVED);
172    }
173
174    @Override
175    public boolean isMoved() {
176        return areFlagsSet(IS_MOVED);
177    }
178
179    @Override
180    public boolean isModified() {
181        return areFlagsSet(IS_MODIFIED);
182    }
183
184    @Override
185    public boolean isPhantom() {
186        return areFlagsSet(IS_PHANTOM);
187    }
188
189    @Override
190    public final boolean isDirty() {
191        return (flags & IS_DIRTY) != 0;
192    }
193
194    protected final void setDirtyFlags(int dirtyFlags) {
195        flags = dirtyFlags & DIRTY_MASK | flags & ~DIRTY_MASK;
196    }
197
198    protected final void appendDirtyFlags(int dirtyFlags) {
199        flags |= (dirtyFlags & DIRTY_MASK);
200    }
201
202    @Override
203    public boolean isReadOnly() {
204        return areFlagsSet(IS_READONLY);
205    }
206
207    @Override
208    public void setReadOnly(boolean value) {
209        if (value) {
210            setFlags(IS_READONLY);
211        } else {
212            clearFlags(IS_READONLY);
213        }
214    }
215
216    public final boolean areFlagsSet(long flags) {
217        return (this.flags & flags) != 0;
218    }
219
220    public final void setFlags(long flags) {
221        this.flags |= flags;
222    }
223
224    public final void clearFlags(long flags) {
225        this.flags &= ~flags;
226    }
227
228    @Override
229    public int getDirtyFlags() {
230        return flags & DIRTY_MASK;
231    }
232
233    @Override
234    public void clearDirtyFlags() {
235        if ((flags & IS_REMOVED) != 0) {
236            // if is removed the property becomes a phantom
237            setDirtyFlags(IS_PHANTOM);
238        } else {
239            setDirtyFlags(NONE);
240        }
241    }
242
243    /**
244     * This method is public because of DataModelImpl which use it.
245     * <p>
246     * TODO after removing DataModelImpl make it protected.
247     */
248    public void setIsModified() {
249        if ((flags & IS_MODIFIED) == 0) { // if not already modified
250            // clear dirty + phatom flag if any
251            flags |= IS_MODIFIED; // set the modified flag
252            flags &= ~IS_PHANTOM; // remove phantom flag if any
253        }
254        if (parent != null) {
255            ((AbstractProperty) parent).setIsModified();
256        }
257    }
258
259    protected void setIsNew() {
260        if (isDirty()) {
261            throw new IllegalStateException("Cannot set IS_NEW flag on a dirty property");
262        }
263        // clear dirty + phantom flag if any
264        setDirtyFlags(IS_NEW); // this clear any dirty flag and set the new
265        // flag
266        if (parent != null) {
267            ((AbstractProperty) parent).setIsModified();
268        }
269    }
270
271    protected void setIsRemoved() {
272        if (isPhantom() || parent == null || parent.isList()) {
273            throw new IllegalStateException("Cannot set IS_REMOVED on removed or properties that are not map elements");
274        }
275        if ((flags & IS_REMOVED) == 0) { // if not already removed
276            // clear dirty + phatom flag if any
277            setDirtyFlags(IS_REMOVED);
278            ((AbstractProperty) parent).setIsModified();
279        }
280    }
281
282    protected void setIsMoved() {
283        if (parent == null || !parent.isList()) {
284            throw new IllegalStateException("Cannot set IS_MOVED on removed or properties that are not map elements");
285        }
286        if ((flags & IS_MOVED) == 0) {
287            flags |= IS_MOVED;
288            ((AbstractProperty) parent).setIsModified();
289        }
290    }
291
292    @Override
293    public <T> T getValue(Class<T> type) throws PropertyException {
294        return convertTo(getValue(), type);
295    }
296
297    @Override
298    public void setValue(Object value) throws PropertyException {
299        // 1. check the read only flag
300        if (isReadOnly()) {
301            throw new ReadOnlyPropertyException(getPath());
302        }
303        // 1. normalize the value
304        Serializable normalizedValue = normalize(value);
305        // 2. backup the current
306        Serializable current = internalGetValue();
307        // if its a phantom, no need to check for changes, set it dirty
308        if (!isSameValue(normalizedValue, current) || isForceDirty()) {
309            // 3. set the normalized value and
310            internalSetValue(normalizedValue);
311            // 4. update flags
312            setIsModified();
313        } else {
314            removePhantomFlag();
315        }
316    }
317
318    protected boolean isSameValue(Serializable value1, Serializable value2) {
319        return ((value1 == null && value2 == null) || (value1 != null && value1.equals(value2)));
320    }
321
322    @Override
323    public void setValue(String path, Object value) throws PropertyException {
324        resolvePath(path).setValue(value);
325    }
326
327    @Override
328    public <T> T getValue(Class<T> type, String path) throws PropertyException {
329        return resolvePath(path).getValue(type);
330    }
331
332    @Override
333    public Serializable getValue(String path) throws PropertyException {
334        return resolvePath(path).getValue();
335    }
336
337    @Override
338    public Serializable getValue() throws PropertyException {
339        if (isPhantom() || isRemoved()) {
340            return getDefaultValue();
341        }
342        return internalGetValue();
343    }
344
345    @Override
346    public Serializable getValueForWrite() throws PropertyException {
347        return getValue();
348    }
349
350    protected Serializable getDefaultValue() {
351        return (Serializable) getField().getDefaultValue();
352    }
353
354    @Override
355    public void moveTo(int index) {
356        if (parent == null || !parent.isList()) {
357            throw new UnsupportedOperationException("Not a list item property");
358        }
359        ListProperty list = (ListProperty) parent;
360        if (list.moveTo(this, index)) {
361            setIsMoved();
362        }
363    }
364
365    @Override
366    public DocumentPart getRoot() {
367        return parent == null ? (DocumentPart) this : parent.getRoot();
368    }
369
370    @Override
371    public Property resolvePath(String path) throws PropertyNotFoundException {
372        return resolvePath(new Path(path));
373    }
374
375    @Override
376    public Property resolvePath(Path path) throws PropertyNotFoundException {
377        // handle absolute paths -> resolve them relative to the root
378        if (path.isAbsolute()) {
379            return getRoot().resolvePath(path.makeRelative());
380        }
381
382        String[] segments = path.segments();
383        // handle ../../ paths
384        Property property = this;
385        int start = 0;
386        for (; start < segments.length; start++) {
387            if (segments[start].equals("..")) {
388                property = property.getParent();
389            } else {
390                break;
391            }
392        }
393
394        // now start resolving the path from 'start' depth relative to
395        // 'property'
396        for (int i = start; i < segments.length; i++) {
397            String segment = segments[i];
398            if (property.isScalar()) {
399                throw new PropertyNotFoundException(path.toString(), "segment " + segment
400                        + " points to a scalar property");
401            }
402            String index = null;
403            if (segment.endsWith("]")) {
404                int p = segment.lastIndexOf('[');
405                if (p == -1) {
406                    throw new PropertyNotFoundException(path.toString(), "Parse error: no matching '[' was found");
407                }
408                index = segment.substring(p + 1, segment.length() - 1);
409                segment = segment.substring(0, p);
410            }
411            if (index == null) {
412                property = property.get(segment);
413                if (property == null) {
414                    throw new PropertyNotFoundException(path.toString(), "segment " + segments[i]
415                            + " cannot be resolved");
416                }
417            } else {
418                property = property.get(index);
419            }
420        }
421        return property;
422    }
423
424    @Override
425    public Serializable normalize(Object value) throws PropertyConversionException {
426        if (isNormalized(value)) {
427            return (Serializable) value;
428        }
429        throw new PropertyConversionException(value.getClass(), Serializable.class, getPath());
430    }
431
432    @Override
433    public boolean isNormalized(Object value) {
434        return value == null || value instanceof Serializable;
435    }
436
437    @Override
438    public <T> T convertTo(Serializable value, Class<T> toType) throws PropertyConversionException {
439        // TODO FIXME XXX make it abstract at this level
440        throw new UnsupportedOperationException("Not implemented");
441    }
442
443    @Override
444    public boolean validateType(Class<?> type) {
445        return true; // TODO XXX FIXME
446    }
447
448    @Override
449    public Object newInstance() {
450        return null; // TODO XXX FIXME
451    }
452
453    @Override
454    public String toString() {
455        return getClass().getSimpleName() + '(' + getPath() + ')';
456    }
457
458    @Override
459    public PropertyObjectResolver getObjectResolver() {
460        ObjectResolver resolver = getType().getObjectResolver();
461        if (resolver != null) {
462            return new PropertyObjectResolverImpl(this, resolver);
463        }
464        return null;
465    }
466
467    @Override
468    public boolean isForceDirty() {
469        return forceDirty;
470    }
471
472    @Override
473    public void setForceDirty(boolean forceDirty) {
474        this.forceDirty = forceDirty;
475    }
476
477}