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