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