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