001/* 002 * (C) Copyright 2014 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.dbs; 020 021import static java.lang.Boolean.TRUE; 022 023import java.io.Serializable; 024import java.util.ArrayList; 025import java.util.Arrays; 026import java.util.Calendar; 027import java.util.Collection; 028import java.util.Collections; 029import java.util.HashMap; 030import java.util.HashSet; 031import java.util.LinkedList; 032import java.util.List; 033import java.util.Map; 034import java.util.Set; 035import java.util.function.Consumer; 036 037import org.apache.commons.lang.StringUtils; 038import org.nuxeo.ecm.core.NXCore; 039import org.nuxeo.ecm.core.api.DocumentNotFoundException; 040import org.nuxeo.ecm.core.api.LifeCycleException; 041import org.nuxeo.ecm.core.api.Lock; 042import org.nuxeo.ecm.core.api.NuxeoException; 043import org.nuxeo.ecm.core.api.PropertyException; 044import org.nuxeo.ecm.core.api.model.DocumentPart; 045import org.nuxeo.ecm.core.api.model.Property; 046import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; 047import org.nuxeo.ecm.core.api.model.ReadOnlyPropertyException; 048import org.nuxeo.ecm.core.api.model.impl.ComplexProperty; 049import org.nuxeo.ecm.core.blob.BlobManager; 050import org.nuxeo.ecm.core.lifecycle.LifeCycle; 051import org.nuxeo.ecm.core.lifecycle.LifeCycleService; 052import org.nuxeo.ecm.core.model.Document; 053import org.nuxeo.ecm.core.model.LockManager; 054import org.nuxeo.ecm.core.model.Session; 055import org.nuxeo.ecm.core.schema.DocumentType; 056import org.nuxeo.ecm.core.schema.SchemaManager; 057import org.nuxeo.ecm.core.schema.types.ComplexType; 058import org.nuxeo.ecm.core.schema.types.CompositeType; 059import org.nuxeo.ecm.core.schema.types.Field; 060import org.nuxeo.ecm.core.schema.types.Schema; 061import org.nuxeo.ecm.core.schema.types.Type; 062import org.nuxeo.ecm.core.storage.BaseDocument; 063import org.nuxeo.ecm.core.storage.State; 064import org.nuxeo.ecm.core.storage.sql.coremodel.SQLDocumentVersion.VersionNotModifiableException; 065import org.nuxeo.runtime.api.Framework; 066 067/** 068 * Implementation of a {@link Document} for Document-Based Storage. The document is stored as a JSON-like Map. The keys 069 * of the Map are the property names (including special names for system properties), and the values Map are 070 * Serializable values, either: 071 * <ul> 072 * <li>a scalar (String, Long, Double, Boolean, Calendar, Binary), 073 * <li>an array of scalars, 074 * <li>a List of Maps, recursively, 075 * <li>or another Map, recursively. 076 * </ul> 077 * An ACP value is stored as a list of maps. Each map has a keys for the ACL name and the actual ACL which is a list of 078 * ACEs. An ACE is a map having as keys username, permission, and grant. 079 * 080 * @since 5.9.4 081 */ 082public class DBSDocument extends BaseDocument<State> { 083 084 private static final Long ZERO = Long.valueOf(0); 085 086 public static final String SYSPROP_FULLTEXT_SIMPLE = "fulltextSimple"; 087 088 public static final String SYSPROP_FULLTEXT_BINARY = "fulltextBinary"; 089 090 public static final String SYSPROP_FULLTEXT_JOBID = "fulltextJobId"; 091 092 public static final String KEY_PREFIX = "ecm:"; 093 094 public static final String KEY_ID = "ecm:id"; 095 096 public static final String KEY_PARENT_ID = "ecm:parentId"; 097 098 public static final String KEY_ANCESTOR_IDS = "ecm:ancestorIds"; 099 100 public static final String KEY_PRIMARY_TYPE = "ecm:primaryType"; 101 102 public static final String KEY_MIXIN_TYPES = "ecm:mixinTypes"; 103 104 public static final String KEY_NAME = "ecm:name"; 105 106 public static final String KEY_POS = "ecm:pos"; 107 108 public static final String KEY_ACP = "ecm:acp"; 109 110 public static final String KEY_ACL_NAME = "name"; 111 112 public static final String KEY_PATH_INTERNAL = "ecm:__path"; 113 114 public static final String KEY_ACL = "acl"; 115 116 public static final String KEY_ACE_USER = "user"; 117 118 public static final String KEY_ACE_PERMISSION = "perm"; 119 120 public static final String KEY_ACE_GRANT = "grant"; 121 122 public static final String KEY_ACE_CREATOR = "creator"; 123 124 public static final String KEY_ACE_BEGIN = "begin"; 125 126 public static final String KEY_ACE_END = "end"; 127 128 public static final String KEY_ACE_STATUS = "status"; 129 130 public static final String KEY_READ_ACL = "ecm:racl"; 131 132 public static final String KEY_IS_CHECKED_IN = "ecm:isCheckedIn"; 133 134 public static final String KEY_IS_VERSION = "ecm:isVersion"; 135 136 public static final String KEY_IS_LATEST_VERSION = "ecm:isLatestVersion"; 137 138 public static final String KEY_IS_LATEST_MAJOR_VERSION = "ecm:isLatestMajorVersion"; 139 140 public static final String KEY_MAJOR_VERSION = "ecm:majorVersion"; 141 142 public static final String KEY_MINOR_VERSION = "ecm:minorVersion"; 143 144 public static final String KEY_VERSION_SERIES_ID = "ecm:versionSeriesId"; 145 146 public static final String KEY_VERSION_CREATED = "ecm:versionCreated"; 147 148 public static final String KEY_VERSION_LABEL = "ecm:versionLabel"; 149 150 public static final String KEY_VERSION_DESCRIPTION = "ecm:versionDescription"; 151 152 public static final String KEY_BASE_VERSION_ID = "ecm:baseVersionId"; 153 154 public static final String KEY_IS_PROXY = "ecm:isProxy"; 155 156 public static final String KEY_PROXY_TARGET_ID = "ecm:proxyTargetId"; 157 158 public static final String KEY_PROXY_VERSION_SERIES_ID = "ecm:proxyVersionSeriesId"; 159 160 public static final String KEY_PROXY_IDS = "ecm:proxyIds"; 161 162 public static final String KEY_LIFECYCLE_POLICY = "ecm:lifeCyclePolicy"; 163 164 public static final String KEY_LIFECYCLE_STATE = "ecm:lifeCycleState"; 165 166 public static final String KEY_LOCK_OWNER = "ecm:lockOwner"; 167 168 public static final String KEY_LOCK_CREATED = "ecm:lockCreated"; 169 170 public static final String KEY_CHANGE_TOKEN = "ecm:changeToken"; 171 172 // used instead of ecm:changeToken when change tokens are disabled 173 public static final String KEY_DC_MODIFIED = "dc:modified"; 174 175 public static final String KEY_BLOB_NAME = "name"; 176 177 public static final String KEY_BLOB_MIME_TYPE = "mime-type"; 178 179 public static final String KEY_BLOB_ENCODING = "encoding"; 180 181 public static final String KEY_BLOB_DIGEST = "digest"; 182 183 public static final String KEY_BLOB_LENGTH = "length"; 184 185 public static final String KEY_BLOB_DATA = "data"; 186 187 public static final String KEY_FULLTEXT_SIMPLE = "ecm:fulltextSimple"; 188 189 public static final String KEY_FULLTEXT_BINARY = "ecm:fulltextBinary"; 190 191 public static final String KEY_FULLTEXT_JOBID = "ecm:fulltextJobId"; 192 193 public static final String KEY_FULLTEXT_SCORE = "ecm:fulltextScore"; 194 195 public static final String APPLICATION_OCTET_STREAM = "application/octet-stream"; 196 197 public static final String INITIAL_CHANGE_TOKEN = "0"; 198 199 protected final String id; 200 201 protected final DBSDocumentState docState; 202 203 protected final DocumentType type; 204 205 protected final List<Schema> proxySchemas; 206 207 protected final DBSSession session; 208 209 protected boolean readonly; 210 211 protected static final Map<String, String> systemPropNameMap; 212 213 static { 214 systemPropNameMap = new HashMap<String, String>(); 215 systemPropNameMap.put(SYSPROP_FULLTEXT_JOBID, KEY_FULLTEXT_JOBID); 216 } 217 218 public DBSDocument(DBSDocumentState docState, DocumentType type, DBSSession session, boolean readonly) { 219 // no state for NullDocument (parent of placeless children) 220 this.id = docState == null ? null : (String) docState.get(KEY_ID); 221 this.docState = docState; 222 this.type = type; 223 this.session = session; 224 if (docState != null && isProxy()) { 225 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 226 proxySchemas = schemaManager.getProxySchemas(type.getName()); 227 } else { 228 proxySchemas = null; 229 } 230 this.readonly = readonly; 231 } 232 233 @Override 234 public DocumentType getType() { 235 return type; 236 } 237 238 @Override 239 public Session getSession() { 240 return session; 241 } 242 243 @Override 244 public String getRepositoryName() { 245 return session.getRepositoryName(); 246 } 247 248 @Override 249 protected List<Schema> getProxySchemas() { 250 return proxySchemas; 251 } 252 253 @Override 254 public String getUUID() { 255 return id; 256 } 257 258 @Override 259 public String getName() { 260 return docState.getName(); 261 } 262 263 @Override 264 public Long getPos() { 265 return (Long) docState.get(KEY_POS); 266 } 267 268 @Override 269 public Document getParent() { 270 if (isVersion()) { 271 Document workingCopy = session.getDocument(getVersionSeriesId()); 272 return workingCopy == null ? null : workingCopy.getParent(); 273 } 274 String parentId = docState.getParentId(); 275 return parentId == null ? null : session.getDocument(parentId); 276 } 277 278 @Override 279 public boolean isProxy() { 280 return TRUE.equals(docState.get(KEY_IS_PROXY)); 281 } 282 283 @Override 284 public boolean isVersion() { 285 return TRUE.equals(docState.get(KEY_IS_VERSION)); 286 } 287 288 @Override 289 public String getPath() { 290 if (isVersion()) { 291 Document workingCopy = session.getDocument(getVersionSeriesId()); 292 return workingCopy == null ? null : workingCopy.getPath(); 293 } 294 String name = getName(); 295 Document doc = getParent(); 296 if (doc == null) { 297 if ("".equals(name)) { 298 return "/"; // root 299 } else { 300 return name; // placeless, no slash 301 } 302 } 303 LinkedList<String> list = new LinkedList<String>(); 304 list.addFirst(name); 305 while (doc != null) { 306 list.addFirst(doc.getName()); 307 doc = doc.getParent(); 308 } 309 return StringUtils.join(list, '/'); 310 } 311 312 @Override 313 public Document getChild(String name) { 314 return session.getChild(id, name); 315 } 316 317 @Override 318 public List<Document> getChildren() { 319 if (!isFolder()) { 320 return Collections.emptyList(); 321 } 322 return session.getChildren(id); 323 } 324 325 @Override 326 public List<String> getChildrenIds() { 327 if (!isFolder()) { 328 return Collections.emptyList(); 329 } 330 return session.getChildrenIds(id); 331 } 332 333 @Override 334 public boolean hasChild(String name) { 335 if (!isFolder()) { 336 return false; 337 } 338 return session.hasChild(id, name); 339 } 340 341 @Override 342 public boolean hasChildren() { 343 if (!isFolder()) { 344 return false; 345 } 346 return session.hasChildren(id); 347 } 348 349 @Override 350 public Document addChild(String name, String typeName) { 351 if (!isFolder()) { 352 throw new IllegalArgumentException("Not a folder"); 353 } 354 return session.createChild(null, id, name, null, typeName); 355 } 356 357 @Override 358 public void orderBefore(String src, String dest) { 359 Document srcDoc = getChild(src); 360 if (srcDoc == null) { 361 throw new DocumentNotFoundException("Document " + this + " has no child: " + src); 362 } 363 Document destDoc; 364 if (dest == null) { 365 destDoc = null; 366 } else { 367 destDoc = getChild(dest); 368 if (destDoc == null) { 369 throw new DocumentNotFoundException("Document " + this + " has no child: " + dest); 370 } 371 } 372 session.orderBefore(id, srcDoc.getUUID(), destDoc == null ? null : destDoc.getUUID()); 373 } 374 375 // simple property only 376 @Override 377 public Serializable getPropertyValue(String name) { 378 DBSDocumentState docState = getStateOrTarget(name); 379 return docState.get(name); 380 } 381 382 // simple property only 383 @Override 384 public void setPropertyValue(String name, Serializable value) { 385 DBSDocumentState docState = getStateOrTarget(name); 386 docState.put(name, value); 387 } 388 389 // helpers for getValue / setValue 390 391 @Override 392 protected State getChild(State state, String name, Type type) { 393 return (State) state.get(name); 394 } 395 396 @Override 397 protected State getChildForWrite(State state, String name, Type type) throws PropertyException { 398 State child = getChild(state, name, type); 399 if (child == null) { 400 state.put(name, child = new State()); 401 } 402 return child; 403 } 404 405 @Override 406 protected List<State> getChildAsList(State state, String name) { 407 @SuppressWarnings("unchecked") 408 List<State> list = (List<State>) state.get(name); 409 if (list == null) { 410 list = new ArrayList<>(); 411 } 412 return list; 413 } 414 415 @Override 416 protected void updateList(State state, String name, Field field, String xpath, List<Object> values) { 417 List<State> childStates = new ArrayList<>(values.size()); 418 int i = 0; 419 for (Object v : values) { 420 State childState = new State(); 421 setValueComplex(childState, field, xpath + '/' + i, v); 422 childStates.add(childState); 423 i++; 424 } 425 state.put(name, (Serializable) childStates); 426 } 427 428 @Override 429 protected List<State> updateList(State state, String name, Property property) throws PropertyException { 430 Collection<Property> properties = property.getChildren(); 431 int newSize = properties.size(); 432 @SuppressWarnings("unchecked") 433 List<State> childStates = (List<State>) state.get(name); 434 if (childStates == null) { 435 childStates = new ArrayList<>(newSize); 436 state.put(name, (Serializable) childStates); 437 } 438 int oldSize = childStates.size(); 439 // remove extra list elements 440 if (oldSize > newSize) { 441 for (int i = oldSize - 1; i >= newSize; i--) { 442 childStates.remove(i); 443 } 444 } 445 // add new list elements 446 if (oldSize < newSize) { 447 for (int i = oldSize; i < newSize; i++) { 448 childStates.add(new State()); 449 } 450 } 451 return childStates; 452 } 453 454 @Override 455 public Object getValue(String xpath) throws PropertyException { 456 DBSDocumentState docState = getStateOrTarget(xpath); 457 return getValueObject(docState.getState(), xpath); 458 } 459 460 @Override 461 public void setValue(String xpath, Object value) throws PropertyException { 462 DBSDocumentState docState = getStateOrTarget(xpath); 463 // markDirty has to be called *before* we change the state 464 docState.markDirty(); 465 setValueObject(docState.getState(), xpath, value); 466 } 467 468 @Override 469 public void visitBlobs(Consumer<BlobAccessor> blobVisitor) throws PropertyException { 470 if (isProxy()) { 471 getTargetDocument().visitBlobs(blobVisitor); 472 // fall through for proxy schemas 473 } 474 Runnable markDirty = () -> docState.markDirty(); 475 visitBlobs(docState.getState(), blobVisitor, markDirty); 476 } 477 478 @Override 479 public Document checkIn(String label, String checkinComment) { 480 if (isProxy()) { 481 return getTargetDocument().checkIn(label, checkinComment); 482 } else if (isVersion()) { 483 throw new VersionNotModifiableException(); 484 } else { 485 Document version = session.checkIn(id, label, checkinComment); 486 Framework.getService(BlobManager.class).freezeVersion(version); 487 return version; 488 } 489 } 490 491 @Override 492 public void checkOut() { 493 if (isProxy()) { 494 getTargetDocument().checkOut(); 495 } else if (isVersion()) { 496 throw new VersionNotModifiableException(); 497 } else { 498 session.checkOut(id); 499 } 500 } 501 502 @Override 503 public List<String> getVersionsIds() { 504 return session.getVersionsIds(getVersionSeriesId()); 505 } 506 507 @Override 508 public List<Document> getVersions() { 509 List<String> ids = session.getVersionsIds(getVersionSeriesId()); 510 List<Document> versions = new ArrayList<Document>(); 511 for (String id : ids) { 512 versions.add(session.getDocument(id)); 513 } 514 return versions; 515 } 516 517 @Override 518 public Document getLastVersion() { 519 return session.getLastVersion(getVersionSeriesId()); 520 } 521 522 @Override 523 public Document getSourceDocument() { 524 if (isProxy()) { 525 return getTargetDocument(); 526 } else if (isVersion()) { 527 return getWorkingCopy(); 528 } else { 529 return this; 530 } 531 } 532 533 @Override 534 public void restore(Document version) { 535 if (!version.isVersion()) { 536 throw new NuxeoException("Cannot restore a non-version: " + version); 537 } 538 session.restoreVersion(this, version); 539 } 540 541 @Override 542 public Document getVersion(String label) { 543 DBSDocumentState state = session.getVersionByLabel(getVersionSeriesId(), label); 544 return session.getDocument(state); 545 } 546 547 @Override 548 public Document getBaseVersion() { 549 if (isProxy()) { 550 return getTargetDocument().getBaseVersion(); 551 } else if (isVersion()) { 552 return null; 553 } else { 554 if (isCheckedOut()) { 555 return null; 556 } else { 557 String id = (String) docState.get(KEY_BASE_VERSION_ID); 558 if (id == null) { 559 // shouldn't happen 560 return null; 561 } 562 return session.getDocument(id); 563 } 564 } 565 } 566 567 @Override 568 public boolean isCheckedOut() { 569 if (isProxy()) { 570 return getTargetDocument().isCheckedOut(); 571 } else if (isVersion()) { 572 return false; 573 } else { 574 return !TRUE.equals(docState.get(KEY_IS_CHECKED_IN)); 575 } 576 } 577 578 @Override 579 public String getVersionSeriesId() { 580 if (isProxy()) { 581 return (String) docState.get(KEY_PROXY_VERSION_SERIES_ID); 582 } else if (isVersion()) { 583 return (String) docState.get(KEY_VERSION_SERIES_ID); 584 } else { 585 return getUUID(); 586 } 587 } 588 589 @Override 590 public Calendar getVersionCreationDate() { 591 DBSDocumentState docState = getStateOrTarget(); 592 return (Calendar) docState.get(KEY_VERSION_CREATED); 593 } 594 595 @Override 596 public String getVersionLabel() { 597 DBSDocumentState docState = getStateOrTarget(); 598 return (String) docState.get(KEY_VERSION_LABEL); 599 } 600 601 @Override 602 public String getCheckinComment() { 603 DBSDocumentState docState = getStateOrTarget(); 604 return (String) docState.get(KEY_VERSION_DESCRIPTION); 605 } 606 607 @Override 608 public boolean isLatestVersion() { 609 return isEqualOnVersion(TRUE, KEY_IS_LATEST_VERSION); 610 } 611 612 @Override 613 public boolean isMajorVersion() { 614 return isEqualOnVersion(ZERO, KEY_MINOR_VERSION); 615 } 616 617 @Override 618 public boolean isLatestMajorVersion() { 619 return isEqualOnVersion(TRUE, KEY_IS_LATEST_MAJOR_VERSION); 620 } 621 622 protected boolean isEqualOnVersion(Object ob, String key) { 623 if (isProxy()) { 624 // TODO avoid getting the target just to check if it's a version 625 // use another specific property instead 626 if (getTargetDocument().isVersion()) { 627 return ob.equals(docState.get(key)); 628 } else { 629 // if live version, return false 630 return false; 631 } 632 } else if (isVersion()) { 633 return ob.equals(docState.get(key)); 634 } else { 635 return false; 636 } 637 } 638 639 @Override 640 public boolean isVersionSeriesCheckedOut() { 641 if (isProxy() || isVersion()) { 642 Document workingCopy = getWorkingCopy(); 643 return workingCopy == null ? false : workingCopy.isCheckedOut(); 644 } else { 645 return isCheckedOut(); 646 } 647 } 648 649 @Override 650 public Document getWorkingCopy() { 651 if (isProxy() || isVersion()) { 652 String versionSeriesId = getVersionSeriesId(); 653 return versionSeriesId == null ? null : session.getDocument(versionSeriesId); 654 } else { 655 return this; 656 } 657 } 658 659 @Override 660 public boolean isFolder() { 661 return type == null // null document 662 || type.isFolder(); 663 } 664 665 @Override 666 public void setReadOnly(boolean readonly) { 667 this.readonly = readonly; 668 } 669 670 @Override 671 public boolean isReadOnly() { 672 return readonly; 673 } 674 675 @Override 676 public void remove() { 677 session.remove(id); 678 } 679 680 @Override 681 public String getLifeCycleState() { 682 DBSDocumentState docState = getStateOrTarget(); 683 return (String) docState.get(KEY_LIFECYCLE_STATE); 684 } 685 686 @Override 687 public void setCurrentLifeCycleState(String lifeCycleState) throws LifeCycleException { 688 DBSDocumentState docState = getStateOrTarget(); 689 docState.put(KEY_LIFECYCLE_STATE, lifeCycleState); 690 BlobManager blobManager = Framework.getService(BlobManager.class); 691 blobManager.notifyChanges(this, Collections.singleton(KEY_LIFECYCLE_STATE)); 692 } 693 694 @Override 695 public String getLifeCyclePolicy() { 696 DBSDocumentState docState = getStateOrTarget(); 697 return (String) docState.get(KEY_LIFECYCLE_POLICY); 698 } 699 700 @Override 701 public void setLifeCyclePolicy(String policy) throws LifeCycleException { 702 DBSDocumentState docState = getStateOrTarget(); 703 docState.put(KEY_LIFECYCLE_POLICY, policy); 704 BlobManager blobManager = Framework.getService(BlobManager.class); 705 blobManager.notifyChanges(this, Collections.singleton(KEY_LIFECYCLE_POLICY)); 706 } 707 708 // TODO generic 709 @Override 710 public void followTransition(String transition) throws LifeCycleException { 711 LifeCycleService service = NXCore.getLifeCycleService(); 712 if (service == null) { 713 throw new LifeCycleException("LifeCycleService not available"); 714 } 715 service.followTransition(this, transition); 716 } 717 718 // TODO generic 719 @Override 720 public Collection<String> getAllowedStateTransitions() throws LifeCycleException { 721 LifeCycleService service = NXCore.getLifeCycleService(); 722 if (service == null) { 723 throw new LifeCycleException("LifeCycleService not available"); 724 } 725 LifeCycle lifeCycle = service.getLifeCycleFor(this); 726 if (lifeCycle == null) { 727 return Collections.emptyList(); 728 } 729 return lifeCycle.getAllowedStateTransitionsFrom(getLifeCycleState()); 730 } 731 732 @Override 733 public void setSystemProp(String name, Serializable value) { 734 String propertyName; 735 if (name.startsWith(SYSPROP_FULLTEXT_SIMPLE)) { 736 propertyName = name.replace(SYSPROP_FULLTEXT_SIMPLE, KEY_FULLTEXT_SIMPLE); 737 } else if (name.startsWith(SYSPROP_FULLTEXT_BINARY)) { 738 propertyName = name.replace(SYSPROP_FULLTEXT_BINARY, KEY_FULLTEXT_BINARY); 739 } else { 740 propertyName = systemPropNameMap.get(name); 741 } 742 if (propertyName == null) { 743 throw new PropertyNotFoundException(name, "Unknown system property"); 744 } 745 setPropertyValue(propertyName, value); 746 } 747 748 @SuppressWarnings("unchecked") 749 @Override 750 public <T extends Serializable> T getSystemProp(String name, Class<T> type) { 751 String propertyName = systemPropNameMap.get(name); 752 if (propertyName == null) { 753 throw new PropertyNotFoundException(name, "Unknown system property: "); 754 } 755 Serializable value = getPropertyValue(propertyName); 756 if (value == null) { 757 if (type == Boolean.class) { 758 value = Boolean.FALSE; 759 } else if (type == Long.class) { 760 value = Long.valueOf(0); 761 } 762 } 763 return (T) value; 764 } 765 766 @Override 767 public String getChangeToken() { 768 DBSDocumentState docState = getStateOrTarget(); 769 if (session.changeTokenEnabled) { 770 return (String) docState.get(KEY_CHANGE_TOKEN); 771 } else { 772 Calendar modified = (Calendar) docState.get(KEY_DC_MODIFIED); 773 return modified == null ? null : String.valueOf(modified.getTimeInMillis()); 774 } 775 } 776 777 protected DBSDocumentState getStateOrTarget(Type type) throws PropertyException { 778 return getStateOrTargetForSchema(type.getName()); 779 } 780 781 protected DBSDocumentState getStateOrTarget(String xpath) { 782 return getStateOrTargetForSchema(getSchema(xpath)); 783 } 784 785 /** 786 * Checks if the given schema should be resolved on the proxy or the target. 787 */ 788 protected DBSDocumentState getStateOrTargetForSchema(String schema) { 789 if (isProxy() && !isSchemaForProxy(schema)) { 790 return getTargetDocument().docState; 791 } else { 792 return docState; 793 } 794 } 795 796 /** 797 * Gets the target state if this is a proxy, or the regular state otherwise. 798 */ 799 protected DBSDocumentState getStateOrTarget() { 800 if (isProxy()) { 801 return getTargetDocument().docState; 802 } else { 803 return docState; 804 } 805 } 806 807 protected boolean isSchemaForProxy(String schema) { 808 SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class); 809 return schemaManager.isProxySchema(schema, getType().getName()); 810 } 811 812 protected String getSchema(String xpath) { 813 switch (xpath) { 814 case KEY_MAJOR_VERSION: 815 case KEY_MINOR_VERSION: 816 case "major_version": 817 case "minor_version": 818 return "uid"; 819 case KEY_FULLTEXT_JOBID: 820 case KEY_LIFECYCLE_POLICY: 821 case KEY_LIFECYCLE_STATE: 822 return "__ecm__"; 823 } 824 if (xpath.startsWith(KEY_FULLTEXT_SIMPLE) || xpath.startsWith(KEY_FULLTEXT_BINARY)) { 825 return "__ecm__"; 826 } 827 String[] segments = xpath.split("/"); 828 String segment = segments[0]; 829 Field field = type.getField(segment); 830 if (field == null) { 831 // check facets 832 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 833 for (String facet : getFacets()) { 834 CompositeType facetType = schemaManager.getFacet(facet); 835 field = facetType.getField(segment); 836 if (field != null) { 837 break; 838 } 839 } 840 } 841 if (field == null && getProxySchemas() != null) { 842 // check proxy schemas 843 for (Schema schema : getProxySchemas()) { 844 field = schema.getField(segment); 845 if (field != null) { 846 break; 847 } 848 } 849 } 850 if (field == null) { 851 throw new PropertyNotFoundException(xpath); 852 } 853 return field.getDeclaringType().getName(); 854 } 855 856 @Override 857 public void readDocumentPart(DocumentPart dp) throws PropertyException { 858 DBSDocumentState docState = getStateOrTarget(dp.getType()); 859 readComplexProperty(docState.getState(), (ComplexProperty) dp); 860 } 861 862 @Override 863 protected String internalName(String name) { 864 switch (name) { 865 case "major_version": 866 return KEY_MAJOR_VERSION; 867 case "minor_version": 868 return KEY_MINOR_VERSION; 869 } 870 return name; 871 } 872 873 @Override 874 public Map<String, Serializable> readPrefetch(ComplexType complexType, Set<String> xpaths) 875 throws PropertyException { 876 DBSDocumentState docState = getStateOrTarget(complexType); 877 return readPrefetch(docState.getState(), complexType, xpaths); 878 } 879 880 @Override 881 public boolean writeDocumentPart(DocumentPart dp, WriteContext writeContext) throws PropertyException { 882 DBSDocumentState docState = getStateOrTarget(dp.getType()); 883 // markDirty has to be called *before* we change the state 884 docState.markDirty(); 885 boolean changed = writeComplexProperty(docState.getState(), (ComplexProperty) dp, writeContext); 886 clearDirtyFlags(dp); 887 return changed; 888 } 889 890 @Override 891 public Set<String> getAllFacets() { 892 Set<String> facets = new HashSet<String>(getType().getFacets()); 893 facets.addAll(Arrays.asList(getFacets())); 894 return facets; 895 } 896 897 @Override 898 public String[] getFacets() { 899 DBSDocumentState docState = getStateOrTarget(); 900 Object[] mixins = (Object[]) docState.get(KEY_MIXIN_TYPES); 901 if (mixins == null) { 902 return EMPTY_STRING_ARRAY; 903 } else { 904 String[] res = new String[mixins.length]; 905 System.arraycopy(mixins, 0, res, 0, mixins.length); 906 return res; 907 } 908 } 909 910 @Override 911 public boolean hasFacet(String facet) { 912 return getAllFacets().contains(facet); 913 } 914 915 @Override 916 public boolean addFacet(String facet) { 917 if (getType().getFacets().contains(facet)) { 918 return false; // already present in type 919 } 920 DBSDocumentState docState = getStateOrTarget(); 921 Object[] mixins = (Object[]) docState.get(KEY_MIXIN_TYPES); 922 if (mixins == null) { 923 mixins = new Object[] { facet }; 924 } else { 925 List<Object> list = Arrays.asList(mixins); 926 if (list.contains(facet)) { 927 return false; // already present in doc 928 } 929 list = new ArrayList<Object>(list); 930 list.add(facet); 931 mixins = list.toArray(new Object[list.size()]); 932 } 933 docState.put(KEY_MIXIN_TYPES, mixins); 934 return true; 935 } 936 937 @Override 938 public boolean removeFacet(String facet) { 939 DBSDocumentState docState = getStateOrTarget(); 940 Object[] mixins = (Object[]) docState.get(KEY_MIXIN_TYPES); 941 if (mixins == null) { 942 return false; 943 } 944 List<Object> list = new ArrayList<Object>(Arrays.asList(mixins)); 945 if (!list.remove(facet)) { 946 return false; // not present in doc 947 } 948 mixins = list.toArray(new Object[list.size()]); 949 if (mixins.length == 0) { 950 mixins = null; 951 } 952 docState.put(KEY_MIXIN_TYPES, mixins); 953 // remove the fields belonging to the facet 954 // except for schemas still present due to the primary type or another facet 955 SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class); 956 CompositeType ft = schemaManager.getFacet(facet); 957 Set<String> otherSchemas = getSchemas(getType(), list); 958 for (Schema schema : ft.getSchemas()) { 959 if (otherSchemas.contains(schema.getName())) { 960 continue; 961 } 962 for (Field field : schema.getFields()) { 963 String name = field.getName().getPrefixedName(); 964 if (docState.containsKey(name)) { 965 docState.put(name, null); 966 } 967 } 968 } 969 return true; 970 } 971 972 protected static Set<String> getSchemas(DocumentType type, List<Object> facets) { 973 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 974 Set<String> schemas = new HashSet<>(Arrays.asList(type.getSchemaNames())); 975 for (Object facet : facets) { 976 CompositeType ft = schemaManager.getFacet((String) facet); 977 if (ft != null) { 978 schemas.addAll(Arrays.asList(ft.getSchemaNames())); 979 } 980 } 981 return schemas; 982 } 983 984 @Override 985 public DBSDocument getTargetDocument() { 986 if (isProxy()) { 987 String targetId = (String) docState.get(KEY_PROXY_TARGET_ID); 988 return session.getDocument(targetId); 989 } else { 990 return null; 991 } 992 } 993 994 @Override 995 public void setTargetDocument(Document target) { 996 if (isProxy()) { 997 if (isReadOnly()) { 998 throw new ReadOnlyPropertyException("Cannot write proxy: " + this); 999 } 1000 if (!target.getVersionSeriesId().equals(getVersionSeriesId())) { 1001 throw new ReadOnlyPropertyException("Cannot set proxy target to different version series"); 1002 } 1003 session.setProxyTarget(this, target); 1004 } else { 1005 throw new NuxeoException("Cannot set proxy target on non-proxy"); 1006 } 1007 } 1008 1009 @Override 1010 protected Lock getDocumentLock() { 1011 String owner = (String) docState.get(KEY_LOCK_OWNER); 1012 if (owner == null) { 1013 return null; 1014 } 1015 Calendar created = (Calendar) docState.get(KEY_LOCK_CREATED); 1016 return new Lock(owner, created); 1017 } 1018 1019 @Override 1020 protected Lock setDocumentLock(Lock lock) { 1021 String owner = (String) docState.get(KEY_LOCK_OWNER); 1022 if (owner != null) { 1023 // return old lock 1024 Calendar created = (Calendar) docState.get(KEY_LOCK_CREATED); 1025 return new Lock(owner, created); 1026 } 1027 docState.put(KEY_LOCK_OWNER, lock.getOwner()); 1028 docState.put(KEY_LOCK_CREATED, lock.getCreated()); 1029 return null; 1030 } 1031 1032 @Override 1033 protected Lock removeDocumentLock(String owner) { 1034 String oldOwner = (String) docState.get(KEY_LOCK_OWNER); 1035 if (oldOwner == null) { 1036 // no previous lock 1037 return null; 1038 } 1039 Calendar oldCreated = (Calendar) docState.get(KEY_LOCK_CREATED); 1040 if (!LockManager.canLockBeRemoved(oldOwner, owner)) { 1041 // existing mismatched lock, flag failure 1042 return new Lock(oldOwner, oldCreated, true); 1043 } 1044 // remove lock 1045 docState.put(KEY_LOCK_OWNER, null); 1046 docState.put(KEY_LOCK_CREATED, null); 1047 // return old lock 1048 return new Lock(oldOwner, oldCreated); 1049 } 1050 1051 @Override 1052 public String toString() { 1053 return getClass().getSimpleName() + '(' + getName() + ',' + getUUID() + ')'; 1054 } 1055 1056 @Override 1057 public boolean equals(Object other) { 1058 if (other == this) { 1059 return true; 1060 } 1061 if (other == null) { 1062 return false; 1063 } 1064 if (other.getClass() == getClass()) { 1065 return equals((DBSDocument) other); 1066 } 1067 return false; 1068 } 1069 1070 private boolean equals(DBSDocument other) { 1071 return id.equals(other.id); 1072 } 1073 1074 @Override 1075 public int hashCode() { 1076 return id.hashCode(); 1077 } 1078 1079}