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