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