/* * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Florent Guillaume */ package org.nuxeo.ecm.core.storage; import java.io.IOException; import java.io.Serializable; import java.lang.reflect.Array; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.function.Consumer; import java.util.regex.Pattern; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.nuxeo.ecm.core.api.Blob; import org.nuxeo.ecm.core.api.DocumentNotFoundException; import org.nuxeo.ecm.core.api.Lock; import org.nuxeo.ecm.core.api.PropertyException; import org.nuxeo.ecm.core.api.model.Property; import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; import org.nuxeo.ecm.core.api.model.impl.ComplexProperty; import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty; import org.nuxeo.ecm.core.blob.BlobInfo; import org.nuxeo.ecm.core.blob.DocumentBlobManager; import org.nuxeo.ecm.core.model.Document; import org.nuxeo.ecm.core.schema.SchemaManager; import org.nuxeo.ecm.core.schema.TypeConstants; import org.nuxeo.ecm.core.schema.types.ComplexType; import org.nuxeo.ecm.core.schema.types.CompositeType; import org.nuxeo.ecm.core.schema.types.Field; import org.nuxeo.ecm.core.schema.types.ListType; import org.nuxeo.ecm.core.schema.types.Schema; import org.nuxeo.ecm.core.schema.types.SimpleTypeImpl; import org.nuxeo.ecm.core.schema.types.Type; import org.nuxeo.ecm.core.schema.types.primitives.BinaryType; import org.nuxeo.ecm.core.schema.types.primitives.BooleanType; import org.nuxeo.ecm.core.schema.types.primitives.DateType; import org.nuxeo.ecm.core.schema.types.primitives.DoubleType; import org.nuxeo.ecm.core.schema.types.primitives.IntegerType; import org.nuxeo.ecm.core.schema.types.primitives.LongType; import org.nuxeo.ecm.core.schema.types.primitives.StringType; import org.nuxeo.runtime.api.Framework; /** * Base implementation for a Document. * <p> * Knows how to read and write values. It is generic in terms of a base State class from which one can read and write * values. * * @since 7.3 */ public abstract class BaseDocument<T extends StateAccessor> implements Document { public static final String[] EMPTY_STRING_ARRAY = new String[0]; public static final String BLOB_NAME = "name"; public static final String BLOB_MIME_TYPE = "mime-type"; public static final String BLOB_ENCODING = "encoding"; public static final String BLOB_DIGEST = "digest"; public static final String BLOB_LENGTH = "length"; public static final String BLOB_DATA = "data"; public static final String DC_PREFIX = "dc:"; public static final String DC_ISSUED = "dc:issued"; // used instead of ecm:changeToken when change tokens are disabled public static final String DC_MODIFIED = "dc:modified"; public static final String RELATED_TEXT_RESOURCES = "relatedtextresources"; public static final String RELATED_TEXT_ID = "relatedtextid"; public static final String RELATED_TEXT = "relatedtext"; public static final String FULLTEXT_JOBID_PROP = "ecm:fulltextJobId"; public static final String FULLTEXT_SIMPLETEXT_PROP = "ecm:simpleText"; public static final String FULLTEXT_BINARYTEXT_PROP = "ecm:binaryText"; public static final String MISC_LIFECYCLE_STATE_PROP = "ecm:lifeCycleState"; public static final String LOCK_OWNER_PROP = "ecm:lockOwner"; public static final String LOCK_CREATED_PROP = "ecm:lockCreated"; public static final Set<String> VERSION_WRITABLE_PROPS = new HashSet<String>(Arrays.asList( // FULLTEXT_JOBID_PROP, // FULLTEXT_BINARYTEXT_PROP, // MISC_LIFECYCLE_STATE_PROP, // LOCK_OWNER_PROP, // LOCK_CREATED_PROP, // DC_ISSUED, // RELATED_TEXT_RESOURCES, // RELATED_TEXT_ID, // RELATED_TEXT // )); protected final static Pattern NON_CANONICAL_INDEX = Pattern.compile("[^/\\[\\]]+" // name + "\\[(\\d+)\\]" // index in brackets ); protected static final Runnable NO_DIRTY = () -> { }; /** * Gets the list of proxy schemas, if this is a proxy. * * @return the proxy schemas, or {@code null} */ protected abstract List<Schema> getProxySchemas(); /** * Gets a child state. * * @param state the parent state * @param name the child name * @param type the child's type * @return the child state, or {@code null} if it doesn't exist */ protected abstract T getChild(T state, String name, Type type) throws PropertyException; /** * Gets a child state into which we will want to write data. * <p> * Creates it if needed. * * @param state the parent state * @param name the child name * @param type the child's type * @return the child state, never {@code null} * @since 7.4 */ protected abstract T getChildForWrite(T state, String name, Type type) throws PropertyException; /** * Gets a child state which is a list. * * @param state the parent state * @param name the child name * @return the child state, never {@code null} */ protected abstract List<T> getChildAsList(T state, String name) throws PropertyException; /** * Update a list. * * @param state the parent state * @param name the child name * @param field the list element type * @param xpath the xpath of this list * @param values the values */ protected abstract void updateList(T state, String name, Field field, String xpath, List<Object> values) throws PropertyException; /** * Update a list. * * @param state the parent state * @param name the child name * @param property the property * @return the list of states to write */ protected abstract List<T> updateList(T state, String name, Property property) throws PropertyException; /** * Finds the internal name to use to refer to this property. */ protected abstract String internalName(String name); /** * Canonicalizes a Nuxeo xpath. * <p> * Replaces {@code a/foo[123]/b} with {@code a/123/b} * * @param xpath the xpath * @return the canonicalized xpath. */ protected static String canonicalXPath(String xpath) { if (xpath.indexOf('[') > 0) { xpath = NON_CANONICAL_INDEX.matcher(xpath).replaceAll("$1"); } return xpath; } /** Copies the array with an appropriate class depending on the type. */ protected static Object[] typedArray(Type type, Object[] array) { if (array == null) { array = EMPTY_STRING_ARRAY; } Class<?> klass; if (type instanceof StringType) { klass = String.class; } else if (type instanceof BooleanType) { klass = Boolean.class; } else if (type instanceof LongType) { klass = Long.class; } else if (type instanceof DoubleType) { klass = Double.class; } else if (type instanceof DateType) { klass = Calendar.class; } else if (type instanceof BinaryType) { klass = String.class; } else if (type instanceof IntegerType) { throw new RuntimeException("Unimplemented primitive type: " + type.getClass().getName()); } else if (type instanceof SimpleTypeImpl) { // simple type with constraints -- ignore constraints XXX return typedArray(type.getSuperType(), array); } else { throw new RuntimeException("Invalid primitive type: " + type.getClass().getName()); } int len = array.length; Object[] copy = (Object[]) Array.newInstance(klass, len); System.arraycopy(array, 0, copy, 0, len); return copy; } protected static boolean isVersionWritableProperty(String name) { return VERSION_WRITABLE_PROPS.contains(name) // || name.startsWith(FULLTEXT_BINARYTEXT_PROP) // || name.startsWith(FULLTEXT_SIMPLETEXT_PROP); } protected static void clearDirtyFlags(Property property) { if (property.isContainer()) { for (Property p : property) { clearDirtyFlags(p); } } property.clearDirtyFlags(); } /** * Checks for ignored writes. May throw. */ protected boolean checkReadOnlyIgnoredWrite(Property property, T state) throws PropertyException { String name = property.getField().getName().getPrefixedName(); if (!isReadOnly() || isVersionWritableProperty(name)) { // do write return false; } if (!isVersion()) { throw new PropertyException("Cannot write readonly property: " + name); } if (!name.startsWith(DC_PREFIX) && !(property.getField().getDeclaringType() instanceof Schema && ((Schema) property.getField().getDeclaringType()).isVersionWritabe())) { throw new PropertyException("Cannot set property on a version: " + name); } // ignore if value is unchanged (only for dublincore) // dublincore contains only scalars and arrays Object value = property.getValueForWrite(); Object oldValue; if (property.getType().isSimpleType()) { oldValue = state.getSingle(name); } else { oldValue = state.getArray(name); } if (!ArrayUtils.isEquals(value, oldValue)) { // do write return false; } // ignore attempt to write identical value return true; } protected BlobInfo getBlobInfo(T state) throws PropertyException { BlobInfo blobInfo = new BlobInfo(); blobInfo.key = (String) state.getSingle(BLOB_DATA); blobInfo.filename = (String) state.getSingle(BLOB_NAME); blobInfo.mimeType = (String) state.getSingle(BLOB_MIME_TYPE); blobInfo.encoding = (String) state.getSingle(BLOB_ENCODING); blobInfo.digest = (String) state.getSingle(BLOB_DIGEST); blobInfo.length = (Long) state.getSingle(BLOB_LENGTH); return blobInfo; } protected void setBlobInfo(T state, BlobInfo blobInfo) throws PropertyException { state.setSingle(BLOB_DATA, blobInfo.key); state.setSingle(BLOB_NAME, blobInfo.filename); state.setSingle(BLOB_MIME_TYPE, blobInfo.mimeType); state.setSingle(BLOB_ENCODING, blobInfo.encoding); state.setSingle(BLOB_DIGEST, blobInfo.digest); state.setSingle(BLOB_LENGTH, blobInfo.length); } /** * Gets a value (may be complex/list) from the document at the given xpath. */ protected Object getValueObject(T state, String xpath) throws PropertyException { xpath = canonicalXPath(xpath); String[] segments = xpath.split("/"); /* * During this loop state may become null if we read an uninitialized complex property (DBS), in that case the * code must treat it as reading uninitialized values for its children. */ ComplexType parentType = getType(); for (int i = 0; i < segments.length; i++) { String segment = segments[i]; Field field = parentType.getField(segment); if (field == null && i == 0) { // check facets SchemaManager schemaManager = Framework.getService(SchemaManager.class); for (String facet : getFacets()) { CompositeType facetType = schemaManager.getFacet(facet); field = facetType.getField(segment); if (field != null) { break; } } } if (field == null && i == 0 && getProxySchemas() != null) { // check proxy schemas for (Schema schema : getProxySchemas()) { field = schema.getField(segment); if (field != null) { break; } } } if (field == null) { throw new PropertyNotFoundException(xpath, i == 0 ? null : "Unknown segment: " + segment); } String name = field.getName().getPrefixedName(); // normalize from segment Type type = field.getType(); // check if we have a complex list index in the next position if (i < segments.length - 1 && StringUtils.isNumeric(segments[i + 1])) { int index = Integer.parseInt(segments[i + 1]); i++; if (!type.isListType() || ((ListType) type).getFieldType().isSimpleType()) { throw new PropertyNotFoundException(xpath, "Cannot use index after segment: " + segment); } List<T> list = state == null ? Collections.emptyList() : getChildAsList(state, name); if (index >= list.size()) { throw new PropertyNotFoundException(xpath, "Index out of bounds: " + index); } // find complex list state state = list.get(index); parentType = (ComplexType) ((ListType) type).getFieldType(); if (i == segments.length - 1) { // last segment return getValueComplex(state, parentType); } else { // not last segment continue; } } if (i == segments.length - 1) { // last segment return state == null ? null : getValueField(state, field); } else { // not last segment if (type.isSimpleType()) { // scalar throw new PropertyNotFoundException(xpath, "Segment must be last: " + segment); } else if (type.isComplexType()) { // complex property state = state == null ? null : getChild(state, name, type); // here state can be null (DBS), continue loop with it, meaning uninitialized for read parentType = (ComplexType) type; } else { // list ListType listType = (ListType) type; if (listType.isArray()) { // array of scalars throw new PropertyNotFoundException(xpath, "Segment must be last: " + segment); } else { // complex list but next segment was not numeric throw new PropertyNotFoundException(xpath, "Missing list index after segment: " + segment); } } } } throw new AssertionError("not reached"); } protected Object getValueField(T state, Field field) throws PropertyException { Type type = field.getType(); String name = field.getName().getPrefixedName(); name = internalName(name); if (type.isSimpleType()) { // scalar return state.getSingle(name); } else if (type.isComplexType()) { // complex property T childState = getChild(state, name, type); if (childState == null) { return null; } return getValueComplex(childState, (ComplexType) type); } else { // array or list Type fieldType = ((ListType) type).getFieldType(); if (fieldType.isSimpleType()) { // array return state.getArray(name); } else { // complex list List<T> childStates = getChildAsList(state, name); List<Object> list = new ArrayList<>(childStates.size()); for (T childState : childStates) { Object value = getValueComplex(childState, (ComplexType) fieldType); list.add(value); } return list; } } } protected Object getValueComplex(T state, ComplexType complexType) throws PropertyException { if (TypeConstants.isContentType(complexType)) { return getValueBlob(state); } Map<String, Object> map = new HashMap<>(); for (Field field : complexType.getFields()) { String name = field.getName().getPrefixedName(); Object value = getValueField(state, field); map.put(name, value); } return map; } protected Blob getValueBlob(T state) throws PropertyException { BlobInfo blobInfo = getBlobInfo(state); DocumentBlobManager blobManager = Framework.getService(DocumentBlobManager.class); try { return blobManager.readBlob(blobInfo, getRepositoryName()); } catch (IOException e) { throw new PropertyException("Cannot get blob info for: " + blobInfo.key, e); } } /** * Sets a value (may be complex/list) into the document at the given xpath. */ protected void setValueObject(T state, String xpath, Object value) throws PropertyException { xpath = canonicalXPath(xpath); String[] segments = xpath.split("/"); ComplexType parentType = getType(); for (int i = 0; i < segments.length; i++) { String segment = segments[i]; Field field = parentType.getField(segment); if (field == null && i == 0) { // check facets SchemaManager schemaManager = Framework.getService(SchemaManager.class); for (String facet : getFacets()) { CompositeType facetType = schemaManager.getFacet(facet); field = facetType.getField(segment); if (field != null) { break; } } } if (field == null && i == 0 && getProxySchemas() != null) { // check proxy schemas for (Schema schema : getProxySchemas()) { field = schema.getField(segment); if (field != null) { break; } } } if (field == null) { throw new PropertyNotFoundException(xpath, i == 0 ? null : "Unknown segment: " + segment); } String name = field.getName().getPrefixedName(); // normalize from segment Type type = field.getType(); // check if we have a complex list index in the next position if (i < segments.length - 1 && StringUtils.isNumeric(segments[i + 1])) { int index = Integer.parseInt(segments[i + 1]); i++; if (!type.isListType() || ((ListType) type).getFieldType().isSimpleType()) { throw new PropertyNotFoundException(xpath, "Cannot use index after segment: " + segment); } List<T> list = getChildAsList(state, name); if (index >= list.size()) { throw new PropertyNotFoundException(xpath, "Index out of bounds: " + index); } // find complex list state state = list.get(index); field = ((ListType) type).getField(); if (i == segments.length - 1) { // last segment setValueComplex(state, field, xpath, value); } else { // not last segment parentType = (ComplexType) field.getType(); } continue; } if (i == segments.length - 1) { // last segment setValueField(state, field, xpath, value); } else { // not last segment if (type.isSimpleType()) { // scalar throw new PropertyNotFoundException(xpath, "Segment must be last: " + segment); } else if (type.isComplexType()) { // complex property state = getChildForWrite(state, name, type); parentType = (ComplexType) type; } else { // list ListType listType = (ListType) type; if (listType.isArray()) { // array of scalars throw new PropertyNotFoundException(xpath, "Segment must be last: " + segment); } else { // complex list but next segment was not numeric throw new PropertyNotFoundException(xpath, "Missing list index after segment: " + segment); } } } } } protected void setValueField(T state, Field field, String xpath, Object value) throws PropertyException { Type type = field.getType(); String name = field.getName().getPrefixedName(); // normalize from map key name = internalName(name); // TODO we could check for read-only here if (type.isSimpleType()) { // scalar state.setSingle(name, value); } else if (type.isComplexType()) { // complex property T childState = getChildForWrite(state, name, type); setValueComplex(childState, field, xpath, value); } else { // array or list ListType listType = (ListType) type; Type fieldType = listType.getFieldType(); if (fieldType.isSimpleType()) { // array if (value instanceof List) { value = ((List<?>) value).toArray(new Object[0]); } state.setArray(name, (Object[]) value); } else { // complex list if (value != null && !(value instanceof List)) { throw new PropertyException( "Expected List value for: " + name + ", got " + value.getClass().getName() + " instead"); } @SuppressWarnings("unchecked") List<Object> values = value == null ? Collections.emptyList() : (List<Object>) value; updateList(state, name, listType.getField(), xpath, values); } } } protected void setValueComplex(T state, Field field, String xpath, Object value) throws PropertyException { ComplexType complexType = (ComplexType) field.getType(); if (TypeConstants.isContentType(complexType)) { if (value != null && !(value instanceof Blob)) { throw new PropertyException( "Expected Blob value for: " + xpath + ", got " + value.getClass().getName() + " instead"); } setValueBlob(state, (Blob) value, xpath); return; } if (value != null && !(value instanceof Map)) { throw new PropertyException( "Expected Map value for: " + xpath + ", got " + value.getClass().getName() + " instead"); } @SuppressWarnings("unchecked") Map<String, Object> map = value == null ? Collections.emptyMap() : (Map<String, Object>) value; Set<String> keys = new HashSet<>(map.keySet()); for (Field f : complexType.getFields()) { String name = f.getName().getPrefixedName(); keys.remove(name); value = map.get(name); setValueField(state, f, xpath + '/' + name, value); } if (!keys.isEmpty()) { throw new PropertyException("Unknown key: " + keys.iterator().next() + " for " + xpath); } } protected void setValueBlob(T state, Blob blob, String xpath) throws PropertyException { BlobInfo blobInfo = new BlobInfo(); if (blob != null) { DocumentBlobManager blobManager = Framework.getService(DocumentBlobManager.class); try { blobInfo.key = blobManager.writeBlob(blob, this, xpath); } catch (IOException e) { throw new PropertyException("Cannot get blob info for: " + blob, e); } blobInfo.filename = blob.getFilename(); blobInfo.mimeType = blob.getMimeType(); blobInfo.encoding = blob.getEncoding(); blobInfo.digest = blob.getDigest(); blobInfo.length = blob.getLength() == -1 ? null : Long.valueOf(blob.getLength()); } setBlobInfo(state, blobInfo); } /** * Reads state into a complex property. */ protected void readComplexProperty(T state, ComplexProperty complexProperty) throws PropertyException { if (state == null) { complexProperty.init(null); return; } if (complexProperty instanceof BlobProperty) { Blob blob = getValueBlob(state); complexProperty.init((Serializable) blob); return; } for (Property property : complexProperty) { String name = property.getField().getName().getPrefixedName(); name = internalName(name); Type type = property.getType(); if (type.isSimpleType()) { // simple property Object value = state.getSingle(name); property.init((Serializable) value); } else if (type.isComplexType()) { // complex property T childState = getChild(state, name, type); readComplexProperty(childState, (ComplexProperty) property); ((ComplexProperty) property).removePhantomFlag(); } else { ListType listType = (ListType) type; if (listType.getFieldType().isSimpleType()) { // array Object[] array = state.getArray(name); array = typedArray(listType.getFieldType(), array); property.init(array); } else { // complex list Field listField = listType.getField(); List<T> childStates = getChildAsList(state, name); // TODO property.init(null) if null children in DBS List<Object> list = new ArrayList<>(childStates.size()); for (T childState : childStates) { ComplexProperty p = (ComplexProperty) complexProperty.getRoot().createProperty(property, listField, 0); readComplexProperty(childState, p); list.add(p.getValue()); } property.init((Serializable) list); } } } } protected static class BlobWriteInfo<T extends StateAccessor> { public final T state; public final Blob blob; public final String xpath; public BlobWriteInfo(T state, Blob blob, String xpath) { this.state = state; this.blob = blob; this.xpath = xpath; } } protected static class BlobWriteContext<T extends StateAccessor> implements WriteContext { public final Map<BaseDocument<T>, List<BlobWriteInfo<T>>> blobWriteInfos = new HashMap<>(); public final Set<String> xpaths = new HashSet<>(); /** * Records a change to a given xpath. */ public void recordChange(String xpath) { xpaths.add(xpath); } /** * Records a blob update. */ public void recordBlob(BaseDocument<T> doc, T state, Blob blob, String xpath) { BlobWriteInfo<T> info = new BlobWriteInfo<T>(state, blob, xpath); blobWriteInfos.computeIfAbsent(doc, k -> new ArrayList<>()).add(info); } @Override public Set<String> getChanges() { return xpaths; } // note, in the proxy case baseDoc may be different from the doc in the map @Override public void flush(Document baseDoc) { // first, write all updated blobs for (Entry<BaseDocument<T>, List<BlobWriteInfo<T>>> es : blobWriteInfos.entrySet()) { BaseDocument<T> doc = es.getKey(); for (BlobWriteInfo<T> info : es.getValue()) { doc.setValueBlob(info.state, info.blob, info.xpath); } } // then inform the blob manager about the changed xpaths DocumentBlobManager blobManager = Framework.getService(DocumentBlobManager.class); blobManager.notifyChanges(baseDoc, xpaths); } } @Override public WriteContext getWriteContext() { return new BlobWriteContext<T>(); } /** * Writes state from a complex property. * * @return {@code true} if something changed */ protected boolean writeComplexProperty(T state, ComplexProperty complexProperty, WriteContext writeContext) throws PropertyException { return writeComplexProperty(state, complexProperty, null, false, writeContext); } /** * Writes state from a complex property. * <p> * Writes only properties that are dirty, unless skipDirtyCheck is true in which case everything is written. * * @return {@code true} if something changed */ protected boolean writeComplexProperty(T state, ComplexProperty complexProperty, String xpath, boolean skipDirtyCheck, WriteContext wc) throws PropertyException { @SuppressWarnings("unchecked") BlobWriteContext<T> writeContext = (BlobWriteContext<T>) wc; if (complexProperty instanceof BlobProperty) { Serializable value = ((BlobProperty) complexProperty).getValueForWrite(); if (value != null && !(value instanceof Blob)) { throw new PropertyException("Cannot write a non-Blob value: " + value); } writeContext.recordBlob(this, state, (Blob) value, xpath); return true; } boolean changed = false; for (Property property : complexProperty) { // write dirty properties, but also phantoms with non-null default values // this is critical for DeltaLong updates to work, they need a non-null initial value if (skipDirtyCheck || property.isDirty() || (property.isPhantom() && property.getField().getDefaultValue() != null)) { // do the write } else { continue; } String name = property.getField().getName().getPrefixedName(); name = internalName(name); if (checkReadOnlyIgnoredWrite(property, state)) { continue; } String xp = xpath == null ? name : xpath + '/' + name; writeContext.recordChange(xp); changed = true; Type type = property.getType(); if (type.isSimpleType()) { // simple property Serializable value = property.getValueForWrite(); state.setSingle(name, value); } else if (type.isComplexType()) { // complex property T childState = getChildForWrite(state, name, type); writeComplexProperty(childState, (ComplexProperty) property, xp, skipDirtyCheck, writeContext); } else { ListType listType = (ListType) type; if (listType.getFieldType().isSimpleType()) { // array Serializable value = property.getValueForWrite(); if (value instanceof List) { List<?> list = (List<?>) value; Object[] array; if (list.isEmpty()) { array = new Object[0]; } else { // use properly-typed array, useful for mem backend that doesn't re-convert all types Class<?> klass = list.get(0).getClass(); array = (Object[]) Array.newInstance(klass, list.size()); } value = list.toArray(array); } else if (value instanceof Object[]) { Object[] ar = (Object[]) value; if (ar.length != 0) { // use properly-typed array, useful for mem backend that doesn't re-convert all types Class<?> klass = Object.class; for (Object o : ar) { if (o != null) { klass = o.getClass(); break; } } Object[] array; if (ar.getClass().getComponentType() == klass) { array = ar; } else { // copy to array with proper component type array = (Object[]) Array.newInstance(klass, ar.length); System.arraycopy(ar, 0, array, 0, ar.length); } value = array; } } else if (value == null) { // ok } else { throw new IllegalStateException(value.toString()); } state.setArray(name, (Object[]) value); } else { // complex list // update it List<T> childStates = updateList(state, name, property); // write values int i = 0; for (Property childProperty : property.getChildren()) { T childState = childStates.get(i); String xpi = xp + '/' + i; boolean moved = childProperty.isMoved(); boolean c = writeComplexProperty(childState, (ComplexProperty) childProperty, xpi, skipDirtyCheck || moved , writeContext); if (c) { writeContext.recordChange(xpi); } i++; } } } } return changed; } /** * Reads prefetched values. */ protected Map<String, Serializable> readPrefetch(T state, ComplexType complexType, Set<String> xpaths) throws PropertyException { // augment xpaths with all prefixes, to cut short recursive search Set<String> prefixes = new HashSet<>(); for (String xpath : xpaths) { for (;;) { // add as prefix if (!prefixes.add(xpath)) { // already present, we can stop break; } // loop with its prefix int i = xpath.lastIndexOf('/'); if (i == -1) { break; } xpath = xpath.substring(0, i); } } Map<String, Serializable> prefetch = new HashMap<String, Serializable>(); readPrefetch(state, complexType, null, null, prefixes, prefetch); return prefetch; } protected void readPrefetch(T state, ComplexType complexType, String xpathGeneric, String xpath, Set<String> prefixes, Map<String, Serializable> prefetch) throws PropertyException { if (TypeConstants.isContentType(complexType)) { if (!prefixes.contains(xpathGeneric)) { return; } Blob blob = getValueBlob(state); prefetch.put(xpath, (Serializable) blob); return; } for (Field field : complexType.getFields()) { readPrefetchField(state, field, xpathGeneric, xpath, prefixes, prefetch); } } protected void readPrefetchField(T state, Field field, String xpathGeneric, String xpath, Set<String> prefixes, Map<String, Serializable> prefetch) { String name = field.getName().getPrefixedName(); Type type = field.getType(); xpathGeneric = xpathGeneric == null ? name : xpathGeneric + '/' + name; xpath = xpath == null ? name : xpath + '/' + name; if (!prefixes.contains(xpathGeneric)) { return; } if (type.isSimpleType()) { // scalar Object value = state.getSingle(name); prefetch.put(xpath, (Serializable) value); } else if (type.isComplexType()) { // complex property T childState = getChild(state, name, type); if (childState != null) { readPrefetch(childState, (ComplexType) type, xpathGeneric, xpath, prefixes, prefetch); } } else { // array or list ListType listType = (ListType) type; if (listType.getFieldType().isSimpleType()) { // array Object[] value = state.getArray(name); prefetch.put(xpath, value); } else { // complex list List<T> childStates = getChildAsList(state, name); Field listField = listType.getField(); xpathGeneric += "/*"; int i = 0; for (T childState : childStates) { readPrefetch(childState, (ComplexType) listField.getType(), xpathGeneric, xpath + '/' + i++, prefixes, prefetch); } } } } /** * Visits all the blobs of this document and calls the passed blob visitor on each one. */ protected void visitBlobs(T state, Consumer<BlobAccessor> blobVisitor, Runnable markDirty) throws PropertyException { Visit visit = new Visit(blobVisitor, markDirty); // structural type visit.visitBlobsComplex(state, getType()); // dynamic facets SchemaManager schemaManager = Framework.getService(SchemaManager.class); for (String facet : getFacets()) { CompositeType facetType = schemaManager.getFacet(facet); if (facetType != null) { // if not obsolete facet visit.visitBlobsComplex(state, facetType); } } // proxy schemas if (getProxySchemas() != null) { for (Schema schema : getProxySchemas()) { visit.visitBlobsComplex(state, schema); } } } protected class StateBlobAccessor implements BlobAccessor { protected final Collection<String> path; protected final T state; protected final Runnable markDirty; public StateBlobAccessor(Collection<String> path, T state, Runnable markDirty) { this.path = path; this.state = state; this.markDirty = markDirty; } @Override public String getXPath() { return StringUtils.join(path, "/"); } @Override public Blob getBlob() throws PropertyException { return getValueBlob(state); } @Override public void setBlob(Blob blob) throws PropertyException { markDirty.run(); setValueBlob(state, blob, getXPath()); } } protected class Visit { protected final Consumer<BlobAccessor> blobVisitor; protected final Runnable markDirty; protected final Deque<String> path; public Visit(Consumer<BlobAccessor> blobVisitor, Runnable markDirty) { this.blobVisitor = blobVisitor; this.markDirty = markDirty; path = new ArrayDeque<>(); } public void visitBlobsComplex(T state, ComplexType complexType) throws PropertyException { if (TypeConstants.isContentType(complexType)) { blobVisitor.accept(new StateBlobAccessor(path, state, markDirty)); return; } for (Field field : complexType.getFields()) { visitBlobsField(state, field); } } protected void visitBlobsField(T state, Field field) throws PropertyException { Type type = field.getType(); if (type.isSimpleType()) { // scalar } else if (type.isComplexType()) { // complex property String name = field.getName().getPrefixedName(); T childState = getChild(state, name, type); if (childState != null) { path.addLast(name); visitBlobsComplex(childState, (ComplexType) type); path.removeLast(); } } else { // array or list Type fieldType = ((ListType) type).getFieldType(); if (fieldType.isSimpleType()) { // array } else { // complex list String name = field.getName().getPrefixedName(); path.addLast(name); int i = 0; for (T childState : getChildAsList(state, name)) { path.addLast(String.valueOf(i++)); visitBlobsComplex(childState, (ComplexType) fieldType); path.removeLast(); } path.removeLast(); } } } } @Override public Lock getLock() { try { return getSession().getLockManager().getLock(getUUID()); } catch (DocumentNotFoundException e) { return getDocumentLock(); } } @Override public Lock setLock(Lock lock) { if (lock == null) { throw new NullPointerException("Attempt to use null lock on: " + getUUID()); } try { return getSession().getLockManager().setLock(getUUID(), lock); } catch (DocumentNotFoundException e) { return setDocumentLock(lock); } } @Override public Lock removeLock(String owner) { try { return getSession().getLockManager().removeLock(getUUID(), owner); } catch (DocumentNotFoundException e) { return removeDocumentLock(owner); } } /** * Gets the lock from this recently created and unsaved document. * * @return the lock, or {@code null} if no lock is set * @since 7.4 */ protected abstract Lock getDocumentLock(); /** * Sets a lock on this recently created and unsaved document. * * @param lock the lock to set * @return {@code null} if locking succeeded, or the existing lock if locking failed * @since 7.4 */ protected abstract Lock setDocumentLock(Lock lock); /** * Removes a lock from this recently created and unsaved document. * * @param the owner to check, or {@code null} for no check * @return {@code null} if there was no lock or if removal succeeded, or a lock if it blocks removal due to owner * mismatch * @since 7.4 */ protected abstract Lock removeDocumentLock(String owner); }