001/*
002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     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.Set;
037import java.util.function.Consumer;
038import java.util.regex.Pattern;
039
040import org.apache.commons.lang.ArrayUtils;
041import org.apache.commons.lang.StringUtils;
042import org.apache.commons.lang3.tuple.Pair;
043import org.nuxeo.ecm.core.api.Blob;
044import org.nuxeo.ecm.core.api.DocumentNotFoundException;
045import org.nuxeo.ecm.core.api.Lock;
046import org.nuxeo.ecm.core.api.PropertyException;
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.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) && !(property.getField().getDeclaringType() instanceof Schema
276                && ((Schema) property.getField().getDeclaringType()).isVersionWritabe())) {
277            throw new PropertyException("Cannot set property on a version: " + name);
278        }
279        // ignore if value is unchanged (only for dublincore)
280        // dublincore contains only scalars and arrays
281        Object value = property.getValueForWrite();
282        Object oldValue;
283        if (property.getType().isSimpleType()) {
284            oldValue = state.getSingle(name);
285        } else {
286            oldValue = state.getArray(name);
287        }
288        if (!ArrayUtils.isEquals(value, oldValue)) {
289            // do write
290            return false;
291        }
292        // ignore attempt to write identical value
293        return true;
294    }
295
296    protected BlobInfo getBlobInfo(T state) throws PropertyException {
297        BlobInfo blobInfo = new BlobInfo();
298        blobInfo.key = (String) state.getSingle(BLOB_DATA);
299        blobInfo.filename = (String) state.getSingle(BLOB_NAME);
300        blobInfo.mimeType = (String) state.getSingle(BLOB_MIME_TYPE);
301        blobInfo.encoding = (String) state.getSingle(BLOB_ENCODING);
302        blobInfo.digest = (String) state.getSingle(BLOB_DIGEST);
303        blobInfo.length = (Long) state.getSingle(BLOB_LENGTH);
304        return blobInfo;
305    }
306
307    protected void setBlobInfo(T state, BlobInfo blobInfo) throws PropertyException {
308        state.setSingle(BLOB_DATA, blobInfo.key);
309        state.setSingle(BLOB_NAME, blobInfo.filename);
310        state.setSingle(BLOB_MIME_TYPE, blobInfo.mimeType);
311        state.setSingle(BLOB_ENCODING, blobInfo.encoding);
312        state.setSingle(BLOB_DIGEST, blobInfo.digest);
313        state.setSingle(BLOB_LENGTH, blobInfo.length);
314    }
315
316    /**
317     * Gets a value (may be complex/list) from the document at the given xpath.
318     */
319    protected Object getValueObject(T state, String xpath) throws PropertyException {
320        xpath = canonicalXPath(xpath);
321        String[] segments = xpath.split("/");
322
323        /*
324         * During this loop state may become null if we read an uninitialized complex property (DBS), in that case the
325         * code must treat it as reading uninitialized values for its children.
326         */
327        ComplexType parentType = getType();
328        for (int i = 0; i < segments.length; i++) {
329            String segment = segments[i];
330            Field field = parentType.getField(segment);
331            if (field == null && i == 0) {
332                // check facets
333                SchemaManager schemaManager = Framework.getService(SchemaManager.class);
334                for (String facet : getFacets()) {
335                    CompositeType facetType = schemaManager.getFacet(facet);
336                    field = facetType.getField(segment);
337                    if (field != null) {
338                        break;
339                    }
340                }
341            }
342            if (field == null && i == 0 && getProxySchemas() != null) {
343                // check proxy schemas
344                for (Schema schema : getProxySchemas()) {
345                    field = schema.getField(segment);
346                    if (field != null) {
347                        break;
348                    }
349                }
350            }
351            if (field == null) {
352                throw new PropertyNotFoundException(xpath, i == 0 ? null : "Unknown segment: " + segment);
353            }
354            String name = field.getName().getPrefixedName(); // normalize from segment
355            Type type = field.getType();
356
357            // check if we have a complex list index in the next position
358            if (i < segments.length - 1 && StringUtils.isNumeric(segments[i + 1])) {
359                int index = Integer.parseInt(segments[i + 1]);
360                i++;
361                if (!type.isListType() || ((ListType) type).getFieldType().isSimpleType()) {
362                    throw new PropertyNotFoundException(xpath, "Cannot use index after segment: " + segment);
363                }
364                List<T> list = state == null ? Collections.emptyList() : getChildAsList(state, name);
365                if (index >= list.size()) {
366                    throw new PropertyNotFoundException(xpath, "Index out of bounds: " + index);
367                }
368                // find complex list state
369                state = list.get(index);
370                parentType = (ComplexType) ((ListType) type).getFieldType();
371                if (i == segments.length - 1) {
372                    // last segment
373                    return getValueComplex(state, parentType);
374                } else {
375                    // not last segment
376                    continue;
377                }
378            }
379
380            if (i == segments.length - 1) {
381                // last segment
382                return state == null ? null : getValueField(state, field);
383            } else {
384                // not last segment
385                if (type.isSimpleType()) {
386                    // scalar
387                    throw new PropertyNotFoundException(xpath, "Segment must be last: " + segment);
388                } else if (type.isComplexType()) {
389                    // complex property
390                    state = state == null ? null : getChild(state, name, type);
391                    // here state can be null (DBS), continue loop with it, meaning uninitialized for read
392                    parentType = (ComplexType) type;
393                } else {
394                    // list
395                    ListType listType = (ListType) type;
396                    if (listType.isArray()) {
397                        // array of scalars
398                        throw new PropertyNotFoundException(xpath, "Segment must be last: " + segment);
399                    } else {
400                        // complex list but next segment was not numeric
401                        throw new PropertyNotFoundException(xpath, "Missing list index after segment: " + segment);
402                    }
403                }
404            }
405        }
406        throw new AssertionError("not reached");
407    }
408
409    protected Object getValueField(T state, Field field) throws PropertyException {
410        Type type = field.getType();
411        String name = field.getName().getPrefixedName();
412        name = internalName(name);
413        if (type.isSimpleType()) {
414            // scalar
415            return state.getSingle(name);
416        } else if (type.isComplexType()) {
417            // complex property
418            T childState = getChild(state, name, type);
419            if (childState == null) {
420                return null;
421            }
422            return getValueComplex(childState, (ComplexType) type);
423        } else {
424            // array or list
425            Type fieldType = ((ListType) type).getFieldType();
426            if (fieldType.isSimpleType()) {
427                // array
428                return state.getArray(name);
429            } else {
430                // complex list
431                List<T> childStates = getChildAsList(state, name);
432                List<Object> list = new ArrayList<>(childStates.size());
433                for (T childState : childStates) {
434                    Object value = getValueComplex(childState, (ComplexType) fieldType);
435                    list.add(value);
436                }
437                return list;
438            }
439        }
440    }
441
442    protected Object getValueComplex(T state, ComplexType complexType) throws PropertyException {
443        if (TypeConstants.isContentType(complexType)) {
444            return getValueBlob(state);
445        }
446        Map<String, Object> map = new HashMap<>();
447        for (Field field : complexType.getFields()) {
448            String name = field.getName().getPrefixedName();
449            Object value = getValueField(state, field);
450            map.put(name, value);
451        }
452        return map;
453    }
454
455    protected Blob getValueBlob(T state) throws PropertyException {
456        BlobInfo blobInfo = getBlobInfo(state);
457        BlobManager blobManager = Framework.getService(BlobManager.class);
458        try {
459            return blobManager.readBlob(blobInfo, getRepositoryName());
460        } catch (IOException e) {
461            throw new PropertyException("Cannot get blob info for: " + blobInfo.key, e);
462        }
463    }
464
465    /**
466     * Sets a value (may be complex/list) into the document at the given xpath.
467     */
468    protected void setValueObject(T state, String xpath, Object value) throws PropertyException {
469        xpath = canonicalXPath(xpath);
470        String[] segments = xpath.split("/");
471
472        ComplexType parentType = getType();
473        for (int i = 0; i < segments.length; i++) {
474            String segment = segments[i];
475            Field field = parentType.getField(segment);
476            if (field == null && i == 0) {
477                // check facets
478                SchemaManager schemaManager = Framework.getService(SchemaManager.class);
479                for (String facet : getFacets()) {
480                    CompositeType facetType = schemaManager.getFacet(facet);
481                    field = facetType.getField(segment);
482                    if (field != null) {
483                        break;
484                    }
485                }
486            }
487            if (field == null && i == 0 && getProxySchemas() != null) {
488                // check proxy schemas
489                for (Schema schema : getProxySchemas()) {
490                    field = schema.getField(segment);
491                    if (field != null) {
492                        break;
493                    }
494                }
495            }
496            if (field == null) {
497                throw new PropertyNotFoundException(xpath, i == 0 ? null : "Unknown segment: " + segment);
498            }
499            String name = field.getName().getPrefixedName(); // normalize from segment
500            Type type = field.getType();
501
502            // check if we have a complex list index in the next position
503            if (i < segments.length - 1 && StringUtils.isNumeric(segments[i + 1])) {
504                int index = Integer.parseInt(segments[i + 1]);
505                i++;
506                if (!type.isListType() || ((ListType) type).getFieldType().isSimpleType()) {
507                    throw new PropertyNotFoundException(xpath, "Cannot use index after segment: " + segment);
508                }
509                List<T> list = getChildAsList(state, name);
510                if (index >= list.size()) {
511                    throw new PropertyNotFoundException(xpath, "Index out of bounds: " + index);
512                }
513                // find complex list state
514                state = list.get(index);
515                field = ((ListType) type).getField();
516                if (i == segments.length - 1) {
517                    // last segment
518                    setValueComplex(state, field, value);
519                } else {
520                    // not last segment
521                    parentType = (ComplexType) field.getType();
522                }
523                continue;
524            }
525
526            if (i == segments.length - 1) {
527                // last segment
528                setValueField(state, field, value);
529            } else {
530                // not last segment
531                if (type.isSimpleType()) {
532                    // scalar
533                    throw new PropertyNotFoundException(xpath, "Segment must be last: " + segment);
534                } else if (type.isComplexType()) {
535                    // complex property
536                    state = getChildForWrite(state, name, type);
537                    parentType = (ComplexType) type;
538                } else {
539                    // list
540                    ListType listType = (ListType) type;
541                    if (listType.isArray()) {
542                        // array of scalars
543                        throw new PropertyNotFoundException(xpath, "Segment must be last: " + segment);
544                    } else {
545                        // complex list but next segment was not numeric
546                        throw new PropertyNotFoundException(xpath, "Missing list index after segment: " + segment);
547                    }
548                }
549            }
550        }
551    }
552
553    protected void setValueField(T state, Field field, Object value) throws PropertyException {
554        Type type = field.getType();
555        String name = field.getName().getPrefixedName(); // normalize from map key
556        name = internalName(name);
557        // TODO we could check for read-only here
558        if (type.isSimpleType()) {
559            // scalar
560            state.setSingle(name, value);
561        } else if (type.isComplexType()) {
562            // complex property
563            T childState = getChildForWrite(state, name, type);
564            setValueComplex(childState, field, value);
565        } else {
566            // array or list
567            ListType listType = (ListType) type;
568            Type fieldType = listType.getFieldType();
569            if (fieldType.isSimpleType()) {
570                // array
571                if (value instanceof List) {
572                    value = ((List<?>) value).toArray(new Object[0]);
573                }
574                state.setArray(name, (Object[]) value);
575            } else {
576                // complex list
577                if (value != null && !(value instanceof List)) {
578                    throw new PropertyException(
579                            "Expected List value for: " + name + ", got " + value.getClass().getName() + " instead");
580                }
581                @SuppressWarnings("unchecked")
582                List<Object> values = value == null ? Collections.emptyList() : (List<Object>) value;
583                updateList(state, name, values, listType.getField());
584            }
585        }
586    }
587
588    // pass field instead of just type for better error messages
589    protected void setValueComplex(T state, Field field, Object value) throws PropertyException {
590        ComplexType complexType = (ComplexType) field.getType();
591        if (TypeConstants.isContentType(complexType)) {
592            if (value != null && !(value instanceof Blob)) {
593                throw new PropertyException("Expected Blob value for: " + field.getName().getPrefixedName() + ", got "
594                        + value.getClass().getName() + " instead");
595            }
596            setValueBlob(state, (Blob) value);
597            return;
598        }
599        if (value != null && !(value instanceof Map)) {
600            throw new PropertyException("Expected Map value for: " + field.getName().getPrefixedName() + ", got "
601                    + value.getClass().getName() + " instead");
602        }
603        @SuppressWarnings("unchecked")
604        Map<String, Object> map = value == null ? Collections.emptyMap() : (Map<String, Object>) value;
605        Set<String> keys = new HashSet<>(map.keySet());
606        for (Field f : complexType.getFields()) {
607            String name = f.getName().getPrefixedName();
608            keys.remove(name);
609            value = map.get(name);
610            setValueField(state, f, value);
611        }
612        if (!keys.isEmpty()) {
613            throw new PropertyException(
614                    "Unknown key: " + keys.iterator().next() + " for " + field.getName().getPrefixedName());
615        }
616    }
617
618    protected void setValueBlob(T state, Blob blob) throws PropertyException {
619        BlobInfo blobInfo = new BlobInfo();
620        if (blob != null) {
621            BlobManager blobManager = Framework.getService(BlobManager.class);
622            try {
623                blobInfo.key = blobManager.writeBlob(blob, this);
624            } catch (IOException e) {
625                throw new PropertyException("Cannot get blob info for: " + blob, e);
626            }
627            blobInfo.filename = blob.getFilename();
628            blobInfo.mimeType = blob.getMimeType();
629            blobInfo.encoding = blob.getEncoding();
630            blobInfo.digest = blob.getDigest();
631            blobInfo.length = blob.getLength() == -1 ? null : Long.valueOf(blob.getLength());
632        }
633        setBlobInfo(state, blobInfo);
634    }
635
636    /**
637     * Reads state into a complex property.
638     */
639    protected void readComplexProperty(T state, ComplexProperty complexProperty) throws PropertyException {
640        if (state == null) {
641            complexProperty.init(null);
642            return;
643        }
644        if (complexProperty instanceof BlobProperty) {
645            Blob blob = getValueBlob(state);
646            complexProperty.init((Serializable) blob);
647            return;
648        }
649        for (Property property : complexProperty) {
650            String name = property.getField().getName().getPrefixedName();
651            name = internalName(name);
652            Type type = property.getType();
653            if (type.isSimpleType()) {
654                // simple property
655                Object value = state.getSingle(name);
656                property.init((Serializable) value);
657            } else if (type.isComplexType()) {
658                // complex property
659                T childState = getChild(state, name, type);
660                readComplexProperty(childState, (ComplexProperty) property);
661                ((ComplexProperty) property).removePhantomFlag();
662            } else {
663                ListType listType = (ListType) type;
664                if (listType.getFieldType().isSimpleType()) {
665                    // array
666                    Object[] array = state.getArray(name);
667                    array = typedArray(listType.getFieldType(), array);
668                    property.init(array);
669                } else {
670                    // complex list
671                    Field listField = listType.getField();
672                    List<T> childStates = getChildAsList(state, name);
673                    // TODO property.init(null) if null children in DBS
674                    List<Object> list = new ArrayList<>(childStates.size());
675                    for (T childState : childStates) {
676                        ComplexProperty p = (ComplexProperty) complexProperty.getRoot().createProperty(property,
677                                listField, 0);
678                        readComplexProperty(childState, p);
679                        list.add(p.getValue());
680                    }
681                    property.init((Serializable) list);
682                }
683            }
684        }
685    }
686
687    protected static class BlobWriteContext<T extends StateAccessor> implements WriteContext {
688
689        public final Map<BaseDocument<T>, List<Pair<T, Blob>>> blobWriteInfosPerDoc = new HashMap<>();
690
691        public final Set<String> xpaths = new HashSet<>();
692
693        /**
694         * Records a change to a given xpath.
695         */
696        public void recordChange(String xpath) {
697            xpaths.add(xpath);
698        }
699
700        /**
701         * Records a blob update.
702         */
703        public void recordBlob(T state, Blob blob, BaseDocument<T> doc) {
704            List<Pair<T, Blob>> list = blobWriteInfosPerDoc.get(doc);
705            if (list == null) {
706                blobWriteInfosPerDoc.put(doc, list = new ArrayList<>());
707            }
708            list.add(Pair.of(state, blob));
709        }
710
711        @Override
712        public Set<String> getChanges() {
713            return xpaths;
714        }
715
716        // note, in the proxy case baseDoc may be different from the doc in the map
717        @Override
718        public void flush(Document baseDoc) {
719            // first, write all updated blobs
720            for (Entry<BaseDocument<T>, List<Pair<T, Blob>>> en : blobWriteInfosPerDoc.entrySet()) {
721                BaseDocument<T> doc = en.getKey();
722                for (Pair<T, Blob> pair : en.getValue()) {
723                    T state = pair.getLeft();
724                    Blob blob = pair.getRight();
725                    doc.setValueBlob(state, blob);
726                }
727            }
728            // then inform the blob manager about the changed xpaths
729            BlobManager blobManager = Framework.getService(BlobManager.class);
730            blobManager.notifyChanges(baseDoc, xpaths);
731        }
732    }
733
734    @Override
735    public WriteContext getWriteContext() {
736        return new BlobWriteContext<T>();
737    }
738
739    /**
740     * Writes state from a complex property.
741     *
742     * @return {@code true} if something changed
743     */
744    protected boolean writeComplexProperty(T state, ComplexProperty complexProperty, WriteContext writeContext)
745            throws PropertyException {
746        return writeComplexProperty(state, complexProperty, null, false, writeContext);
747    }
748
749    /**
750     * Writes state from a complex property.
751     * <p>
752     * Writes only properties that are dirty, unless skipDirtyCheck is true in which case everything is written.
753     *
754     * @return {@code true} if something changed
755     */
756    protected boolean writeComplexProperty(T state, ComplexProperty complexProperty, String xpath,
757            boolean skipDirtyCheck, WriteContext wc) throws PropertyException {
758        @SuppressWarnings("unchecked")
759        BlobWriteContext<T> writeContext = (BlobWriteContext<T>) wc;
760        if (complexProperty instanceof BlobProperty) {
761            Serializable value = ((BlobProperty) complexProperty).getValueForWrite();
762            if (value != null && !(value instanceof Blob)) {
763                throw new PropertyException("Cannot write a non-Blob value: " + value);
764            }
765            writeContext.recordBlob(state, (Blob) value, this);
766            return true;
767        }
768        boolean changed = false;
769        for (Property property : complexProperty) {
770            // write dirty properties, but also phantoms with non-null default values
771            // this is critical for DeltaLong updates to work, they need a non-null initial value
772            if (skipDirtyCheck || property.isDirty()
773                    || (property.isPhantom() && property.getField().getDefaultValue() != null)) {
774                // do the write
775            } else {
776                continue;
777            }
778            String name = property.getField().getName().getPrefixedName();
779            name = internalName(name);
780            if (checkReadOnlyIgnoredWrite(property, state)) {
781                continue;
782            }
783            String xp = xpath == null ? name : xpath + '/' + name;
784            writeContext.recordChange(xp);
785            changed = true;
786
787            Type type = property.getType();
788            if (type.isSimpleType()) {
789                // simple property
790                Serializable value = property.getValueForWrite();
791                state.setSingle(name, value);
792            } else if (type.isComplexType()) {
793                // complex property
794                T childState = getChildForWrite(state, name, type);
795                writeComplexProperty(childState, (ComplexProperty) property, xp, skipDirtyCheck, writeContext);
796            } else {
797                ListType listType = (ListType) type;
798                if (listType.getFieldType().isSimpleType()) {
799                    // array
800                    Serializable value = property.getValueForWrite();
801                    if (value instanceof List) {
802                        List<?> list = (List<?>) value;
803                        Object[] array;
804                        if (list.isEmpty()) {
805                            array = new Object[0];
806                        } else {
807                            // use properly-typed array, useful for mem backend that doesn't re-convert all types
808                            Class<?> klass = list.get(0).getClass();
809                            array = (Object[]) Array.newInstance(klass, list.size());
810                        }
811                        value = list.toArray(array);
812                    } else if (value instanceof Object[]) {
813                        Object[] ar = (Object[]) value;
814                        if (ar.length != 0) {
815                            // use properly-typed array, useful for mem backend that doesn't re-convert all types
816                            Class<?> klass = Object.class;
817                            for (Object o : ar) {
818                                if (o != null) {
819                                    klass = o.getClass();
820                                    break;
821                                }
822                            }
823                            Object[] array;
824                            if (ar.getClass().getComponentType() == klass) {
825                                array = ar;
826                            } else {
827                                // copy to array with proper component type
828                                array = (Object[]) Array.newInstance(klass, ar.length);
829                                System.arraycopy(ar, 0, array, 0, ar.length);
830                            }
831                            value = array;
832                        }
833                    } else if (value == null) {
834                        // ok
835                    } else {
836                        throw new IllegalStateException(value.toString());
837                    }
838                    state.setArray(name, (Object[]) value);
839                } else {
840                    // complex list
841                    // update it
842                    List<T> childStates = updateList(state, name, property);
843                    // write values
844                    int i = 0;
845                    for (Property childProperty : property.getChildren()) {
846                        T childState = childStates.get(i);
847                        String xpi = xp + '/' + i;
848                        boolean moved = childProperty.isMoved();
849                        boolean c = writeComplexProperty(childState, (ComplexProperty) childProperty, xpi,
850                                skipDirtyCheck || moved , writeContext);
851                        if (c) {
852                            writeContext.recordChange(xpi);
853                        }
854                        i++;
855                    }
856                }
857            }
858        }
859        return changed;
860    }
861
862    /**
863     * Reads prefetched values.
864     */
865    protected Map<String, Serializable> readPrefetch(T state, ComplexType complexType, Set<String> xpaths)
866            throws PropertyException {
867        // augment xpaths with all prefixes, to cut short recursive search
868        Set<String> prefixes = new HashSet<>();
869        for (String xpath : xpaths) {
870            for (;;) {
871                // add as prefix
872                if (!prefixes.add(xpath)) {
873                    // already present, we can stop
874                    break;
875                }
876                // loop with its prefix
877                int i = xpath.lastIndexOf('/');
878                if (i == -1) {
879                    break;
880                }
881                xpath = xpath.substring(0, i);
882            }
883        }
884        Map<String, Serializable> prefetch = new HashMap<String, Serializable>();
885        readPrefetch(state, complexType, null, null, prefixes, prefetch);
886        return prefetch;
887    }
888
889    protected void readPrefetch(T state, ComplexType complexType, String xpathGeneric, String xpath,
890            Set<String> prefixes, Map<String, Serializable> prefetch) throws PropertyException {
891        if (TypeConstants.isContentType(complexType)) {
892            if (!prefixes.contains(xpathGeneric)) {
893                return;
894            }
895            Blob blob = getValueBlob(state);
896            prefetch.put(xpath, (Serializable) blob);
897            return;
898        }
899        for (Field field : complexType.getFields()) {
900            readPrefetchField(state, field, xpathGeneric, xpath, prefixes, prefetch);
901        }
902    }
903
904    protected void readPrefetchField(T state, Field field, String xpathGeneric, String xpath, Set<String> prefixes,
905            Map<String, Serializable> prefetch) {
906        String name = field.getName().getPrefixedName();
907        Type type = field.getType();
908        xpathGeneric = xpathGeneric == null ? name : xpathGeneric + '/' + name;
909        xpath = xpath == null ? name : xpath + '/' + name;
910        if (!prefixes.contains(xpathGeneric)) {
911            return;
912        }
913        if (type.isSimpleType()) {
914            // scalar
915            Object value = state.getSingle(name);
916            prefetch.put(xpath, (Serializable) value);
917        } else if (type.isComplexType()) {
918            // complex property
919            T childState = getChild(state, name, type);
920            if (childState != null) {
921                readPrefetch(childState, (ComplexType) type, xpathGeneric, xpath, prefixes, prefetch);
922            }
923        } else {
924            // array or list
925            ListType listType = (ListType) type;
926            if (listType.getFieldType().isSimpleType()) {
927                // array
928                Object[] value = state.getArray(name);
929                prefetch.put(xpath, value);
930            } else {
931                // complex list
932                List<T> childStates = getChildAsList(state, name);
933                Field listField = listType.getField();
934                xpathGeneric += "/*";
935                int i = 0;
936                for (T childState : childStates) {
937                    readPrefetch(childState, (ComplexType) listField.getType(), xpathGeneric, xpath + '/' + i++,
938                            prefixes, prefetch);
939                }
940            }
941        }
942    }
943
944    /**
945     * Visits all the blobs of this document and calls the passed blob visitor on each one.
946     */
947    protected void visitBlobs(T state, Consumer<BlobAccessor> blobVisitor, Runnable markDirty)
948            throws PropertyException {
949        Visit visit = new Visit(blobVisitor, markDirty);
950        // structural type
951        visit.visitBlobsComplex(state, getType());
952        // dynamic facets
953        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
954        for (String facet : getFacets()) {
955            CompositeType facetType = schemaManager.getFacet(facet);
956            if (facetType != null) { // if not obsolete facet
957                visit.visitBlobsComplex(state, facetType);
958            }
959        }
960        // proxy schemas
961        if (getProxySchemas() != null) {
962            for (Schema schema : getProxySchemas()) {
963                visit.visitBlobsComplex(state, schema);
964            }
965        }
966    }
967
968    protected class StateBlobAccessor implements BlobAccessor {
969
970        protected final Collection<String> path;
971
972        protected final T state;
973
974        protected final Runnable markDirty;
975
976        public StateBlobAccessor(Collection<String> path, T state, Runnable markDirty) {
977            this.path = path;
978            this.state = state;
979            this.markDirty = markDirty;
980        }
981
982        @Override
983        public String getXPath() {
984            return StringUtils.join(path, "/");
985        }
986
987        @Override
988        public Blob getBlob() throws PropertyException {
989            return getValueBlob(state);
990        }
991
992        @Override
993        public void setBlob(Blob blob) throws PropertyException {
994            markDirty.run();
995            setValueBlob(state, blob);
996        }
997    }
998
999    protected class Visit {
1000
1001        protected final Consumer<BlobAccessor> blobVisitor;
1002
1003        protected final Runnable markDirty;
1004
1005        protected final Deque<String> path;
1006
1007        public Visit(Consumer<BlobAccessor> blobVisitor, Runnable markDirty) {
1008            this.blobVisitor = blobVisitor;
1009            this.markDirty = markDirty;
1010            path = new ArrayDeque<>();
1011        }
1012
1013        public void visitBlobsComplex(T state, ComplexType complexType) throws PropertyException {
1014            if (TypeConstants.isContentType(complexType)) {
1015                blobVisitor.accept(new StateBlobAccessor(path, state, markDirty));
1016                return;
1017            }
1018            for (Field field : complexType.getFields()) {
1019                visitBlobsField(state, field);
1020            }
1021        }
1022
1023        protected void visitBlobsField(T state, Field field) throws PropertyException {
1024            Type type = field.getType();
1025            if (type.isSimpleType()) {
1026                // scalar
1027            } else if (type.isComplexType()) {
1028                // complex property
1029                String name = field.getName().getPrefixedName();
1030                T childState = getChild(state, name, type);
1031                if (childState != null) {
1032                    path.addLast(name);
1033                    visitBlobsComplex(childState, (ComplexType) type);
1034                    path.removeLast();
1035                }
1036            } else {
1037                // array or list
1038                Type fieldType = ((ListType) type).getFieldType();
1039                if (fieldType.isSimpleType()) {
1040                    // array
1041                } else {
1042                    // complex list
1043                    String name = field.getName().getPrefixedName();
1044                    path.addLast(name);
1045                    int i = 0;
1046                    for (T childState : getChildAsList(state, name)) {
1047                        path.addLast(String.valueOf(i++));
1048                        visitBlobsComplex(childState, (ComplexType) fieldType);
1049                        path.removeLast();
1050                    }
1051                    path.removeLast();
1052                }
1053            }
1054        }
1055    }
1056
1057    @Override
1058    public Lock getLock() {
1059        try {
1060            return getSession().getLockManager().getLock(getUUID());
1061        } catch (DocumentNotFoundException e) {
1062            return getDocumentLock();
1063        }
1064    }
1065
1066    @Override
1067    public Lock setLock(Lock lock) {
1068        if (lock == null) {
1069            throw new NullPointerException("Attempt to use null lock on: " + getUUID());
1070        }
1071        try {
1072            return getSession().getLockManager().setLock(getUUID(), lock);
1073        } catch (DocumentNotFoundException e) {
1074            return setDocumentLock(lock);
1075        }
1076    }
1077
1078    @Override
1079    public Lock removeLock(String owner) {
1080        try {
1081            return getSession().getLockManager().removeLock(getUUID(), owner);
1082        } catch (DocumentNotFoundException e) {
1083            return removeDocumentLock(owner);
1084        }
1085    }
1086
1087    /**
1088     * Gets the lock from this recently created and unsaved document.
1089     *
1090     * @return the lock, or {@code null} if no lock is set
1091     * @since 7.4
1092     */
1093    protected abstract Lock getDocumentLock();
1094
1095    /**
1096     * Sets a lock on this recently created and unsaved document.
1097     *
1098     * @param lock the lock to set
1099     * @return {@code null} if locking succeeded, or the existing lock if locking failed
1100     * @since 7.4
1101     */
1102    protected abstract Lock setDocumentLock(Lock lock);
1103
1104    /**
1105     * Removes a lock from this recently created and unsaved document.
1106     *
1107     * @param the owner to check, or {@code null} for no check
1108     * @return {@code null} if there was no lock or if removal succeeded, or a lock if it blocks removal due to owner
1109     *         mismatch
1110     * @since 7.4
1111     */
1112    protected abstract Lock removeDocumentLock(String owner);
1113
1114}