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)) {
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                property.init((Serializable) value);
656            } else if (type.isComplexType()) {
657                // complex property
658                T childState = getChild(state, name, type);
659                readComplexProperty(childState, (ComplexProperty) property);
660                ((ComplexProperty) property).removePhantomFlag();
661            } else {
662                ListType listType = (ListType) type;
663                if (listType.getFieldType().isSimpleType()) {
664                    // array
665                    Object[] array = state.getArray(name);
666                    array = typedArray(listType.getFieldType(), array);
667                    property.init(array);
668                } else {
669                    // complex list
670                    Field listField = listType.getField();
671                    List<T> childStates = getChildAsList(state, name);
672                    // TODO property.init(null) if null children in DBS
673                    List<Object> list = new ArrayList<>(childStates.size());
674                    for (T childState : childStates) {
675                        ComplexProperty p = (ComplexProperty) complexProperty.getRoot().createProperty(property,
676                                listField, 0);
677                        readComplexProperty(childState, p);
678                        list.add(p.getValue());
679                    }
680                    property.init((Serializable) list);
681                }
682            }
683        }
684    }
685
686    protected static class BlobWriteContext<T extends StateAccessor> implements WriteContext {
687
688        public final Map<BaseDocument<T>, List<Pair<T, Blob>>> blobWriteInfosPerDoc = new HashMap<>();
689
690        public final Set<String> xpaths = new HashSet<>();
691
692        /**
693         * Records a change to a given xpath.
694         */
695        public void recordChange(String xpath) {
696            xpaths.add(xpath);
697        }
698
699        /**
700         * Records a blob update.
701         */
702        public void recordBlob(T state, Blob blob, BaseDocument<T> doc) {
703            List<Pair<T, Blob>> list = blobWriteInfosPerDoc.get(doc);
704            if (list == null) {
705                blobWriteInfosPerDoc.put(doc, list = new ArrayList<>());
706            }
707            list.add(Pair.of(state, blob));
708        }
709
710        @Override
711        public Set<String> getChanges() {
712            return xpaths;
713        }
714
715        // note, in the proxy case baseDoc may be different from the doc in the map
716        @Override
717        public void flush(Document baseDoc) {
718            // first, write all updated blobs
719            for (Entry<BaseDocument<T>, List<Pair<T, Blob>>> en : blobWriteInfosPerDoc.entrySet()) {
720                BaseDocument<T> doc = en.getKey();
721                for (Pair<T, Blob> pair : en.getValue()) {
722                    T state = pair.getLeft();
723                    Blob blob = pair.getRight();
724                    doc.setValueBlob(state, blob);
725                }
726            }
727            // then inform the blob manager about the changed xpaths
728            BlobManager blobManager = Framework.getService(BlobManager.class);
729            blobManager.notifyChanges(baseDoc, xpaths);
730        }
731    }
732
733    @Override
734    public WriteContext getWriteContext() {
735        return new BlobWriteContext<T>();
736    }
737
738    /**
739     * Writes state from a complex property.
740     *
741     * @return {@code true} if something changed
742     */
743    protected boolean writeComplexProperty(T state, ComplexProperty complexProperty, WriteContext writeContext)
744            throws PropertyException {
745        return writeComplexProperty(state, complexProperty, null, writeContext);
746    }
747
748    /**
749     * Writes state from a complex property.
750     * <p>
751     * Writes only properties that are dirty.
752     *
753     * @return {@code true} if something changed
754     */
755    protected boolean writeComplexProperty(T state, ComplexProperty complexProperty, String xpath, WriteContext wc)
756            throws PropertyException {
757        @SuppressWarnings("unchecked")
758        BlobWriteContext<T> writeContext = (BlobWriteContext<T>) wc;
759        if (complexProperty instanceof BlobProperty) {
760            Serializable value = ((BlobProperty) complexProperty).getValueForWrite();
761            if (value != null && !(value instanceof Blob)) {
762                throw new PropertyException("Cannot write a non-Blob value: " + value);
763            }
764            writeContext.recordBlob(state, (Blob) value, this);
765            return true;
766        }
767        boolean changed = false;
768        for (Property property : complexProperty) {
769            // write dirty properties, but also phantoms with non-null default values
770            // this is critical for DeltaLong updates to work, they need a non-null initial value
771            if (property.isDirty() || (property.isPhantom() && property.getField().getDefaultValue() != null)) {
772                // do the write
773            } else {
774                continue;
775            }
776            String name = property.getField().getName().getPrefixedName();
777            name = internalName(name);
778            if (checkReadOnlyIgnoredWrite(property, state)) {
779                continue;
780            }
781            String xp = xpath == null ? name : xpath + '/' + name;
782            writeContext.recordChange(xp);
783            changed = true;
784
785            Type type = property.getType();
786            if (type.isSimpleType()) {
787                // simple property
788                Serializable value = property.getValueForWrite();
789                state.setSingle(name, value);
790            } else if (type.isComplexType()) {
791                // complex property
792                T childState = getChildForWrite(state, name, type);
793                writeComplexProperty(childState, (ComplexProperty) property, xp, writeContext);
794            } else {
795                ListType listType = (ListType) type;
796                if (listType.getFieldType().isSimpleType()) {
797                    // array
798                    Serializable value = property.getValueForWrite();
799                    if (value instanceof List) {
800                        List<?> list = (List<?>) value;
801                        Object[] array;
802                        if (list.isEmpty()) {
803                            array = new Object[0];
804                        } else {
805                            // use properly-typed array, useful for mem backend that doesn't re-convert all types
806                            Class<?> klass = list.get(0).getClass();
807                            array = (Object[]) Array.newInstance(klass, list.size());
808                        }
809                        value = list.toArray(array);
810                    } else if (value instanceof Object[]) {
811                        Object[] ar = (Object[]) value;
812                        if (ar.length != 0) {
813                            // use properly-typed array, useful for mem backend that doesn't re-convert all types
814                            Class<?> klass = Object.class;
815                            for (Object o : ar) {
816                                if (o != null) {
817                                    klass = o.getClass();
818                                    break;
819                                }
820                            }
821                            Object[] array;
822                            if (ar.getClass().getComponentType() == klass) {
823                                array = ar;
824                            } else {
825                                // copy to array with proper component type
826                                array = (Object[]) Array.newInstance(klass, ar.length);
827                                System.arraycopy(ar, 0, array, 0, ar.length);
828                            }
829                            value = array;
830                        }
831                    } else if (value == null) {
832                        // ok
833                    } else {
834                        throw new IllegalStateException(value.toString());
835                    }
836                    state.setArray(name, (Object[]) value);
837                } else {
838                    // complex list
839                    // update it
840                    List<T> childStates = updateList(state, name, property);
841                    // write values
842                    int i = 0;
843                    for (Property childProperty : property.getChildren()) {
844                        T childState = childStates.get(i);
845                        String xpi = xp + '/' + i;
846                        boolean c = writeComplexProperty(childState, (ComplexProperty) childProperty, xpi,
847                                writeContext);
848                        if (c) {
849                            writeContext.recordChange(xpi);
850                        }
851                        i++;
852                    }
853                }
854            }
855        }
856        return changed;
857    }
858
859    /**
860     * Reads prefetched values.
861     */
862    protected Map<String, Serializable> readPrefetch(T state, ComplexType complexType, Set<String> xpaths)
863            throws PropertyException {
864        // augment xpaths with all prefixes, to cut short recursive search
865        Set<String> prefixes = new HashSet<>();
866        for (String xpath : xpaths) {
867            for (;;) {
868                // add as prefix
869                if (!prefixes.add(xpath)) {
870                    // already present, we can stop
871                    break;
872                }
873                // loop with its prefix
874                int i = xpath.lastIndexOf('/');
875                if (i == -1) {
876                    break;
877                }
878                xpath = xpath.substring(0, i);
879            }
880        }
881        Map<String, Serializable> prefetch = new HashMap<String, Serializable>();
882        readPrefetch(state, complexType, null, null, prefixes, prefetch);
883        return prefetch;
884    }
885
886    protected void readPrefetch(T state, ComplexType complexType, String xpathGeneric, String xpath,
887            Set<String> prefixes, Map<String, Serializable> prefetch) throws PropertyException {
888        if (TypeConstants.isContentType(complexType)) {
889            if (!prefixes.contains(xpathGeneric)) {
890                return;
891            }
892            Blob blob = getValueBlob(state);
893            prefetch.put(xpath, (Serializable) blob);
894            return;
895        }
896        for (Field field : complexType.getFields()) {
897            readPrefetchField(state, field, xpathGeneric, xpath, prefixes, prefetch);
898        }
899    }
900
901    protected void readPrefetchField(T state, Field field, String xpathGeneric, String xpath, Set<String> prefixes,
902            Map<String, Serializable> prefetch) {
903        String name = field.getName().getPrefixedName();
904        Type type = field.getType();
905        xpathGeneric = xpathGeneric == null ? name : xpathGeneric + '/' + name;
906        xpath = xpath == null ? name : xpath + '/' + name;
907        if (!prefixes.contains(xpathGeneric)) {
908            return;
909        }
910        if (type.isSimpleType()) {
911            // scalar
912            Object value = state.getSingle(name);
913            prefetch.put(xpath, (Serializable) value);
914        } else if (type.isComplexType()) {
915            // complex property
916            T childState = getChild(state, name, type);
917            if (childState != null) {
918                readPrefetch(childState, (ComplexType) type, xpathGeneric, xpath, prefixes, prefetch);
919            }
920        } else {
921            // array or list
922            ListType listType = (ListType) type;
923            if (listType.getFieldType().isSimpleType()) {
924                // array
925                Object[] value = state.getArray(name);
926                prefetch.put(xpath, value);
927            } else {
928                // complex list
929                List<T> childStates = getChildAsList(state, name);
930                Field listField = listType.getField();
931                xpathGeneric += "/*";
932                int i = 0;
933                for (T childState : childStates) {
934                    readPrefetch(childState, (ComplexType) listField.getType(), xpathGeneric, xpath + '/' + i++,
935                            prefixes, prefetch);
936                }
937            }
938        }
939    }
940
941    /**
942     * Visits all the blobs of this document and calls the passed blob visitor on each one.
943     */
944    protected void visitBlobs(T state, Consumer<BlobAccessor> blobVisitor, Runnable markDirty)
945            throws PropertyException {
946        Visit visit = new Visit(blobVisitor, markDirty);
947        // structural type
948        visit.visitBlobsComplex(state, getType());
949        // dynamic facets
950        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
951        for (String facet : getFacets()) {
952            CompositeType facetType = schemaManager.getFacet(facet);
953            if (facetType != null) { // if not obsolete facet
954                visit.visitBlobsComplex(state, facetType);
955            }
956        }
957        // proxy schemas
958        if (getProxySchemas() != null) {
959            for (Schema schema : getProxySchemas()) {
960                visit.visitBlobsComplex(state, schema);
961            }
962        }
963    }
964
965    protected class StateBlobAccessor implements BlobAccessor {
966
967        protected final Collection<String> path;
968
969        protected final T state;
970
971        protected final Runnable markDirty;
972
973        public StateBlobAccessor(Collection<String> path, T state, Runnable markDirty) {
974            this.path = path;
975            this.state = state;
976            this.markDirty = markDirty;
977        }
978
979        @Override
980        public String getXPath() {
981            return StringUtils.join(path, "/");
982        }
983
984        @Override
985        public Blob getBlob() throws PropertyException {
986            return getValueBlob(state);
987        }
988
989        @Override
990        public void setBlob(Blob blob) throws PropertyException {
991            markDirty.run();
992            setValueBlob(state, blob);
993        }
994    }
995
996    protected class Visit {
997
998        protected final Consumer<BlobAccessor> blobVisitor;
999
1000        protected final Runnable markDirty;
1001
1002        protected final Deque<String> path;
1003
1004        public Visit(Consumer<BlobAccessor> blobVisitor, Runnable markDirty) {
1005            this.blobVisitor = blobVisitor;
1006            this.markDirty = markDirty;
1007            path = new ArrayDeque<>();
1008        }
1009
1010        public void visitBlobsComplex(T state, ComplexType complexType) throws PropertyException {
1011            if (TypeConstants.isContentType(complexType)) {
1012                blobVisitor.accept(new StateBlobAccessor(path, state, markDirty));
1013                return;
1014            }
1015            for (Field field : complexType.getFields()) {
1016                visitBlobsField(state, field);
1017            }
1018        }
1019
1020        protected void visitBlobsField(T state, Field field) throws PropertyException {
1021            Type type = field.getType();
1022            if (type.isSimpleType()) {
1023                // scalar
1024            } else if (type.isComplexType()) {
1025                // complex property
1026                String name = field.getName().getPrefixedName();
1027                T childState = getChild(state, name, type);
1028                if (childState != null) {
1029                    path.addLast(name);
1030                    visitBlobsComplex(childState, (ComplexType) type);
1031                    path.removeLast();
1032                }
1033            } else {
1034                // array or list
1035                Type fieldType = ((ListType) type).getFieldType();
1036                if (fieldType.isSimpleType()) {
1037                    // array
1038                } else {
1039                    // complex list
1040                    String name = field.getName().getPrefixedName();
1041                    path.addLast(name);
1042                    int i = 0;
1043                    for (T childState : getChildAsList(state, name)) {
1044                        path.addLast(String.valueOf(i++));
1045                        visitBlobsComplex(childState, (ComplexType) fieldType);
1046                        path.removeLast();
1047                    }
1048                    path.removeLast();
1049                }
1050            }
1051        }
1052    }
1053
1054    @Override
1055    public Lock getLock() {
1056        try {
1057            return getSession().getLockManager().getLock(getUUID());
1058        } catch (DocumentNotFoundException e) {
1059            return getDocumentLock();
1060        }
1061    }
1062
1063    @Override
1064    public Lock setLock(Lock lock) {
1065        if (lock == null) {
1066            throw new NullPointerException("Attempt to use null lock on: " + getUUID());
1067        }
1068        try {
1069            return getSession().getLockManager().setLock(getUUID(), lock);
1070        } catch (DocumentNotFoundException e) {
1071            return setDocumentLock(lock);
1072        }
1073    }
1074
1075    @Override
1076    public Lock removeLock(String owner) {
1077        try {
1078            return getSession().getLockManager().removeLock(getUUID(), owner);
1079        } catch (DocumentNotFoundException e) {
1080            return removeDocumentLock(owner);
1081        }
1082    }
1083
1084    /**
1085     * Gets the lock from this recently created and unsaved document.
1086     *
1087     * @return the lock, or {@code null} if no lock is set
1088     * @since 7.4
1089     */
1090    protected abstract Lock getDocumentLock();
1091
1092    /**
1093     * Sets a lock on this recently created and unsaved document.
1094     *
1095     * @param lock the lock to set
1096     * @return {@code null} if locking succeeded, or the existing lock if locking failed
1097     * @since 7.4
1098     */
1099    protected abstract Lock setDocumentLock(Lock lock);
1100
1101    /**
1102     * Removes a lock from this recently created and unsaved document.
1103     *
1104     * @param the owner to check, or {@code null} for no check
1105     * @return {@code null} if there was no lock or if removal succeeded, or a lock if it blocks removal due to owner
1106     *         mismatch
1107     * @since 7.4
1108     */
1109    protected abstract Lock removeDocumentLock(String owner);
1110
1111}