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