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