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