001/*
002 * (C) Copyright 2015-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 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.core.storage;
020
021import java.io.IOException;
022import java.io.Serializable;
023import java.lang.reflect.Array;
024import java.util.ArrayDeque;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Calendar;
028import java.util.Collection;
029import java.util.Collections;
030import java.util.Deque;
031import java.util.HashMap;
032import java.util.HashSet;
033import java.util.List;
034import java.util.Map;
035import java.util.Map.Entry;
036import java.util.Objects;
037import java.util.Set;
038import java.util.function.Consumer;
039import java.util.regex.Pattern;
040
041import org.apache.commons.lang3.StringUtils;
042import org.nuxeo.ecm.core.api.Blob;
043import org.nuxeo.ecm.core.api.Blobs;
044import org.nuxeo.ecm.core.api.CoreSession;
045import org.nuxeo.ecm.core.api.DocumentNotFoundException;
046import org.nuxeo.ecm.core.api.Lock;
047import org.nuxeo.ecm.core.api.PropertyException;
048import org.nuxeo.ecm.core.api.model.BlobNotFoundException;
049import org.nuxeo.ecm.core.api.model.DocumentPart;
050import org.nuxeo.ecm.core.api.model.Property;
051import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
052import org.nuxeo.ecm.core.api.model.impl.ComplexProperty;
053import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty;
054import org.nuxeo.ecm.core.blob.BlobInfo;
055import org.nuxeo.ecm.core.blob.DocumentBlobManager;
056import org.nuxeo.ecm.core.model.Document;
057import org.nuxeo.ecm.core.schema.SchemaManager;
058import org.nuxeo.ecm.core.schema.TypeConstants;
059import org.nuxeo.ecm.core.schema.types.ComplexType;
060import org.nuxeo.ecm.core.schema.types.CompositeType;
061import org.nuxeo.ecm.core.schema.types.Field;
062import org.nuxeo.ecm.core.schema.types.ListType;
063import org.nuxeo.ecm.core.schema.types.Schema;
064import org.nuxeo.ecm.core.schema.types.SimpleTypeImpl;
065import org.nuxeo.ecm.core.schema.types.Type;
066import org.nuxeo.ecm.core.schema.types.primitives.BinaryType;
067import org.nuxeo.ecm.core.schema.types.primitives.BooleanType;
068import org.nuxeo.ecm.core.schema.types.primitives.DateType;
069import org.nuxeo.ecm.core.schema.types.primitives.DoubleType;
070import org.nuxeo.ecm.core.schema.types.primitives.IntegerType;
071import org.nuxeo.ecm.core.schema.types.primitives.LongType;
072import org.nuxeo.ecm.core.schema.types.primitives.StringType;
073import org.nuxeo.runtime.api.Framework;
074
075/**
076 * Base implementation for a Document.
077 * <p>
078 * Knows how to read and write values. It is generic in terms of a base State class from which one can read and write
079 * values.
080 *
081 * @since 7.3
082 */
083public abstract class BaseDocument<T extends StateAccessor> implements Document {
084
085    public static final String[] EMPTY_STRING_ARRAY = new String[0];
086
087    public static final String BLOB_NAME = "name";
088
089    public static final String BLOB_MIME_TYPE = "mime-type";
090
091    public static final String BLOB_ENCODING = "encoding";
092
093    public static final String BLOB_DIGEST = "digest";
094
095    public static final String BLOB_LENGTH = "length";
096
097    public static final String BLOB_DATA = "data";
098
099    public static final String DC_PREFIX = "dc:";
100
101    public static final String DC_ISSUED = "dc:issued";
102
103    // used instead of ecm:changeToken when change tokens are disabled
104    public static final String DC_MODIFIED = "dc:modified";
105
106    public static final String RELATED_TEXT_RESOURCES = "relatedtextresources";
107
108    public static final String RELATED_TEXT_ID = "relatedtextid";
109
110    public static final String RELATED_TEXT = "relatedtext";
111
112    public static final String FULLTEXT_JOBID_PROP = "ecm:fulltextJobId";
113
114    public static final String FULLTEXT_SIMPLETEXT_PROP = "ecm:fulltextSimple";
115
116    public static final String FULLTEXT_BINARYTEXT_PROP = "ecm:fulltextBinary";
117
118    public static final String IS_TRASHED_PROP = "ecm:isTrashed";
119
120    public static final String MISC_LIFECYCLE_STATE_PROP = "ecm:lifeCycleState";
121
122    public static final String LOCK_OWNER_PROP = "ecm:lockOwner";
123
124    public static final String LOCK_CREATED_PROP = "ecm:lockCreated";
125
126    /** @since 11.1 */
127    public static final String IS_RECORD_PROP = "ecm:isRecord";
128
129    /** @since 11.1 */
130    public static final String RETAIN_UNTIL_PROP = "ecm:retainUntil";
131
132    /** @since 11.1 */
133    public static final String HAS_LEGAL_HOLD_PROP = "ecm:hasLegalHold";
134
135    public static final Set<String> VERSION_WRITABLE_PROPS = new HashSet<>(Arrays.asList( //
136            FULLTEXT_JOBID_PROP, //
137            FULLTEXT_BINARYTEXT_PROP, //
138            IS_TRASHED_PROP, //
139            MISC_LIFECYCLE_STATE_PROP, //
140            LOCK_OWNER_PROP, //
141            LOCK_CREATED_PROP, //
142            DC_ISSUED, //
143            IS_RECORD_PROP, //
144            RETAIN_UNTIL_PROP, //
145            HAS_LEGAL_HOLD_PROP, //
146            RELATED_TEXT_RESOURCES, //
147            RELATED_TEXT_ID, //
148            RELATED_TEXT //
149    ));
150
151    protected final static Pattern NON_CANONICAL_INDEX = Pattern.compile("[^/\\[\\]]+" // name
152            + "\\[(\\d+)\\]" // index in brackets
153    );
154
155    protected static final Runnable NO_DIRTY = () -> {
156    };
157
158    /**
159     * Gets the list of proxy schemas, if this is a proxy.
160     *
161     * @return the proxy schemas, or {@code null}
162     */
163    protected abstract List<Schema> getProxySchemas();
164
165    /**
166     * Gets a child state.
167     *
168     * @param state the parent state
169     * @param name the child name
170     * @param type the child's type
171     * @return the child state, or {@code null} if it doesn't exist
172     */
173    protected abstract T getChild(T state, String name, Type type) throws PropertyException;
174
175    /**
176     * Gets a child state into which we will want to write data.
177     * <p>
178     * Creates it if needed.
179     *
180     * @param state the parent state
181     * @param name the child name
182     * @param type the child's type
183     * @return the child state, never {@code null}
184     * @since 7.4
185     */
186    protected abstract T getChildForWrite(T state, String name, Type type) throws PropertyException;
187
188    /**
189     * Gets a child state which is a list.
190     *
191     * @param state the parent state
192     * @param name the child name
193     * @return the child state, never {@code null}
194     */
195    protected abstract List<T> getChildAsList(T state, String name) throws PropertyException;
196
197    /**
198     * Update a list.
199     *
200     * @param state the parent state
201     * @param name the child name
202     * @param field the list element type
203     * @param xpath the xpath of this list
204     * @param values the values
205     */
206    protected abstract void updateList(T state, String name, Field field, String xpath, List<Object> values)
207            throws PropertyException;
208
209    /**
210     * Update a list.
211     *
212     * @param state the parent state
213     * @param name the child name
214     * @param property the property
215     * @return the list of states to write
216     */
217    protected abstract List<T> updateList(T state, String name, Property property) throws PropertyException;
218
219    /**
220     * Finds the internal name to use to refer to this property.
221     */
222    protected abstract String internalName(String name);
223
224    /**
225     * Canonicalizes a Nuxeo xpath.
226     * <p>
227     * Replaces {@code a/foo[123]/b} with {@code a/123/b}
228     *
229     * @param xpath the xpath
230     * @return the canonicalized xpath.
231     */
232    protected static String canonicalXPath(String xpath) {
233        if (xpath.indexOf('[') > 0) {
234            xpath = NON_CANONICAL_INDEX.matcher(xpath).replaceAll("$1");
235        }
236        return xpath;
237    }
238
239    /** Copies the array with an appropriate class depending on the type. */
240    protected static Object[] typedArray(Type type, Object[] array) {
241        if (array == null) {
242            array = EMPTY_STRING_ARRAY;
243        }
244        Class<?> klass;
245        if (type instanceof StringType) {
246            klass = String.class;
247        } else if (type instanceof BooleanType) {
248            klass = Boolean.class;
249        } else if (type instanceof LongType) {
250            klass = Long.class;
251        } else if (type instanceof DoubleType) {
252            klass = Double.class;
253        } else if (type instanceof DateType) {
254            klass = Calendar.class;
255        } else if (type instanceof BinaryType) {
256            klass = String.class;
257        } else if (type instanceof IntegerType) {
258            throw new RuntimeException("Unimplemented primitive type: " + type.getClass().getName());
259        } else if (type instanceof SimpleTypeImpl) {
260            // simple type with constraints -- ignore constraints XXX
261            return typedArray(type.getSuperType(), array);
262        } else {
263            throw new RuntimeException("Invalid primitive type: " + type.getClass().getName());
264        }
265        int len = array.length;
266        Object[] copy = (Object[]) Array.newInstance(klass, len);
267        System.arraycopy(array, 0, copy, 0, len);
268        return copy;
269    }
270
271    protected static boolean isVersionWritableProperty(String name) {
272        return VERSION_WRITABLE_PROPS.contains(name) //
273                || name.startsWith(FULLTEXT_BINARYTEXT_PROP) //
274                || name.startsWith(FULLTEXT_SIMPLETEXT_PROP);
275    }
276
277    protected static void clearDirtyFlags(Property property) {
278        if (property.isContainer()) {
279            for (Property p : property) {
280                clearDirtyFlags(p);
281            }
282        }
283        property.clearDirtyFlags();
284    }
285
286    /**
287     * Checks for ignored writes. May throw.
288     */
289    protected boolean checkReadOnlyIgnoredWrite(Property property, T state) throws PropertyException {
290        String name = property.getField().getName().getPrefixedName();
291        if (!isReadOnly() || isVersionWritableProperty(name)) {
292            // do write
293            return false;
294        }
295        if (!isVersion()) {
296            throw new PropertyException("Cannot write readonly property: " + name);
297        }
298        if (getTopLevelSchema(property).isVersionWritabe()) {
299            // do write
300            return false;
301        }
302        if (!name.startsWith(DC_PREFIX)) {
303            throw new PropertyException("Cannot set property on a version: " + name);
304        }
305        // ignore write if value can quickly be detected as unchanged
306        Object value = property.getValueForWrite();
307        Type type = property.getType();
308        boolean equals;
309        if (type.isSimpleType()) {
310            Object oldValue = state.getSingle(name);
311            equals = Objects.deepEquals(value, oldValue);
312        } else if (type.isListType() && ((ListType) type).getFieldType().isSimpleType()) {
313            Object[] oldValue = state.getArray(name);
314            if (value == null && oldValue != null && oldValue.length == 0) {
315                // empty array and null are the same
316                equals = true;
317            } else {
318                equals = Objects.deepEquals(value, oldValue);
319            }
320        } else {
321            // complex property or complex list, no quick way to detect changes
322            equals = false;
323        }
324        if (equals) {
325            // unchanged, ignore write even though strictly speaking writing the version is not allowed
326            return true;
327        } else {
328            if (Framework.getService(SchemaManager.class).getAllowVersionWriteForDublinCore()) {
329                // do write (compatibility with old Nuxeo versions that had this bug)
330                return false;
331            }
332            throw new PropertyException("Cannot set property on a version: " + name);
333        }
334    }
335
336    /**
337     * Gets the {@link Schema} at the top-level of the type hierarchy for this {@link Property}.
338     *
339     * @since 9.3
340     */
341    protected Schema getTopLevelSchema(Property property) {
342        for (;;) {
343            Type type = property.getType();
344            if (type instanceof Schema) {
345                return (Schema) type;
346            }
347            property = property.getParent();
348        }
349    }
350
351    protected BlobInfo getBlobInfo(T state) throws PropertyException {
352        BlobInfo blobInfo = new BlobInfo();
353        blobInfo.key = (String) state.getSingle(BLOB_DATA);
354        blobInfo.filename = (String) state.getSingle(BLOB_NAME);
355        blobInfo.mimeType = (String) state.getSingle(BLOB_MIME_TYPE);
356        blobInfo.encoding = (String) state.getSingle(BLOB_ENCODING);
357        blobInfo.digest = (String) state.getSingle(BLOB_DIGEST);
358        blobInfo.length = (Long) state.getSingle(BLOB_LENGTH);
359        return blobInfo;
360    }
361
362    protected void setBlobInfo(T state, BlobInfo blobInfo) throws PropertyException {
363        state.setSingle(BLOB_DATA, blobInfo.key);
364        state.setSingle(BLOB_NAME, blobInfo.filename);
365        state.setSingle(BLOB_MIME_TYPE, blobInfo.mimeType);
366        state.setSingle(BLOB_ENCODING, blobInfo.encoding);
367        state.setSingle(BLOB_DIGEST, blobInfo.digest);
368        state.setSingle(BLOB_LENGTH, blobInfo.length);
369    }
370
371    /**
372     * Gets a value (may be complex/list) from the document at the given xpath.
373     */
374    protected Object getValueObject(T state, String xpath) throws PropertyException {
375        xpath = canonicalXPath(xpath);
376        String[] segments = xpath.split("/");
377
378        /*
379         * During this loop state may become null if we read an uninitialized complex property (DBS), in that case the
380         * code must treat it as reading uninitialized values for its children.
381         */
382        ComplexType parentType = getType();
383        for (int i = 0; i < segments.length; i++) {
384            String segment = segments[i];
385            Field field = parentType.getField(segment);
386            if (field == null && i == 0) {
387                // check facets
388                SchemaManager schemaManager = Framework.getService(SchemaManager.class);
389                for (String facet : getFacets()) {
390                    CompositeType facetType = schemaManager.getFacet(facet);
391                    field = facetType.getField(segment);
392                    if (field != null) {
393                        break;
394                    }
395                }
396            }
397            if (field == null && i == 0 && getProxySchemas() != null) {
398                // check proxy schemas
399                for (Schema schema : getProxySchemas()) {
400                    field = schema.getField(segment);
401                    if (field != null) {
402                        break;
403                    }
404                }
405            }
406            if (field == null) {
407                throw new PropertyNotFoundException(xpath, i == 0 ? null : "Unknown segment: " + segment);
408            }
409            String name = field.getName().getPrefixedName(); // normalize from segment
410            Type type = field.getType();
411
412            // check if we have a complex list index in the next position
413            if (i < segments.length - 1 && StringUtils.isNumeric(segments[i + 1])) {
414                int index = Integer.parseInt(segments[i + 1]);
415                i++;
416                if (!type.isListType() || ((ListType) type).getFieldType().isSimpleType()) {
417                    throw new PropertyNotFoundException(xpath, "Cannot use index after segment: " + segment);
418                }
419                List<T> list = state == null ? Collections.emptyList() : getChildAsList(state, name);
420                if (index >= list.size()) {
421                    throw new PropertyNotFoundException(xpath, "Index out of bounds: " + index);
422                }
423                // find complex list state
424                state = list.get(index);
425                parentType = (ComplexType) ((ListType) type).getFieldType();
426                if (i == segments.length - 1) {
427                    // last segment
428                    return getValueComplex(state, parentType, xpath);
429                } else {
430                    // not last segment
431                    continue;
432                }
433            }
434
435            if (i == segments.length - 1) {
436                // last segment
437                return state == null ? null : getValueField(state, field, xpath);
438            } else {
439                // not last segment
440                if (type.isSimpleType()) {
441                    // scalar
442                    throw new PropertyNotFoundException(xpath, "Segment must be last: " + segment);
443                } else if (type.isComplexType()) {
444                    // complex property
445                    state = state == null ? null : getChild(state, name, type);
446                    // here state can be null (DBS), continue loop with it, meaning uninitialized for read
447                    parentType = (ComplexType) type;
448                } else {
449                    // list
450                    ListType listType = (ListType) type;
451                    if (listType.isArray()) {
452                        // array of scalars
453                        throw new PropertyNotFoundException(xpath, "Segment must be last: " + segment);
454                    } else {
455                        // complex list but next segment was not numeric
456                        throw new PropertyNotFoundException(xpath, "Missing list index after segment: " + segment);
457                    }
458                }
459            }
460        }
461        throw new AssertionError("not reached");
462    }
463
464    protected Object getValueField(T state, Field field, String xpath) throws PropertyException {
465        Type type = field.getType();
466        String name = field.getName().getPrefixedName();
467        name = internalName(name);
468        String xp = xpath == null ? name : xpath + '/' + name;
469        if (type.isSimpleType()) {
470            // scalar
471            return state.getSingle(name);
472        } else if (type.isComplexType()) {
473            // complex property
474            T childState = getChild(state, name, type);
475            if (childState == null) {
476                return null;
477            }
478            return getValueComplex(childState, (ComplexType) type, xp);
479        } else {
480            // array or list
481            Type fieldType = ((ListType) type).getFieldType();
482            if (fieldType.isSimpleType()) {
483                // array
484                return state.getArray(name);
485            } else {
486                // complex list
487                List<T> childStates = getChildAsList(state, name);
488                List<Object> list = new ArrayList<>(childStates.size());
489                int i = 0;
490                for (T childState : childStates) {
491                    String xpi = xp + '/' + i++;
492                    Object value = getValueComplex(childState, (ComplexType) fieldType, xpi);
493                    list.add(value);
494                }
495                return list;
496            }
497        }
498    }
499
500    protected Object getValueComplex(T state, ComplexType complexType, String xpath) throws PropertyException {
501        if (TypeConstants.isContentType(complexType)) {
502            return getValueBlob(state, xpath);
503        }
504        Map<String, Object> map = new HashMap<>();
505        for (Field field : complexType.getFields()) {
506            String name = field.getName().getPrefixedName();
507            String xp = xpath + '/' + name;
508            Object value = getValueField(state, field, xp);
509            map.put(name, value);
510        }
511        return map;
512    }
513
514    protected Blob getValueBlob(T state, String xpath) throws PropertyException {
515        BlobInfo blobInfo = getBlobInfo(state);
516        DocumentBlobManager blobManager = Framework.getService(DocumentBlobManager.class);
517        try {
518            return blobManager.readBlob(blobInfo, this, xpath);
519        } catch (IOException e) {
520            throw new BlobNotFoundException("Unable to find blob with key: " + blobInfo.key, e);
521        }
522    }
523
524    /**
525     * Sets a value (may be complex/list) into the document at the given xpath.
526     */
527    protected void setValueObject(T state, String xpath, Object value) throws PropertyException {
528        xpath = canonicalXPath(xpath);
529        String[] segments = xpath.split("/");
530
531        ComplexType parentType = getType();
532        for (int i = 0; i < segments.length; i++) {
533            String segment = segments[i];
534            Field field = parentType.getField(segment);
535            if (field == null && i == 0) {
536                // check facets
537                SchemaManager schemaManager = Framework.getService(SchemaManager.class);
538                for (String facet : getFacets()) {
539                    CompositeType facetType = schemaManager.getFacet(facet);
540                    field = facetType.getField(segment);
541                    if (field != null) {
542                        break;
543                    }
544                }
545            }
546            if (field == null && i == 0 && getProxySchemas() != null) {
547                // check proxy schemas
548                for (Schema schema : getProxySchemas()) {
549                    field = schema.getField(segment);
550                    if (field != null) {
551                        break;
552                    }
553                }
554            }
555            if (field == null) {
556                throw new PropertyNotFoundException(xpath, i == 0 ? null : "Unknown segment: " + segment);
557            }
558            String name = field.getName().getPrefixedName(); // normalize from segment
559            Type type = field.getType();
560
561            // check if we have a complex list index in the next position
562            if (i < segments.length - 1 && StringUtils.isNumeric(segments[i + 1])) {
563                int index = Integer.parseInt(segments[i + 1]);
564                i++;
565                if (!type.isListType() || ((ListType) type).getFieldType().isSimpleType()) {
566                    throw new PropertyNotFoundException(xpath, "Cannot use index after segment: " + segment);
567                }
568                List<T> list = getChildAsList(state, name);
569                if (index >= list.size()) {
570                    throw new PropertyNotFoundException(xpath, "Index out of bounds: " + index);
571                }
572                // find complex list state
573                state = list.get(index);
574                field = ((ListType) type).getField();
575                if (i == segments.length - 1) {
576                    // last segment
577                    setValueComplex(state, field, xpath, value);
578                } else {
579                    // not last segment
580                    parentType = (ComplexType) field.getType();
581                }
582                continue;
583            }
584
585            if (i == segments.length - 1) {
586                // last segment
587                setValueField(state, field, xpath, value);
588            } else {
589                // not last segment
590                if (type.isSimpleType()) {
591                    // scalar
592                    throw new PropertyNotFoundException(xpath, "Segment must be last: " + segment);
593                } else if (type.isComplexType()) {
594                    // complex property
595                    state = getChildForWrite(state, name, type);
596                    parentType = (ComplexType) type;
597                } else {
598                    // list
599                    ListType listType = (ListType) type;
600                    if (listType.isArray()) {
601                        // array of scalars
602                        throw new PropertyNotFoundException(xpath, "Segment must be last: " + segment);
603                    } else {
604                        // complex list but next segment was not numeric
605                        throw new PropertyNotFoundException(xpath, "Missing list index after segment: " + segment);
606                    }
607                }
608            }
609        }
610    }
611
612    protected void setValueField(T state, Field field, String xpath, Object value) throws PropertyException {
613        Type type = field.getType();
614        String name = field.getName().getPrefixedName(); // normalize from map key
615        name = internalName(name);
616        // TODO we could check for read-only here
617        if (type.isSimpleType()) {
618            // scalar
619            state.setSingle(name, value);
620        } else if (type.isComplexType()) {
621            // complex property
622            T childState = getChildForWrite(state, name, type);
623            setValueComplex(childState, field, xpath, value);
624        } else {
625            // array or list
626            ListType listType = (ListType) type;
627            Type fieldType = listType.getFieldType();
628            if (fieldType.isSimpleType()) {
629                // array
630                if (value instanceof List) {
631                    value = ((List<?>) value).toArray(new Object[0]);
632                }
633                state.setArray(name, (Object[]) value);
634            } else {
635                // complex list
636                if (value != null && !(value instanceof List)) {
637                    throw new PropertyException(
638                            "Expected List value for: " + name + ", got " + value.getClass().getName() + " instead");
639                }
640                @SuppressWarnings("unchecked")
641                List<Object> values = value == null ? Collections.emptyList() : (List<Object>) value;
642                updateList(state, name, listType.getField(), xpath, values);
643            }
644        }
645    }
646
647    protected void setValueComplex(T state, Field field, String xpath, Object value) throws PropertyException {
648        ComplexType complexType = (ComplexType) field.getType();
649        if (TypeConstants.isContentType(complexType)) {
650            if (value != null && !(value instanceof Blob)) {
651                throw new PropertyException(
652                        "Expected Blob value for: " + xpath + ", got " + value.getClass().getName() + " instead");
653            }
654            setValueBlob(state, (Blob) value, xpath);
655            return;
656        }
657        if (value != null && !(value instanceof Map)) {
658            throw new PropertyException(
659                    "Expected Map value for: " + xpath + ", got " + value.getClass().getName() + " instead");
660        }
661        @SuppressWarnings("unchecked")
662        Map<String, Object> map = value == null ? Collections.emptyMap() : (Map<String, Object>) value;
663        Set<String> keys = new HashSet<>(map.keySet());
664        for (Field f : complexType.getFields()) {
665            String name = f.getName().getPrefixedName();
666            keys.remove(name);
667            value = map.get(name);
668            setValueField(state, f, xpath + '/' + name, value);
669        }
670        if (!keys.isEmpty()) {
671            throw new PropertyException("Unknown key: " + keys.iterator().next() + " for " + xpath);
672        }
673    }
674
675    protected void setValueBlob(T state, Blob blob, String xpath) throws PropertyException {
676        BlobInfo blobInfo = new BlobInfo();
677        DocumentBlobManager blobManager = Framework.getService(DocumentBlobManager.class);
678        try {
679            blobInfo.key = blobManager.writeBlob(blob, this, xpath);
680        } catch (IOException e) {
681            throw new PropertyException("Cannot get blob info for: " + blob, e);
682        }
683        if (blob != null) {
684            blobInfo.filename = blob.getFilename();
685            blobInfo.mimeType = blob.getMimeType();
686            blobInfo.encoding = blob.getEncoding();
687            blobInfo.digest = blob.getDigest();
688            blobInfo.length = blob.getLength() == -1 ? null : Long.valueOf(blob.getLength());
689        }
690        setBlobInfo(state, blobInfo);
691    }
692
693    protected void setPropertyBlobData(String xpath, String string) {
694        Blob blob = string == null ? null : Blobs.createBlob(string);
695        DocumentBlobManager blobManager = Framework.getService(DocumentBlobManager.class);
696        String key;
697        try {
698            key = blobManager.writeBlob(blob, this, xpath);
699        } catch (IOException e) {
700            throw new PropertyException("Cannot write binary for doc: " + getUUID(), e);
701        }
702        setPropertyValue(xpath, key);
703    }
704
705    /**
706     * Reads state into a complex property.
707     */
708    protected void readComplexProperty(T state, ComplexProperty complexProperty) throws PropertyException {
709        readComplexProperty(state, complexProperty, null);
710    }
711
712    protected void readComplexProperty(T state, ComplexProperty complexProperty, String xpath)
713            throws PropertyException {
714        if (state == null) {
715            complexProperty.init(null);
716            return;
717        }
718        if (complexProperty instanceof BlobProperty) {
719            Blob blob = getValueBlob(state, xpath);
720            complexProperty.init((Serializable) blob);
721            return;
722        }
723        for (Property property : complexProperty) {
724            String name = property.getField().getName().getPrefixedName();
725            name = internalName(name);
726            String xp = xpath == null ? name : xpath + '/' + name;
727            Type type = property.getType();
728            if (type.isSimpleType()) {
729                // simple property
730                Object value = state.getSingle(name);
731                property.init((Serializable) value);
732            } else if (type.isComplexType()) {
733                // complex property
734                T childState = getChild(state, name, type);
735                readComplexProperty(childState, (ComplexProperty) property, xp);
736                ((ComplexProperty) property).removePhantomFlag();
737            } else {
738                ListType listType = (ListType) type;
739                if (listType.getFieldType().isSimpleType()) {
740                    // array
741                    Object[] array = state.getArray(name);
742                    array = typedArray(listType.getFieldType(), array);
743                    property.init(array);
744                } else {
745                    // complex list
746                    Field listField = listType.getField();
747                    List<T> childStates = getChildAsList(state, name);
748                    // TODO property.init(null) if null children in DBS
749                    List<Object> list = new ArrayList<>(childStates.size());
750                    int i = 0;
751                    for (T childState : childStates) {
752                        String xpi = xp + '/' + i++;
753                        ComplexProperty p = (ComplexProperty) complexProperty.getRoot()
754                                                                             .createProperty(property, listField, 0);
755                        readComplexProperty(childState, p, xpi);
756                        list.add(p.getValue());
757                    }
758                    property.init((Serializable) list);
759                }
760            }
761        }
762    }
763
764    protected static class BlobWriteInfo<T extends StateAccessor> {
765
766        public final T state;
767
768        public final Blob blob;
769
770        public final String xpath;
771
772        public BlobWriteInfo(T state, Blob blob, String xpath) {
773            this.state = state;
774            this.blob = blob;
775            this.xpath = xpath;
776        }
777    }
778
779    protected static class BlobWriteContext<T extends StateAccessor> implements WriteContext {
780
781        public final Map<BaseDocument<T>, List<BlobWriteInfo<T>>> blobWriteInfos = new HashMap<>();
782
783        public final Set<String> xpaths = new HashSet<>();
784
785        /**
786         * Records a change to a given xpath.
787         */
788        public void recordChange(String xpath) {
789            xpaths.add(xpath);
790        }
791
792        /**
793         * Records a blob update.
794         */
795        public void recordBlob(BaseDocument<T> doc, T state, Blob blob, String xpath) {
796            BlobWriteInfo<T> info = new BlobWriteInfo<>(state, blob, xpath);
797            blobWriteInfos.computeIfAbsent(doc, k -> new ArrayList<>()).add(info);
798        }
799
800        @Override
801        public Set<String> getChanges() {
802            return xpaths;
803        }
804
805        // note, in the proxy case baseDoc may be different from the doc in the map
806        @Override
807        public void flush(Document baseDoc) {
808            // first, write all updated blobs
809            for (Entry<BaseDocument<T>, List<BlobWriteInfo<T>>> es : blobWriteInfos.entrySet()) {
810                BaseDocument<T> doc = es.getKey();
811                for (BlobWriteInfo<T> info : es.getValue()) {
812                    doc.setValueBlob(info.state, info.blob, info.xpath);
813                }
814            }
815            // then inform the blob manager about the changed xpaths
816            DocumentBlobManager blobManager = Framework.getService(DocumentBlobManager.class);
817            blobManager.notifyChanges(baseDoc, xpaths);
818        }
819    }
820
821    @Override
822    public WriteContext getWriteContext() {
823        return new BlobWriteContext<T>();
824    }
825
826    /**
827     * Writes state from a complex property.
828     *
829     * @deprecated since 11.1, use
830     *             {@link #writeDocumentPart(StateAccessor, DocumentPart, org.nuxeo.ecm.core.model.Document.WriteContext, boolean)}
831     *             instead
832     */
833    @Deprecated
834    protected boolean writeComplexProperty(T state, ComplexProperty complexProperty, WriteContext writeContext)
835            throws PropertyException {
836        return writeDocumentPart(state, (DocumentPart) complexProperty, writeContext, false);
837    }
838
839    /**
840     * Writes state from a document part.
841     *
842     * @return {@code true} if something changed
843     */
844    protected boolean writeDocumentPart(T state, DocumentPart dp, WriteContext writeContext, boolean create)
845            throws PropertyException {
846        boolean writeAll = create;
847        boolean writeAllChildren = dp.getClearComplexPropertyBeforeSet();
848        return writeComplexProperty(state, (ComplexProperty) dp, null, writeAll, writeAllChildren, writeContext);
849    }
850
851    /**
852     * Writes state from a complex property.
853     * <p>
854     * Writes only properties that are dirty, unless writeAll is true in which case everything is written.
855     *
856     * @return {@code true} if something changed
857     */
858    protected boolean writeComplexProperty(T state, ComplexProperty complexProperty, String xpath, boolean writeAll,
859            boolean writeAllChildren, WriteContext wc) throws PropertyException {
860        @SuppressWarnings("unchecked")
861        BlobWriteContext<T> writeContext = (BlobWriteContext<T>) wc;
862        if (complexProperty instanceof BlobProperty) {
863            Serializable value = complexProperty.getValueForWrite();
864            if (value != null && !(value instanceof Blob)) {
865                throw new PropertyException("Cannot write a non-Blob value: " + value);
866            }
867            writeContext.recordBlob(this, state, (Blob) value, xpath);
868            return true;
869        }
870        boolean changed = false;
871        for (Property property : complexProperty) {
872            // write dirty properties, but also phantoms with non-null default values
873            // this is critical for DeltaLong updates to work, they need a non-null initial value
874            if (writeAll || property.isDirty() || property.isPhantom() && property.hasDefaultValue()) {
875                // do the write
876            } else {
877                continue;
878            }
879            String name = property.getField().getName().getPrefixedName();
880            name = internalName(name);
881            if (checkReadOnlyIgnoredWrite(property, state)) {
882                continue;
883            }
884            String xp = xpath == null ? name : xpath + '/' + name;
885            writeContext.recordChange(xp);
886            changed = true;
887
888            Type type = property.getType();
889            if (type.isSimpleType()) {
890                // simple property
891                Serializable value = property.getValueForWrite();
892                state.setSingle(name, value);
893            } else if (type.isComplexType()) {
894                // complex property
895                T childState = getChildForWrite(state, name, type);
896                writeComplexProperty(childState, (ComplexProperty) property, xp, writeAllChildren, writeAllChildren,
897                        writeContext);
898            } else {
899                ListType listType = (ListType) type;
900                if (listType.getFieldType().isSimpleType()) {
901                    // array
902                    Serializable value = property.getValueForWrite();
903                    if (value instanceof List) {
904                        List<?> list = (List<?>) value;
905                        Object[] array;
906                        // use properly-typed array, useful for mem backend that doesn't re-convert all types
907                        Class<?> klass = Object.class;
908                        for (Object o : list) {
909                            if (o != null) {
910                                klass = o.getClass();
911                                break;
912                            }
913                        }
914                        array = (Object[]) Array.newInstance(klass, list.size());
915                        value = list.toArray(array);
916                    } else if (value instanceof Object[]) {
917                        Object[] ar = (Object[]) value;
918                        if (ar.length != 0) {
919                            // use properly-typed array, useful for mem backend that doesn't re-convert all types
920                            Class<?> klass = Object.class;
921                            for (Object o : ar) {
922                                if (o != null) {
923                                    klass = o.getClass();
924                                    break;
925                                }
926                            }
927                            Object[] array;
928                            if (ar.getClass().getComponentType() == klass) {
929                                array = ar;
930                            } else {
931                                // copy to array with proper component type
932                                array = (Object[]) Array.newInstance(klass, ar.length);
933                                System.arraycopy(ar, 0, array, 0, ar.length);
934                            }
935                            value = array;
936                        }
937                    } else if (value == null) {
938                        // ok
939                    } else {
940                        throw new IllegalStateException(value.toString());
941                    }
942                    state.setArray(name, (Object[]) value);
943                } else {
944                    // complex list
945                    // update it
946                    List<T> childStates = updateList(state, name, property);
947                    // write values
948                    int i = 0;
949                    for (Property childProperty : property.getChildren()) {
950                        T childState = childStates.get(i);
951                        String xpi = xp + '/' + i;
952                        boolean moved = childProperty.isMoved();
953                        boolean c = writeComplexProperty(childState, (ComplexProperty) childProperty, xpi,
954                                writeAllChildren || moved, writeAllChildren || moved, writeContext);
955                        if (c) {
956                            writeContext.recordChange(xpi);
957                        }
958                        i++;
959                    }
960                }
961            }
962        }
963        return changed;
964    }
965
966    /**
967     * Visits all the blobs of this document and calls the passed blob visitor on each one.
968     */
969    protected void visitBlobs(T state, Consumer<BlobAccessor> blobVisitor, Runnable markDirty)
970            throws PropertyException {
971        Visit visit = new Visit(blobVisitor, markDirty);
972        // structural type
973        visit.visitBlobsComplex(state, getType());
974        // dynamic facets
975        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
976        for (String facet : getFacets()) {
977            CompositeType facetType = schemaManager.getFacet(facet);
978            if (facetType != null) { // if not obsolete facet
979                visit.visitBlobsComplex(state, facetType);
980            }
981        }
982        // proxy schemas
983        if (getProxySchemas() != null) {
984            for (Schema schema : getProxySchemas()) {
985                visit.visitBlobsComplex(state, schema);
986            }
987        }
988    }
989
990    protected class StateBlobAccessor implements BlobAccessor {
991
992        protected final Collection<String> path;
993
994        protected final T state;
995
996        protected final Runnable markDirty;
997
998        public StateBlobAccessor(Collection<String> path, T state, Runnable markDirty) {
999            this.path = path;
1000            this.state = state;
1001            this.markDirty = markDirty;
1002        }
1003
1004        @Override
1005        public String getXPath() {
1006            return StringUtils.join(path, "/");
1007        }
1008
1009        @Override
1010        public Blob getBlob() throws PropertyException {
1011            return getValueBlob(state, getXPath());
1012        }
1013
1014        @Override
1015        public void setBlob(Blob blob) throws PropertyException {
1016            // markDirty has to be called *before* we change the state
1017            markDirty.run();
1018            setValueBlob(state, blob, getXPath());
1019        }
1020    }
1021
1022    protected class Visit {
1023
1024        protected final Consumer<BlobAccessor> blobVisitor;
1025
1026        protected final Runnable markDirty;
1027
1028        protected final Deque<String> path;
1029
1030        public Visit(Consumer<BlobAccessor> blobVisitor, Runnable markDirty) {
1031            this.blobVisitor = blobVisitor;
1032            this.markDirty = markDirty;
1033            path = new ArrayDeque<>();
1034        }
1035
1036        public void visitBlobsComplex(T state, ComplexType complexType) throws PropertyException {
1037            if (TypeConstants.isContentType(complexType)) {
1038                blobVisitor.accept(new StateBlobAccessor(path, state, markDirty));
1039                return;
1040            }
1041            for (Field field : complexType.getFields()) {
1042                visitBlobsField(state, field);
1043            }
1044        }
1045
1046        protected void visitBlobsField(T state, Field field) throws PropertyException {
1047            Type type = field.getType();
1048            if (type.isSimpleType()) {
1049                // scalar
1050            } else if (type.isComplexType()) {
1051                // complex property
1052                String name = field.getName().getPrefixedName();
1053                T childState = getChild(state, name, type);
1054                if (childState != null) {
1055                    path.addLast(name);
1056                    visitBlobsComplex(childState, (ComplexType) type);
1057                    path.removeLast();
1058                }
1059            } else {
1060                // array or list
1061                Type fieldType = ((ListType) type).getFieldType();
1062                if (fieldType.isSimpleType()) {
1063                    // array
1064                } else {
1065                    // complex list
1066                    String name = field.getName().getPrefixedName();
1067                    path.addLast(name);
1068                    int i = 0;
1069                    for (T childState : getChildAsList(state, name)) {
1070                        path.addLast(String.valueOf(i++));
1071                        visitBlobsComplex(childState, (ComplexType) fieldType);
1072                        path.removeLast();
1073                    }
1074                    path.removeLast();
1075                }
1076            }
1077        }
1078    }
1079
1080    @Override
1081    public Lock getLock() {
1082        try {
1083            return getSession().getLockManager().getLock(getUUID());
1084        } catch (DocumentNotFoundException e) {
1085            return getDocumentLock();
1086        }
1087    }
1088
1089    @Override
1090    public Lock setLock(Lock lock) {
1091        if (lock == null) {
1092            throw new NullPointerException("Attempt to use null lock on: " + getUUID());
1093        }
1094        try {
1095            return getSession().getLockManager().setLock(getUUID(), lock);
1096        } catch (DocumentNotFoundException e) {
1097            return setDocumentLock(lock);
1098        }
1099    }
1100
1101    @Override
1102    public Lock removeLock(String owner) {
1103        try {
1104            return getSession().getLockManager().removeLock(getUUID(), owner);
1105        } catch (DocumentNotFoundException e) {
1106            return removeDocumentLock(owner);
1107        }
1108    }
1109
1110    /**
1111     * Gets the lock from this recently created and unsaved document.
1112     *
1113     * @return the lock, or {@code null} if no lock is set
1114     * @since 7.4
1115     */
1116    protected abstract Lock getDocumentLock();
1117
1118    /**
1119     * Sets a lock on this recently created and unsaved document.
1120     *
1121     * @param lock the lock to set
1122     * @return {@code null} if locking succeeded, or the existing lock if locking failed
1123     * @since 7.4
1124     */
1125    protected abstract Lock setDocumentLock(Lock lock);
1126
1127    /**
1128     * Removes a lock from this recently created and unsaved document.
1129     *
1130     * @param owner the owner to check, or {@code null} for no check
1131     * @return {@code null} if there was no lock or if removal succeeded, or a lock if it blocks removal due to owner
1132     *         mismatch
1133     * @since 7.4
1134     */
1135    protected abstract Lock removeDocumentLock(String owner);
1136
1137    // also used as a regexp for split
1138    public static final String TOKEN_SEP = "-";
1139
1140    /**
1141     * Builds the user-visible change token from low-level change token and system change token information.
1142     *
1143     * @param sysChangeToken the system change token
1144     * @param changeToken the change token
1145     * @return the user-visible change token
1146     * @since 9.2
1147     */
1148    public static String buildUserVisibleChangeToken(Long sysChangeToken, Long changeToken) {
1149        if (sysChangeToken == null || changeToken == null) {
1150            return null;
1151        }
1152        return sysChangeToken.toString() + TOKEN_SEP + changeToken.toString();
1153    }
1154
1155    /**
1156     * Validates that the passed user-visible change token is compatible with the current change token.
1157     *
1158     * @param sysChangeToken the system change token
1159     * @param changeToken the change token
1160     * @param userVisibleChangeToken the user-visible change token
1161     * @return {@code false} if the change token is not valid
1162     * @since 9.2
1163     */
1164    public static boolean validateUserVisibleChangeToken(Long sysChangeToken, Long changeToken,
1165            String userVisibleChangeToken) {
1166        if (sysChangeToken == null || changeToken == null) {
1167            return true;
1168        }
1169        // we only compare the change token, not the system change token, to allow background system updates
1170        String[] parts = userVisibleChangeToken.split(TOKEN_SEP);
1171        if (parts.length != 2) {
1172            return false; // invalid format
1173        }
1174        return parts[1].equals(changeToken.toString());
1175    }
1176
1177    /**
1178     * Validates that the passed user-visible change token is compatible with the current legacy change token.
1179     *
1180     * @param modified the {@code dc:modified} timestamp
1181     * @param userVisibleChangeToken the user-visible change token
1182     * @return {@code false} if the change token is not valid
1183     * @since 9.2
1184     */
1185    protected boolean validateLegacyChangeToken(Calendar modified, String userVisibleChangeToken) {
1186        if (modified == null) {
1187            return true;
1188        }
1189        return userVisibleChangeToken.equals(String.valueOf(modified.getTimeInMillis()));
1190    }
1191
1192    /**
1193     * Gets the legacy change token for the given timestamp.
1194     *
1195     * @param modified the {@code dc:modified} timestamp
1196     * @return the legacy change token
1197     * @since 9.2
1198     */
1199    protected String getLegacyChangeToken(Calendar modified) {
1200        if (modified == null) {
1201            return null;
1202        }
1203        return String.valueOf(modified.getTimeInMillis());
1204    }
1205
1206    /**
1207     * Updates a change token to its new value.
1208     *
1209     * @param changeToken the change token (not {@code null})
1210     * @return the updated change token
1211     * @since 9.2
1212     */
1213    public static Long updateChangeToken(Long changeToken) {
1214        return Long.valueOf(changeToken.longValue() + 1);
1215    }
1216
1217    @Override
1218    public boolean isUnderRetentionOrLegalHold() {
1219        Calendar retainUntil;
1220        return hasLegalHold()
1221                || (((retainUntil = getRetainUntil()) != null) && Calendar.getInstance().before(retainUntil));
1222    }
1223
1224    protected boolean allowNewRetention(Calendar current, Calendar retainUntil) {
1225        if (current == null) {
1226            return true;
1227        }
1228        if (current.compareTo(CoreSession.RETAIN_UNTIL_INDETERMINATE) == 0) {
1229            return true;
1230        }
1231        if (retainUntil == null) {
1232            // setting back to null is allowed if retention has already expired
1233            return Calendar.getInstance().after(current);
1234        }
1235        // can only extend retention
1236        return retainUntil.after(current);
1237    }
1238
1239}