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