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