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.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.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_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 public static final String KEY_LOCK_OWNER = "ecm:lockOwner"; 169 170 public static final String KEY_LOCK_CREATED = "ecm:lockCreated"; 171 172 public static final String KEY_SYS_CHANGE_TOKEN = "ecm:systemChangeToken"; 173 174 public static final String KEY_CHANGE_TOKEN = "ecm:changeToken"; 175 176 // used instead of ecm:changeToken when change tokens are disabled 177 public static final String KEY_DC_MODIFIED = "dc:modified"; 178 179 public static final String KEY_BLOB_NAME = "name"; 180 181 public static final String KEY_BLOB_MIME_TYPE = "mime-type"; 182 183 public static final String KEY_BLOB_ENCODING = "encoding"; 184 185 public static final String KEY_BLOB_DIGEST = "digest"; 186 187 public static final String KEY_BLOB_LENGTH = "length"; 188 189 public static final String KEY_BLOB_DATA = "data"; 190 191 public static final String KEY_FULLTEXT_SIMPLE = "ecm:fulltextSimple"; 192 193 public static final String KEY_FULLTEXT_BINARY = "ecm:fulltextBinary"; 194 195 public static final String KEY_FULLTEXT_JOBID = "ecm:fulltextJobId"; 196 197 public static final String KEY_FULLTEXT_SCORE = "ecm:fulltextScore"; 198 199 public static final String APPLICATION_OCTET_STREAM = "application/octet-stream"; 200 201 public static final String PROP_UID_MAJOR_VERSION = "uid:major_version"; 202 203 public static final String PROP_UID_MINOR_VERSION = "uid:minor_version"; 204 205 public static final String PROP_MAJOR_VERSION = "major_version"; 206 207 public static final String PROP_MINOR_VERSION = "minor_version"; 208 /** 209 * @since 9.3 210 */ 211 public static final String FACETED_TAG = "nxtag:tags"; 212 213 /** 214 * @since 9.3 215 */ 216 public static final String FACETED_TAG_LABEL = "label"; 217 218 public static final Long INITIAL_SYS_CHANGE_TOKEN = Long.valueOf(0); 219 220 public static final Long INITIAL_CHANGE_TOKEN = Long.valueOf(0); 221 222 protected final String id; 223 224 protected final DBSDocumentState docState; 225 226 protected final DocumentType type; 227 228 protected final List<Schema> proxySchemas; 229 230 protected final DBSSession session; 231 232 protected boolean readonly; 233 234 protected static final Map<String, String> systemPropNameMap; 235 236 static { 237 systemPropNameMap = new HashMap<String, String>(); 238 systemPropNameMap.put(SYSPROP_FULLTEXT_JOBID, KEY_FULLTEXT_JOBID); 239 } 240 241 public DBSDocument(DBSDocumentState docState, DocumentType type, DBSSession session, boolean readonly) { 242 // no state for NullDocument (parent of placeless children) 243 this.id = docState == null ? null : (String) docState.get(KEY_ID); 244 this.docState = docState; 245 this.type = type; 246 this.session = session; 247 if (docState != null && isProxy()) { 248 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 249 proxySchemas = schemaManager.getProxySchemas(type.getName()); 250 } else { 251 proxySchemas = null; 252 } 253 this.readonly = readonly; 254 } 255 256 @Override 257 public DocumentType getType() { 258 return type; 259 } 260 261 @Override 262 public Session getSession() { 263 return session; 264 } 265 266 @Override 267 public String getRepositoryName() { 268 return session.getRepositoryName(); 269 } 270 271 @Override 272 protected List<Schema> getProxySchemas() { 273 return proxySchemas; 274 } 275 276 @Override 277 public String getUUID() { 278 return id; 279 } 280 281 @Override 282 public String getName() { 283 return docState.getName(); 284 } 285 286 @Override 287 public Long getPos() { 288 return (Long) docState.get(KEY_POS); 289 } 290 291 @Override 292 public Document getParent() { 293 if (isVersion()) { 294 Document workingCopy = session.getDocument(getVersionSeriesId()); 295 return workingCopy == null ? null : workingCopy.getParent(); 296 } 297 String parentId = docState.getParentId(); 298 return parentId == null ? null : session.getDocument(parentId); 299 } 300 301 @Override 302 public boolean isProxy() { 303 return TRUE.equals(docState.get(KEY_IS_PROXY)); 304 } 305 306 @Override 307 public boolean isVersion() { 308 return TRUE.equals(docState.get(KEY_IS_VERSION)); 309 } 310 311 @Override 312 public String getPath() { 313 if (isVersion()) { 314 Document workingCopy = session.getDocument(getVersionSeriesId()); 315 return workingCopy == null ? null : workingCopy.getPath(); 316 } 317 String name = getName(); 318 Document doc = getParent(); 319 if (doc == null) { 320 if ("".equals(name)) { 321 return "/"; // root 322 } else { 323 return name; // placeless, no slash 324 } 325 } 326 LinkedList<String> list = new LinkedList<String>(); 327 list.addFirst(name); 328 while (doc != null) { 329 list.addFirst(doc.getName()); 330 doc = doc.getParent(); 331 } 332 return StringUtils.join(list, '/'); 333 } 334 335 @Override 336 public Document getChild(String name) { 337 return session.getChild(id, name); 338 } 339 340 @Override 341 public List<Document> getChildren() { 342 if (!isFolder()) { 343 return Collections.emptyList(); 344 } 345 return session.getChildren(id); 346 } 347 348 @Override 349 public List<String> getChildrenIds() { 350 if (!isFolder()) { 351 return Collections.emptyList(); 352 } 353 return session.getChildrenIds(id); 354 } 355 356 @Override 357 public boolean hasChild(String name) { 358 if (!isFolder()) { 359 return false; 360 } 361 return session.hasChild(id, name); 362 } 363 364 @Override 365 public boolean hasChildren() { 366 if (!isFolder()) { 367 return false; 368 } 369 return session.hasChildren(id); 370 } 371 372 @Override 373 public Document addChild(String name, String typeName) { 374 if (!isFolder()) { 375 throw new IllegalArgumentException("Not a folder"); 376 } 377 return session.createChild(null, id, name, null, typeName); 378 } 379 380 @Override 381 public void orderBefore(String src, String dest) { 382 Document srcDoc = getChild(src); 383 if (srcDoc == null) { 384 throw new DocumentNotFoundException("Document " + this + " has no child: " + src); 385 } 386 Document destDoc; 387 if (dest == null) { 388 destDoc = null; 389 } else { 390 destDoc = getChild(dest); 391 if (destDoc == null) { 392 throw new DocumentNotFoundException("Document " + this + " has no child: " + dest); 393 } 394 } 395 session.orderBefore(id, srcDoc.getUUID(), destDoc == null ? null : destDoc.getUUID()); 396 } 397 398 // simple property only 399 @Override 400 public Serializable getPropertyValue(String name) { 401 DBSDocumentState docState = getStateOrTarget(name); 402 return docState.get(name); 403 } 404 405 // simple property only 406 @Override 407 public void setPropertyValue(String name, Serializable value) { 408 DBSDocumentState docState = getStateOrTarget(name); 409 docState.put(name, value); 410 } 411 412 // helpers for getValue / setValue 413 414 @Override 415 protected State getChild(State state, String name, Type type) { 416 return (State) state.get(name); 417 } 418 419 @Override 420 protected State getChildForWrite(State state, String name, Type type) throws PropertyException { 421 State child = getChild(state, name, type); 422 if (child == null) { 423 state.put(name, child = new State()); 424 } 425 return child; 426 } 427 428 @Override 429 protected List<State> getChildAsList(State state, String name) { 430 @SuppressWarnings("unchecked") 431 List<State> list = (List<State>) state.get(name); 432 if (list == null) { 433 list = new ArrayList<>(); 434 } 435 return list; 436 } 437 438 @Override 439 protected void updateList(State state, String name, Field field, String xpath, List<Object> values) { 440 List<State> childStates = new ArrayList<>(values.size()); 441 int i = 0; 442 for (Object v : values) { 443 State childState = new State(); 444 setValueComplex(childState, field, xpath + '/' + i, v); 445 childStates.add(childState); 446 i++; 447 } 448 state.put(name, (Serializable) childStates); 449 } 450 451 @Override 452 protected List<State> updateList(State state, String name, Property property) throws PropertyException { 453 Collection<Property> properties = property.getChildren(); 454 int newSize = properties.size(); 455 @SuppressWarnings("unchecked") 456 List<State> childStates = (List<State>) state.get(name); 457 if (childStates == null) { 458 childStates = new ArrayList<>(newSize); 459 state.put(name, (Serializable) childStates); 460 } 461 int oldSize = childStates.size(); 462 // remove extra list elements 463 if (oldSize > newSize) { 464 for (int i = oldSize - 1; i >= newSize; i--) { 465 childStates.remove(i); 466 } 467 } 468 // add new list elements 469 if (oldSize < newSize) { 470 for (int i = oldSize; i < newSize; i++) { 471 childStates.add(new State()); 472 } 473 } 474 return childStates; 475 } 476 477 @Override 478 public Object getValue(String xpath) throws PropertyException { 479 DBSDocumentState docState = getStateOrTarget(xpath); 480 return getValueObject(docState.getState(), xpath); 481 } 482 483 @Override 484 public void setValue(String xpath, Object value) throws PropertyException { 485 DBSDocumentState docState = getStateOrTarget(xpath); 486 // markDirty has to be called *before* we change the state 487 docState.markDirty(); 488 setValueObject(docState.getState(), xpath, value); 489 } 490 491 @Override 492 public void visitBlobs(Consumer<BlobAccessor> blobVisitor) throws PropertyException { 493 if (isProxy()) { 494 getTargetDocument().visitBlobs(blobVisitor); 495 // fall through for proxy schemas 496 } 497 Runnable markDirty = () -> docState.markDirty(); 498 visitBlobs(docState.getState(), blobVisitor, markDirty); 499 } 500 501 @Override 502 public boolean isRetentionActive() { 503 DBSDocumentState docState = getStateOrTarget(); 504 return TRUE.equals(docState.get(KEY_IS_RETENTION_ACTIVE)); 505 } 506 507 @Override 508 public void setRetentionActive(boolean retentionActive) { 509 DBSDocumentState docState = getStateOrTarget(); 510 docState.put(KEY_IS_RETENTION_ACTIVE, retentionActive ? TRUE : null); 511 } 512 513 @Override 514 public Document checkIn(String label, String checkinComment) { 515 if (isProxy()) { 516 return getTargetDocument().checkIn(label, checkinComment); 517 } else if (isVersion()) { 518 throw new VersionNotModifiableException(); 519 } else { 520 Document version = session.checkIn(id, label, checkinComment); 521 DocumentBlobManager blobManager = Framework.getService(DocumentBlobManager.class); 522 blobManager.freezeVersion(version); 523 return version; 524 } 525 } 526 527 @Override 528 public void checkOut() { 529 if (isProxy()) { 530 getTargetDocument().checkOut(); 531 } else if (isVersion()) { 532 throw new VersionNotModifiableException(); 533 } else { 534 session.checkOut(id); 535 } 536 } 537 538 @Override 539 public List<String> getVersionsIds() { 540 return session.getVersionsIds(getVersionSeriesId()); 541 } 542 543 @Override 544 public List<Document> getVersions() { 545 List<String> ids = session.getVersionsIds(getVersionSeriesId()); 546 List<Document> versions = new ArrayList<Document>(); 547 for (String id : ids) { 548 versions.add(session.getDocument(id)); 549 } 550 return versions; 551 } 552 553 @Override 554 public Document getLastVersion() { 555 return session.getLastVersion(getVersionSeriesId()); 556 } 557 558 @Override 559 public Document getSourceDocument() { 560 if (isProxy()) { 561 return getTargetDocument(); 562 } else if (isVersion()) { 563 return getWorkingCopy(); 564 } else { 565 return this; 566 } 567 } 568 569 @Override 570 public void restore(Document version) { 571 if (!version.isVersion()) { 572 throw new NuxeoException("Cannot restore a non-version: " + version); 573 } 574 session.restoreVersion(this, version); 575 } 576 577 @Override 578 public Document getVersion(String label) { 579 DBSDocumentState state = session.getVersionByLabel(getVersionSeriesId(), label); 580 return session.getDocument(state); 581 } 582 583 @Override 584 public Document getBaseVersion() { 585 if (isProxy()) { 586 return getTargetDocument().getBaseVersion(); 587 } else if (isVersion()) { 588 return null; 589 } else { 590 if (isCheckedOut()) { 591 return null; 592 } else { 593 String id = (String) docState.get(KEY_BASE_VERSION_ID); 594 if (id == null) { 595 // shouldn't happen 596 return null; 597 } 598 return session.getDocument(id); 599 } 600 } 601 } 602 603 @Override 604 public boolean isCheckedOut() { 605 if (isProxy()) { 606 return getTargetDocument().isCheckedOut(); 607 } else if (isVersion()) { 608 return false; 609 } else { 610 return !TRUE.equals(docState.get(KEY_IS_CHECKED_IN)); 611 } 612 } 613 614 @Override 615 public String getVersionSeriesId() { 616 if (isProxy()) { 617 return (String) docState.get(KEY_PROXY_VERSION_SERIES_ID); 618 } else if (isVersion()) { 619 return (String) docState.get(KEY_VERSION_SERIES_ID); 620 } else { 621 return getUUID(); 622 } 623 } 624 625 @Override 626 public Calendar getVersionCreationDate() { 627 DBSDocumentState docState = getStateOrTarget(); 628 return (Calendar) docState.get(KEY_VERSION_CREATED); 629 } 630 631 @Override 632 public String getVersionLabel() { 633 DBSDocumentState docState = getStateOrTarget(); 634 return (String) docState.get(KEY_VERSION_LABEL); 635 } 636 637 @Override 638 public String getCheckinComment() { 639 DBSDocumentState docState = getStateOrTarget(); 640 return (String) docState.get(KEY_VERSION_DESCRIPTION); 641 } 642 643 @Override 644 public boolean isLatestVersion() { 645 return isEqualOnVersion(TRUE, KEY_IS_LATEST_VERSION); 646 } 647 648 @Override 649 public boolean isMajorVersion() { 650 return isEqualOnVersion(ZERO, KEY_MINOR_VERSION); 651 } 652 653 @Override 654 public boolean isLatestMajorVersion() { 655 return isEqualOnVersion(TRUE, KEY_IS_LATEST_MAJOR_VERSION); 656 } 657 658 protected boolean isEqualOnVersion(Object ob, String key) { 659 if (isProxy()) { 660 // TODO avoid getting the target just to check if it's a version 661 // use another specific property instead 662 if (getTargetDocument().isVersion()) { 663 return ob.equals(docState.get(key)); 664 } else { 665 // if live version, return false 666 return false; 667 } 668 } else if (isVersion()) { 669 return ob.equals(docState.get(key)); 670 } else { 671 return false; 672 } 673 } 674 675 @Override 676 public boolean isVersionSeriesCheckedOut() { 677 if (isProxy() || isVersion()) { 678 Document workingCopy = getWorkingCopy(); 679 return workingCopy == null ? false : workingCopy.isCheckedOut(); 680 } else { 681 return isCheckedOut(); 682 } 683 } 684 685 @Override 686 public Document getWorkingCopy() { 687 if (isProxy() || isVersion()) { 688 String versionSeriesId = getVersionSeriesId(); 689 return versionSeriesId == null ? null : session.getDocument(versionSeriesId); 690 } else { 691 return this; 692 } 693 } 694 695 @Override 696 public boolean isFolder() { 697 return type == null // null document 698 || type.isFolder(); 699 } 700 701 @Override 702 public void setReadOnly(boolean readonly) { 703 this.readonly = readonly; 704 } 705 706 @Override 707 public boolean isReadOnly() { 708 return readonly; 709 } 710 711 @Override 712 public void remove() { 713 session.remove(id); 714 } 715 716 @Override 717 public String getLifeCycleState() { 718 DBSDocumentState docState = getStateOrTarget(); 719 return (String) docState.get(KEY_LIFECYCLE_STATE); 720 } 721 722 @Override 723 public void setCurrentLifeCycleState(String lifeCycleState) throws LifeCycleException { 724 DBSDocumentState docState = getStateOrTarget(); 725 docState.put(KEY_LIFECYCLE_STATE, lifeCycleState); 726 DocumentBlobManager blobManager = Framework.getService(DocumentBlobManager.class); 727 blobManager.notifyChanges(this, Collections.singleton(KEY_LIFECYCLE_STATE)); 728 } 729 730 @Override 731 public String getLifeCyclePolicy() { 732 DBSDocumentState docState = getStateOrTarget(); 733 return (String) docState.get(KEY_LIFECYCLE_POLICY); 734 } 735 736 @Override 737 public void setLifeCyclePolicy(String policy) throws LifeCycleException { 738 DBSDocumentState docState = getStateOrTarget(); 739 docState.put(KEY_LIFECYCLE_POLICY, policy); 740 DocumentBlobManager blobManager = Framework.getService(DocumentBlobManager.class); 741 blobManager.notifyChanges(this, Collections.singleton(KEY_LIFECYCLE_POLICY)); 742 } 743 744 // TODO generic 745 @Override 746 public void followTransition(String transition) throws LifeCycleException { 747 LifeCycleService service = NXCore.getLifeCycleService(); 748 if (service == null) { 749 throw new LifeCycleException("LifeCycleService not available"); 750 } 751 service.followTransition(this, transition); 752 } 753 754 // TODO generic 755 @Override 756 public Collection<String> getAllowedStateTransitions() throws LifeCycleException { 757 LifeCycleService service = NXCore.getLifeCycleService(); 758 if (service == null) { 759 throw new LifeCycleException("LifeCycleService not available"); 760 } 761 LifeCycle lifeCycle = service.getLifeCycleFor(this); 762 if (lifeCycle == null) { 763 return Collections.emptyList(); 764 } 765 return lifeCycle.getAllowedStateTransitionsFrom(getLifeCycleState()); 766 } 767 768 @Override 769 public void setSystemProp(String name, Serializable value) { 770 String propertyName; 771 if (name.startsWith(SYSPROP_FULLTEXT_SIMPLE)) { 772 propertyName = name.replace(SYSPROP_FULLTEXT_SIMPLE, KEY_FULLTEXT_SIMPLE); 773 } else if (name.startsWith(SYSPROP_FULLTEXT_BINARY)) { 774 propertyName = name.replace(SYSPROP_FULLTEXT_BINARY, KEY_FULLTEXT_BINARY); 775 } else { 776 propertyName = systemPropNameMap.get(name); 777 } 778 if (propertyName == null) { 779 throw new PropertyNotFoundException(name, "Unknown system property"); 780 } 781 setPropertyValue(propertyName, value); 782 } 783 784 @SuppressWarnings("unchecked") 785 @Override 786 public <T extends Serializable> T getSystemProp(String name, Class<T> type) { 787 String propertyName = systemPropNameMap.get(name); 788 if (propertyName == null) { 789 throw new PropertyNotFoundException(name, "Unknown system property: "); 790 } 791 Serializable value = getPropertyValue(propertyName); 792 if (value == null) { 793 if (type == Boolean.class) { 794 value = Boolean.FALSE; 795 } else if (type == Long.class) { 796 value = Long.valueOf(0); 797 } 798 } 799 return (T) value; 800 } 801 802 public static final String CHANGE_TOKEN_PROXY_SEP = "/"; 803 804 @Override 805 public String getChangeToken() { 806 if (session.changeTokenEnabled) { 807 Long sysChangeToken = docState.getSysChangeToken(); 808 Long changeToken = docState.getChangeToken(); 809 String userVisibleChangeToken = buildUserVisibleChangeToken(sysChangeToken, changeToken); 810 if (isProxy()) { 811 String targetUserVisibleChangeToken = getTargetDocument().getChangeToken(); 812 return getProxyUserVisibleChangeToken(userVisibleChangeToken, targetUserVisibleChangeToken); 813 } else { 814 return userVisibleChangeToken; 815 } 816 } else { 817 DBSDocumentState docState = getStateOrTarget(); 818 Calendar modified = (Calendar) docState.get(KEY_DC_MODIFIED); 819 return getLegacyChangeToken(modified); 820 } 821 } 822 823 protected static String getProxyUserVisibleChangeToken(String proxyToken, String targetToken) { 824 if (proxyToken == null && targetToken == null) { 825 return null; 826 } else { 827 if (proxyToken == null) { 828 proxyToken = ""; 829 } else if (targetToken == null) { 830 targetToken = ""; 831 } 832 return proxyToken + CHANGE_TOKEN_PROXY_SEP + targetToken; 833 } 834 } 835 836 @Override 837 public boolean validateUserVisibleChangeToken(String userVisibleChangeToken) { 838 if (userVisibleChangeToken == null) { 839 return true; 840 } 841 if (session.changeTokenEnabled) { 842 if (isProxy()) { 843 return validateProxyChangeToken(userVisibleChangeToken, docState, getTargetDocument().docState); 844 } else { 845 return docState.validateUserVisibleChangeToken(userVisibleChangeToken); 846 } 847 } else { 848 DBSDocumentState docState = getStateOrTarget(); 849 Calendar modified = (Calendar) docState.get(KEY_DC_MODIFIED); 850 return validateLegacyChangeToken(modified, userVisibleChangeToken); 851 } 852 } 853 854 protected static boolean validateProxyChangeToken(String userVisibleChangeToken, DBSDocumentState proxyState, 855 DBSDocumentState targetState) { 856 String[] parts = userVisibleChangeToken.split(CHANGE_TOKEN_PROXY_SEP, 2); 857 if (parts.length != 2) { 858 // invalid format 859 return false; 860 } 861 String proxyToken = parts[0]; 862 if (proxyToken.isEmpty()) { 863 proxyToken = null; 864 } 865 String targetToken = parts[1]; 866 if (targetToken.isEmpty()) { 867 targetToken = null; 868 } 869 if (proxyToken == null && targetToken == null) { 870 return true; 871 } 872 return proxyState.validateUserVisibleChangeToken(proxyToken) 873 && targetState.validateUserVisibleChangeToken(targetToken); 874 } 875 876 @Override 877 public void markUserChange() { 878 if (isProxy()) { 879 session.markUserChange(getTargetDocumentId()); 880 } 881 session.markUserChange(id); 882 } 883 884 protected DBSDocumentState getStateOrTarget(Type type) throws PropertyException { 885 return getStateOrTargetForSchema(type.getName()); 886 } 887 888 protected DBSDocumentState getStateOrTarget(String xpath) { 889 return getStateOrTargetForSchema(getSchema(xpath)); 890 } 891 892 /** 893 * Checks if the given schema should be resolved on the proxy or the target. 894 */ 895 protected DBSDocumentState getStateOrTargetForSchema(String schema) { 896 if (isProxy() && !isSchemaForProxy(schema)) { 897 return getTargetDocument().docState; 898 } else { 899 return docState; 900 } 901 } 902 903 /** 904 * Gets the target state if this is a proxy, or the regular state otherwise. 905 */ 906 protected DBSDocumentState getStateOrTarget() { 907 if (isProxy()) { 908 return getTargetDocument().docState; 909 } else { 910 return docState; 911 } 912 } 913 914 protected boolean isSchemaForProxy(String schema) { 915 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 916 return schemaManager.isProxySchema(schema, getType().getName()); 917 } 918 919 protected String getSchema(String xpath) { 920 switch (xpath) { 921 case KEY_MAJOR_VERSION: 922 case KEY_MINOR_VERSION: 923 case "major_version": 924 case "minor_version": 925 return "uid"; 926 case KEY_FULLTEXT_JOBID: 927 case KEY_LIFECYCLE_POLICY: 928 case KEY_LIFECYCLE_STATE: 929 return "__ecm__"; 930 } 931 if (xpath.startsWith(KEY_FULLTEXT_SIMPLE) || xpath.startsWith(KEY_FULLTEXT_BINARY)) { 932 return "__ecm__"; 933 } 934 String[] segments = xpath.split("/"); 935 String segment = segments[0]; 936 Field field = type.getField(segment); 937 if (field == null) { 938 // check facets 939 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 940 for (String facet : getFacets()) { 941 CompositeType facetType = schemaManager.getFacet(facet); 942 field = facetType.getField(segment); 943 if (field != null) { 944 break; 945 } 946 } 947 } 948 if (field == null && getProxySchemas() != null) { 949 // check proxy schemas 950 for (Schema schema : getProxySchemas()) { 951 field = schema.getField(segment); 952 if (field != null) { 953 break; 954 } 955 } 956 } 957 if (field == null) { 958 throw new PropertyNotFoundException(xpath); 959 } 960 return field.getDeclaringType().getName(); 961 } 962 963 @Override 964 public void readDocumentPart(DocumentPart dp) throws PropertyException { 965 DBSDocumentState docState = getStateOrTarget(dp.getType()); 966 readComplexProperty(docState.getState(), (ComplexProperty) dp); 967 } 968 969 @Override 970 protected String internalName(String name) { 971 switch (name) { 972 case PROP_MAJOR_VERSION: 973 return KEY_MAJOR_VERSION; 974 case PROP_MINOR_VERSION: 975 return KEY_MINOR_VERSION; 976 } 977 return name; 978 } 979 980 @Override 981 public Map<String, Serializable> readPrefetch(ComplexType complexType, Set<String> xpaths) 982 throws PropertyException { 983 DBSDocumentState docState = getStateOrTarget(complexType); 984 return readPrefetch(docState.getState(), complexType, xpaths); 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<String>(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<Object>(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<Object>(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}