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