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