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