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