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