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 || (value instanceof Object[] && ((Object[]) value).length == 0)) {
079            // ignore null or empty values, properties will be 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 getXPath() {
135        StringBuilder buf = new StringBuilder();
136        getXPath(buf);
137        return buf.toString();
138    }
139
140    protected void getXPath(StringBuilder buf) {
141        if (parent != null) {
142            ((AbstractProperty) parent).getXPath(buf);
143            if (parent.isList()) {
144                buf.append('/');
145                int i = ((ListProperty) parent).children.indexOf(this);
146                buf.append(i);
147            } else {
148                if (buf.length() != 0) {
149                    buf.append('/');
150                }
151                buf.append(getName());
152            }
153        }
154    }
155
156    @Override
157    public String getPath() {
158        Path path = collectPath(new Path("/"));
159        return path.toString();
160    }
161
162    protected Path collectPath(Path path) {
163        String name = getName();
164        if (parent != null) {
165            if (parent.isList()) {
166                int i = ((ListProperty) parent).children.indexOf(this);
167                name = name + '[' + i + ']';
168            }
169            path = ((AbstractProperty) parent).collectPath(path);
170        }
171        return path.append(name);
172    }
173
174    @Override
175    public Schema getSchema() {
176        return getRoot().getSchema();
177    }
178
179    @Override
180    public boolean isList() {
181        return getType().isListType();
182    }
183
184    @Override
185    public boolean isComplex() {
186        return getType().isComplexType();
187    }
188
189    @Override
190    public boolean isScalar() {
191        return getType().isSimpleType();
192    }
193
194    @Override
195    public boolean isNew() {
196        return areFlagsSet(IS_NEW);
197    }
198
199    @Override
200    public boolean isRemoved() {
201        return areFlagsSet(IS_REMOVED);
202    }
203
204    @Override
205    public boolean isMoved() {
206        return areFlagsSet(IS_MOVED);
207    }
208
209    @Override
210    public boolean isModified() {
211        return areFlagsSet(IS_MODIFIED);
212    }
213
214    @Override
215    public boolean isPhantom() {
216        return areFlagsSet(IS_PHANTOM);
217    }
218
219    @Override
220    public final boolean isDirty() {
221        return (flags & IS_DIRTY) != 0;
222    }
223
224    protected final void setDirtyFlags(int dirtyFlags) {
225        flags = dirtyFlags & DIRTY_MASK | flags & ~DIRTY_MASK;
226    }
227
228    protected final void appendDirtyFlags(int dirtyFlags) {
229        flags |= (dirtyFlags & DIRTY_MASK);
230    }
231
232    @Override
233    public boolean isReadOnly() {
234        return areFlagsSet(IS_READONLY);
235    }
236
237    @Override
238    public void setReadOnly(boolean value) {
239        if (value) {
240            setFlags(IS_READONLY);
241        } else {
242            clearFlags(IS_READONLY);
243        }
244    }
245
246    public final boolean areFlagsSet(long flags) {
247        return (this.flags & flags) != 0;
248    }
249
250    public final void setFlags(long flags) {
251        this.flags |= flags;
252    }
253
254    public final void clearFlags(long flags) {
255        this.flags &= ~flags;
256    }
257
258    @Override
259    public int getDirtyFlags() {
260        return flags & DIRTY_MASK;
261    }
262
263    @Override
264    public void clearDirtyFlags() {
265        if ((flags & IS_REMOVED) != 0) {
266            // if is removed the property becomes a phantom
267            setDirtyFlags(IS_PHANTOM);
268        } else {
269            setDirtyFlags(NONE);
270        }
271    }
272
273    /**
274     * This method is public because of DataModelImpl which use it.
275     * <p>
276     * TODO after removing DataModelImpl make it protected.
277     */
278    public void setIsModified() {
279        if ((flags & IS_MODIFIED) == 0) { // if not already modified
280            // clear dirty + phatom flag if any
281            flags |= IS_MODIFIED; // set the modified flag
282            flags &= ~IS_PHANTOM; // remove phantom flag if any
283        }
284        if (parent != null) {
285            ((AbstractProperty) parent).setIsModified();
286        }
287    }
288
289    protected void setIsNew() {
290        if (isDirty()) {
291            throw new IllegalStateException("Cannot set IS_NEW flag on a dirty property");
292        }
293        // clear dirty + phantom flag if any
294        setDirtyFlags(IS_NEW); // this clear any dirty flag and set the new
295        // flag
296        if (parent != null) {
297            ((AbstractProperty) parent).setIsModified();
298        }
299    }
300
301    protected void setIsRemoved() {
302        if (isPhantom() || parent == null || parent.isList()) {
303            throw new IllegalStateException("Cannot set IS_REMOVED on removed or properties that are not map elements");
304        }
305        if ((flags & IS_REMOVED) == 0) { // if not already removed
306            // clear dirty + phatom flag if any
307            setDirtyFlags(IS_REMOVED);
308            ((AbstractProperty) parent).setIsModified();
309        }
310    }
311
312    protected void setIsMoved() {
313        if (parent == null || !parent.isList()) {
314            throw new IllegalStateException("Cannot set IS_MOVED on removed or properties that are not map elements");
315        }
316        if ((flags & IS_MOVED) == 0) {
317            flags |= IS_MOVED;
318            ((AbstractProperty) parent).setIsModified();
319        }
320    }
321
322    @Override
323    public <T> T getValue(Class<T> type) throws PropertyException {
324        return convertTo(getValue(), type);
325    }
326
327    @Override
328    public void setValue(Object value) throws PropertyException {
329        // 1. check the read only flag
330        if (isReadOnly()) {
331            throw new ReadOnlyPropertyException(getXPath());
332        }
333        // 1. normalize the value
334        Serializable normalizedValue = normalize(value);
335        // 2. backup the current
336        Serializable current = internalGetValue();
337        // if its a phantom, no need to check for changes, set it dirty
338        if (!isSameValue(normalizedValue, current) || isForceDirty()) {
339            // 3. set the normalized value and
340            internalSetValue(normalizedValue);
341            // 4. update flags
342            setIsModified();
343        } else {
344            removePhantomFlag();
345        }
346    }
347
348    protected boolean isSameValue(Serializable value1, Serializable value2) {
349        return ((value1 == null && value2 == null) || (value1 != null && value1.equals(value2)));
350    }
351
352    @Override
353    public void setValue(String path, Object value) throws PropertyException {
354        resolvePath(path).setValue(value);
355    }
356
357    @Override
358    public <T> T getValue(Class<T> type, String path) throws PropertyException {
359        return resolvePath(path).getValue(type);
360    }
361
362    @Override
363    public Serializable getValue(String path) throws PropertyException {
364        return resolvePath(path).getValue();
365    }
366
367    @Override
368    public Serializable getValue() throws PropertyException {
369        if (isPhantom() || isRemoved()) {
370            return getDefaultValue();
371        }
372        return internalGetValue();
373    }
374
375    @Override
376    public Serializable getValueForWrite() throws PropertyException {
377        return getValue();
378    }
379
380    protected Serializable getDefaultValue() {
381        return (Serializable) getField().getDefaultValue();
382    }
383
384    @Override
385    public void moveTo(int index) {
386        if (parent == null || !parent.isList()) {
387            throw new UnsupportedOperationException("Not a list item property");
388        }
389        ListProperty list = (ListProperty) parent;
390        if (list.moveTo(this, index)) {
391            setIsMoved();
392        }
393    }
394
395    @Override
396    public DocumentPart getRoot() {
397        return parent == null ? (DocumentPart) this : parent.getRoot();
398    }
399
400    @Override
401    public Property resolvePath(String path) throws PropertyNotFoundException {
402        return resolvePath(new Path(path));
403    }
404
405    @Override
406    public Property resolvePath(Path path) throws PropertyNotFoundException {
407        // handle absolute paths -> resolve them relative to the root
408        if (path.isAbsolute()) {
409            return getRoot().resolvePath(path.makeRelative());
410        }
411
412        String[] segments = path.segments();
413        // handle ../../ paths
414        Property property = this;
415        int start = 0;
416        for (; start < segments.length; start++) {
417            if (segments[start].equals("..")) {
418                property = property.getParent();
419            } else {
420                break;
421            }
422        }
423
424        // now start resolving the path from 'start' depth relative to
425        // 'property'
426        for (int i = start; i < segments.length; i++) {
427            String segment = segments[i];
428            if (property.isScalar()) {
429                throw new PropertyNotFoundException(path.toString(), "segment " + segment
430                        + " points to a scalar property");
431            }
432            String index = null;
433            if (segment.endsWith("]")) {
434                int p = segment.lastIndexOf('[');
435                if (p == -1) {
436                    throw new PropertyNotFoundException(path.toString(), "Parse error: no matching '[' was found");
437                }
438                index = segment.substring(p + 1, segment.length() - 1);
439                segment = segment.substring(0, p);
440            }
441            if (index == null) {
442                property = property.get(segment);
443                if (property == null) {
444                    throw new PropertyNotFoundException(path.toString(), "segment " + segments[i]
445                            + " cannot be resolved");
446                }
447            } else {
448                property = property.get(index);
449            }
450        }
451        return property;
452    }
453
454    @Override
455    public Serializable normalize(Object value) throws PropertyConversionException {
456        if (isNormalized(value)) {
457            return (Serializable) value;
458        }
459        throw new PropertyConversionException(value.getClass(), Serializable.class, getXPath());
460    }
461
462    @Override
463    public boolean isNormalized(Object value) {
464        return value == null || value instanceof Serializable;
465    }
466
467    @Override
468    public <T> T convertTo(Serializable value, Class<T> toType) throws PropertyConversionException {
469        // TODO FIXME XXX make it abstract at this level
470        throw new UnsupportedOperationException("Not implemented");
471    }
472
473    @Override
474    public boolean validateType(Class<?> type) {
475        return true; // TODO XXX FIXME
476    }
477
478    @Override
479    public Object newInstance() {
480        return null; // TODO XXX FIXME
481    }
482
483    @Override
484    public String toString() {
485        return getClass().getSimpleName() + '(' + getXPath() + ')';
486    }
487
488    @Override
489    public PropertyObjectResolver getObjectResolver() {
490        ObjectResolver resolver = getType().getObjectResolver();
491        if (resolver != null) {
492            return new PropertyObjectResolverImpl(this, resolver);
493        }
494        return null;
495    }
496
497    @Override
498    public boolean isForceDirty() {
499        return forceDirty;
500    }
501
502    @Override
503    public void setForceDirty(boolean forceDirty) {
504        this.forceDirty = forceDirty;
505    }
506
507}