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