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