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