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;
024import java.util.regex.Matcher;
025import java.util.regex.Pattern;
026
027import org.apache.commons.logging.Log;
028import org.apache.commons.logging.LogFactory;
029import org.nuxeo.common.utils.Path;
030import org.nuxeo.ecm.core.api.NuxeoException;
031import org.nuxeo.ecm.core.api.PropertyException;
032import org.nuxeo.ecm.core.api.model.DocumentPart;
033import org.nuxeo.ecm.core.api.model.Property;
034import org.nuxeo.ecm.core.api.model.PropertyConversionException;
035import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
036import org.nuxeo.ecm.core.api.model.ReadOnlyPropertyException;
037import org.nuxeo.ecm.core.api.model.resolver.PropertyObjectResolver;
038import org.nuxeo.ecm.core.api.model.resolver.PropertyObjectResolverImpl;
039import org.nuxeo.ecm.core.schema.PropertyDeprecationHandler;
040import org.nuxeo.ecm.core.schema.SchemaManager;
041import org.nuxeo.ecm.core.schema.types.Schema;
042import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolver;
043import org.nuxeo.runtime.api.Framework;
044
045public abstract class AbstractProperty implements Property {
046
047    private static final long serialVersionUID = 1L;
048
049    private static final Log log = LogFactory.getLog(AbstractProperty.class);
050
051    protected final static Pattern NON_CANON_INDEX = Pattern.compile("[^/\\[\\]]+" // name
052            + "\\[(-?\\d+)\\]" // index in brackets - could be -1 if element is new to list
053    );
054
055    /**
056     * Whether or not this property is read only.
057     */
058    public static final int IS_READONLY = 32;
059
060    public final Property parent;
061
062    /**
063     * for SimpleDocumentModel uses
064     */
065    public boolean forceDirty = false;
066
067    protected int flags;
068
069    protected Boolean isDeprecated;
070
071    protected Property deprecatedFallback;
072
073    protected AbstractProperty(Property parent) {
074        this.parent = parent;
075    }
076
077    protected AbstractProperty(Property parent, int flags) {
078        this.parent = parent;
079        this.flags = flags;
080    }
081
082    /**
083     * Sets the given normalized value.
084     * <p>
085     * This applies only for nodes that physically store a value (that means non container nodes). Container nodes does
086     * nothing.
087     *
088     * @param value
089     */
090    public abstract void internalSetValue(Serializable value) throws PropertyException;
091
092    public abstract Serializable internalGetValue() throws PropertyException;
093
094    @Override
095    public void init(Serializable value) throws PropertyException {
096        if (value == null || (value instanceof Object[] && ((Object[]) value).length == 0)) {
097            // ignore null or empty values, properties will be considered phantoms
098            return;
099        }
100        internalSetValue(value);
101        removePhantomFlag();
102    }
103
104    public void removePhantomFlag() {
105        flags &= ~IS_PHANTOM;
106        if (parent != null) {
107            ((AbstractProperty) parent).removePhantomFlag();
108        }
109    }
110
111    @Override
112    public void setValue(int index, Object value) throws PropertyException {
113        Property property = get(index);
114        property.setValue(value);
115    }
116
117    @Override
118    public int size() {
119        return getChildren().size();
120    }
121
122    @Override
123    public Iterator<Property> iterator() {
124        return getChildren().iterator();
125    }
126
127    @Override
128    public Serializable remove() throws PropertyException {
129        Serializable value = getValue();
130        if (parent != null && parent.isList()) { // remove from list is
131            // handled separately
132            ListProperty list = (ListProperty) parent;
133            list.remove(this);
134        } else if (!isPhantom()) { // remove from map is easier -> mark the
135            // field as removed and remove the value
136            // do not remove the field if the previous value was null, except if its a property from a
137            // SimpleDocumentModel (forceDirty mode)
138            Serializable previous = internalGetValue();
139            init(null);
140            if (previous != null || isForceDirty()) {
141                setIsRemoved();
142            }
143        }
144        return value;
145    }
146
147    @Override
148    public Property getParent() {
149        return parent;
150    }
151
152    @Override
153    public String getXPath() {
154        StringBuilder buf = new StringBuilder();
155        getXPath(buf);
156        return buf.toString();
157    }
158
159    protected void getXPath(StringBuilder buf) {
160        if (parent != null) {
161            ((AbstractProperty) parent).getXPath(buf);
162            if (parent.isList()) {
163                buf.append('/');
164                int i = ((ListProperty) parent).children.indexOf(this);
165                buf.append(i);
166            } else {
167                if (buf.length() != 0) {
168                    buf.append('/');
169                }
170                buf.append(getName());
171            }
172        }
173    }
174
175    @Override
176    @Deprecated
177    public String getPath() {
178        Path path = collectPath(new Path("/"));
179        return path.toString();
180    }
181
182    protected Path collectPath(Path path) {
183        String name = getName();
184        if (parent != null) {
185            if (parent.isList()) {
186                int i = ((ListProperty) parent).children.indexOf(this);
187                name = name + '[' + i + ']';
188            }
189            path = ((AbstractProperty) parent).collectPath(path);
190        }
191        return path.append(name);
192    }
193
194    @Override
195    public Schema getSchema() {
196        return getRoot().getSchema();
197    }
198
199    @Override
200    public boolean isList() {
201        return getType().isListType();
202    }
203
204    @Override
205    public boolean isComplex() {
206        return getType().isComplexType();
207    }
208
209    @Override
210    public boolean isScalar() {
211        return getType().isSimpleType();
212    }
213
214    @Override
215    public boolean isNew() {
216        return areFlagsSet(IS_NEW);
217    }
218
219    @Override
220    public boolean isRemoved() {
221        return areFlagsSet(IS_REMOVED);
222    }
223
224    @Override
225    public boolean isMoved() {
226        return areFlagsSet(IS_MOVED);
227    }
228
229    @Override
230    public boolean isModified() {
231        return areFlagsSet(IS_MODIFIED);
232    }
233
234    @Override
235    public boolean isPhantom() {
236        return areFlagsSet(IS_PHANTOM);
237    }
238
239    @Override
240    public final boolean isDirty() {
241        return (flags & IS_DIRTY) != 0;
242    }
243
244    protected final void setDirtyFlags(int dirtyFlags) {
245        flags = dirtyFlags & DIRTY_MASK | flags & ~DIRTY_MASK;
246    }
247
248    protected final void appendDirtyFlags(int dirtyFlags) {
249        flags |= (dirtyFlags & DIRTY_MASK);
250    }
251
252    @Override
253    public boolean isReadOnly() {
254        return areFlagsSet(IS_READONLY);
255    }
256
257    @Override
258    public void setReadOnly(boolean value) {
259        if (value) {
260            setFlags(IS_READONLY);
261        } else {
262            clearFlags(IS_READONLY);
263        }
264    }
265
266    public final boolean areFlagsSet(long flags) {
267        return (this.flags & flags) != 0;
268    }
269
270    public final void setFlags(long flags) {
271        this.flags |= flags;
272    }
273
274    public final void clearFlags(long flags) {
275        this.flags &= ~flags;
276    }
277
278    @Override
279    public int getDirtyFlags() {
280        return flags & DIRTY_MASK;
281    }
282
283    @Override
284    public void clearDirtyFlags() {
285        if ((flags & IS_REMOVED) != 0) {
286            // if is removed the property becomes a phantom
287            setDirtyFlags(IS_PHANTOM);
288        } else {
289            setDirtyFlags(NONE);
290        }
291    }
292
293    /**
294     * This method is public because of DataModelImpl which use it.
295     * <p>
296     * TODO after removing DataModelImpl make it protected.
297     */
298    public void setIsModified() {
299        if ((flags & IS_MODIFIED) == 0) { // if not already modified
300            // clear dirty + phatom flag if any
301            flags |= IS_MODIFIED; // set the modified flag
302            flags &= ~IS_PHANTOM; // remove phantom flag if any
303        }
304        if (parent != null) {
305            ((AbstractProperty) parent).setIsModified();
306        }
307    }
308
309    protected void setIsNew() {
310        if (isDirty()) {
311            throw new IllegalStateException("Cannot set IS_NEW flag on a dirty property");
312        }
313        // clear dirty + phantom flag if any
314        setDirtyFlags(IS_NEW); // this clear any dirty flag and set the new
315        // flag
316        if (parent != null) {
317            ((AbstractProperty) parent).setIsModified();
318        }
319    }
320
321    protected void setIsRemoved() {
322        if (isPhantom() || parent == null || parent.isList()) {
323            throw new IllegalStateException("Cannot set IS_REMOVED on removed or properties that are not map elements");
324        }
325        if ((flags & IS_REMOVED) == 0) { // if not already removed
326            // clear dirty + phatom flag if any
327            setDirtyFlags(IS_REMOVED);
328            ((AbstractProperty) parent).setIsModified();
329        }
330    }
331
332    protected void setIsMoved() {
333        if (parent == null || !parent.isList()) {
334            throw new IllegalStateException("Cannot set IS_MOVED on removed or properties that are not map elements");
335        }
336        if ((flags & IS_MOVED) == 0) {
337            flags |= IS_MOVED;
338            ((AbstractProperty) parent).setIsModified();
339        }
340    }
341
342    protected boolean isDeprecated() {
343        if (isDeprecated == null) {
344            boolean deprecated = false;
345            // compute the deprecated state
346            // first check if this property is a child of a deprecated property
347            if (parent instanceof AbstractProperty) {
348                AbstractProperty absParent = (AbstractProperty) parent;
349                deprecated = absParent.isDeprecated();
350                Property parentDeprecatedFallback = absParent.deprecatedFallback;
351                if (deprecated && parentDeprecatedFallback != null) {
352                    deprecatedFallback = parentDeprecatedFallback.resolvePath(getName());
353                }
354            }
355            if (!deprecated) {
356                // check if this property is deprecated
357                String name = getXPath();
358                String schema = getSchema().getName();
359                SchemaManager schemaManager = Framework.getService(SchemaManager.class);
360                PropertyDeprecationHandler deprecatedProperties = schemaManager.getDeprecatedProperties();
361                deprecated = deprecatedProperties.isMarked(schema, name);
362                if (deprecated) {
363                    // get the possible fallback
364                    String fallback = deprecatedProperties.getFallback(schema, name);
365                    if (fallback != null) {
366                        deprecatedFallback = resolvePath('/' + fallback);
367                    }
368                }
369            }
370            isDeprecated = Boolean.valueOf(deprecated);
371        }
372        return isDeprecated.booleanValue();
373    }
374
375    @Override
376    public <T> T getValue(Class<T> type) throws PropertyException {
377        return convertTo(getValue(), type);
378    }
379
380    @Override
381    public void setValue(Object value) throws PropertyException {
382        // 1. check the read only flag
383        if (isReadOnly()) {
384            throw new ReadOnlyPropertyException(getXPath());
385        }
386        // 1. normalize the value
387        Serializable normalizedValue = normalize(value);
388        // 2. backup the current
389        Serializable current = internalGetValue();
390        // if its a phantom, no need to check for changes, set it dirty
391        if (!isSameValue(normalizedValue, current) || isForceDirty()) {
392            // 3. set the normalized value and
393            internalSetValue(normalizedValue);
394            // 4. set also value to fallback if this property is deprecated and has one
395            setValueDeprecation(normalizedValue, true);
396            // 5. update flags
397            setIsModified();
398        } else {
399            removePhantomFlag();
400        }
401    }
402
403    /**
404     * If this property is deprecated and has a fallback, set value to fallback.
405     */
406    protected void setValueDeprecation(Object value, boolean setFallback) throws PropertyException {
407        if (isDeprecated()) {
408            // First check if we need to set the fallback value
409            if (setFallback && deprecatedFallback != null) {
410                deprecatedFallback.setValue(value);
411            }
412            // Second check if we need to log deprecation message
413            if (log.isWarnEnabled()) {
414                StringBuilder msg = newDeprecatedMessage();
415                msg.append("Set value to deprecated property");
416                if (deprecatedFallback != null) {
417                    msg.append(" and to fallback property '").append(deprecatedFallback.getXPath()).append("'");
418                }
419                if (log.isTraceEnabled()) {
420                    log.warn(msg, new NuxeoException("debug stack trace"));
421                } else {
422                    log.warn(msg);
423                }
424            }
425        }
426    }
427
428    protected boolean isSameValue(Serializable value1, Serializable value2) {
429        return ((value1 == null && value2 == null) || (value1 != null && value1.equals(value2)));
430    }
431
432    @Override
433    public void setValue(String path, Object value) throws PropertyException {
434        resolvePath(path).setValue(value);
435    }
436
437    @Override
438    public <T> T getValue(Class<T> type, String path) throws PropertyException {
439        return resolvePath(path).getValue(type);
440    }
441
442    @Override
443    public Serializable getValue(String path) throws PropertyException {
444        return resolvePath(path).getValue();
445    }
446
447    @Override
448    public Serializable getValue() throws PropertyException {
449        if (isPhantom() || isRemoved()) {
450            return getDefaultValue();
451        }
452        Serializable fallbackValue = getValueDeprecation();
453        return fallbackValue == null ? internalGetValue() : fallbackValue;
454    }
455
456    /**
457     * @return the fallback value if this property is deprecated and has a fallback, otherwise return null
458     */
459    protected Serializable getValueDeprecation() {
460        if (isDeprecated()) {
461            // Check if we need to log deprecation message
462            if (log.isWarnEnabled()) {
463                StringBuilder msg = newDeprecatedMessage();
464                if (deprecatedFallback == null) {
465                    msg.append("Return value from deprecated property");
466                } else {
467                    msg.append("Return value from '").append(deprecatedFallback.getXPath()).append(
468                            "' if not null, from deprecated property otherwise");
469                }
470                if (log.isTraceEnabled()) {
471                    log.warn(msg, new NuxeoException());
472                } else {
473                    log.warn(msg);
474                }
475            }
476            if (deprecatedFallback != null) {
477                return deprecatedFallback.getValue();
478            }
479        }
480        return null;
481    }
482
483    protected StringBuilder newDeprecatedMessage() {
484        StringBuilder builder = new StringBuilder().append("Property '")
485                                                   .append(getXPath())
486                                                   .append("' is marked as deprecated from '")
487                                                   .append(getSchema().getName())
488                                                   .append("' schema");
489        Property deprecatedParent = getDeprecatedParent();
490        if (deprecatedParent != this) {
491            builder.append(" because property '")
492                   .append(deprecatedParent.getXPath())
493                   .append("' is marked as deprecated");
494        }
495        return builder.append(", don't use it anymore. ");
496    }
497
498    /**
499     * @return the higher deprecated parent.
500     */
501    protected AbstractProperty getDeprecatedParent() {
502        if (parent instanceof AbstractProperty) {
503            AbstractProperty absParent = (AbstractProperty) parent;
504            if (absParent.isDeprecated()) {
505                return absParent.getDeprecatedParent();
506            }
507        }
508        return this;
509    }
510
511    @Override
512    public Serializable getValueForWrite() throws PropertyException {
513        return getValue();
514    }
515
516    protected Serializable getDefaultValue() {
517        return (Serializable) getField().getDefaultValue();
518    }
519
520    @Override
521    public void moveTo(int index) {
522        if (parent == null || !parent.isList()) {
523            throw new UnsupportedOperationException("Not a list item property");
524        }
525        ListProperty list = (ListProperty) parent;
526        if (list.moveTo(this, index)) {
527            setIsMoved();
528        }
529    }
530
531    @Override
532    public DocumentPart getRoot() {
533        return parent == null ? (DocumentPart) this : parent.getRoot();
534    }
535
536    @Override
537    public Property resolvePath(String path) throws PropertyNotFoundException {
538        return resolvePath(new Path(path));
539    }
540
541    @Override
542    public Property resolvePath(Path path) throws PropertyNotFoundException {
543        // handle absolute paths -> resolve them relative to the root
544        if (path.isAbsolute()) {
545            return getRoot().resolvePath(path.makeRelative());
546        }
547
548        String[] segments = path.segments();
549        // handle ../../ paths
550        Property property = this;
551        int start = 0;
552        for (; start < segments.length; start++) {
553            if (segments[start].equals("..")) {
554                property = property.getParent();
555            } else {
556                break;
557            }
558        }
559
560        // now start resolving the path from 'start' depth relative to
561        // 'property'
562        for (int i = start; i < segments.length; i++) {
563            String segment = segments[i];
564            if (property.isScalar()) {
565                throw new PropertyNotFoundException(path.toString(),
566                        "segment " + segment + " points to a scalar property");
567            }
568            String index = null;
569            if (segment.endsWith("]")) {
570                int p = segment.lastIndexOf('[');
571                if (p == -1) {
572                    throw new PropertyNotFoundException(path.toString(), "Parse error: no matching '[' was found");
573                }
574                index = segment.substring(p + 1, segment.length() - 1);
575            }
576            if (index == null) {
577                property = property.get(segment);
578                if (property == null) {
579                    throw new PropertyNotFoundException(path.toString(), "segment " + segment + " cannot be resolved");
580                }
581            } else {
582                property = property.get(index);
583            }
584        }
585        return property;
586    }
587
588    /**
589     * Returns the {@link RemovedProperty} if it is a removed property or null otherwise.
590     *
591     * @since 9.2
592     */
593    protected Property computeRemovedProperty(String name) {
594        String schema = getSchema().getName();
595        // name is only the property name we try to get, build its path in order to check it against configuration
596        String originalXpath = collectPath(new Path("/")).append(name).toString().substring(1);
597        String xpath;
598        // replace all something[..] in a path by *, for example files/item[2]/filename -> files/*/filename
599        if (originalXpath.indexOf('[') != -1) {
600            xpath = NON_CANON_INDEX.matcher(originalXpath).replaceAll("*");
601        } else {
602            xpath = originalXpath;
603        }
604        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
605        PropertyDeprecationHandler removedProperties = schemaManager.getRemovedProperties();
606        if (!removedProperties.isMarked(schema, xpath)) {
607            return null;
608        }
609        String fallback = removedProperties.getFallback(schema, xpath);
610        if (fallback == null) {
611            return new RemovedProperty(this, name);
612        }
613
614        // Retrieve fallback property
615        Matcher matcher = NON_CANON_INDEX.matcher(originalXpath);
616        while (matcher.find()) {
617            fallback = fallback.replaceFirst("\\*", matcher.group(0));
618        }
619        Property fallbackProperty;
620        // Handle creation of complex property in a list ie: xpath contains [-1]
621        int i = fallback.lastIndexOf("[-1]");
622        if (i != -1) {
623            // skip [-1]/ to get next property
624            fallbackProperty = get(fallback.substring(i + 5));
625        } else {
626            fallbackProperty = resolvePath('/' + fallback);
627        }
628        return new RemovedProperty(this, name, fallbackProperty);
629    }
630
631    @Override
632    public Serializable normalize(Object value) throws PropertyConversionException {
633        if (isNormalized(value)) {
634            return (Serializable) value;
635        }
636        throw new PropertyConversionException(value.getClass(), Serializable.class, getXPath());
637    }
638
639    @Override
640    public boolean isNormalized(Object value) {
641        return value == null || value instanceof Serializable;
642    }
643
644    @Override
645    public <T> T convertTo(Serializable value, Class<T> toType) throws PropertyConversionException {
646        // TODO FIXME XXX make it abstract at this level
647        throw new UnsupportedOperationException("Not implemented");
648    }
649
650    @Override
651    public boolean validateType(Class<?> type) {
652        return true; // TODO XXX FIXME
653    }
654
655    @Override
656    public Object newInstance() {
657        return null; // TODO XXX FIXME
658    }
659
660    @Override
661    public String toString() {
662        return getClass().getSimpleName() + '(' + getXPath() + ')';
663    }
664
665    @Override
666    public PropertyObjectResolver getObjectResolver() {
667        ObjectResolver resolver = getType().getObjectResolver();
668        if (resolver != null) {
669            return new PropertyObjectResolverImpl(this, resolver);
670        }
671        return null;
672    }
673
674    @Override
675    public boolean isForceDirty() {
676        return forceDirty;
677    }
678
679    @Override
680    public void setForceDirty(boolean forceDirty) {
681        this.forceDirty = forceDirty;
682    }
683
684}