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