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