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() || isVersion()) { 541 return null; 542 } else { 543 if (isCheckedOut()) { 544 return null; 545 } else { 546 String id = (String) docState.get(KEY_BASE_VERSION_ID); 547 if (id == null) { 548 // shouldn't happen 549 return null; 550 } 551 return session.getDocument(id); 552 } 553 } 554 } 555 556 @Override 557 public boolean isCheckedOut() { 558 if (isProxy()) { 559 return getTargetDocument().isCheckedOut(); 560 } else if (isVersion()) { 561 return false; 562 } else { 563 return !TRUE.equals(docState.get(KEY_IS_CHECKED_IN)); 564 } 565 } 566 567 @Override 568 public String getVersionSeriesId() { 569 if (isProxy()) { 570 return (String) docState.get(KEY_PROXY_VERSION_SERIES_ID); 571 } else if (isVersion()) { 572 return (String) docState.get(KEY_VERSION_SERIES_ID); 573 } else { 574 return getUUID(); 575 } 576 } 577 578 @Override 579 public Calendar getVersionCreationDate() { 580 DBSDocumentState docState = getStateOrTarget(); 581 return (Calendar) docState.get(KEY_VERSION_CREATED); 582 } 583 584 @Override 585 public String getVersionLabel() { 586 DBSDocumentState docState = getStateOrTarget(); 587 return (String) docState.get(KEY_VERSION_LABEL); 588 } 589 590 @Override 591 public String getCheckinComment() { 592 DBSDocumentState docState = getStateOrTarget(); 593 return (String) docState.get(KEY_VERSION_DESCRIPTION); 594 } 595 596 @Override 597 public boolean isLatestVersion() { 598 return isEqualOnVersion(TRUE, KEY_IS_LATEST_VERSION); 599 } 600 601 @Override 602 public boolean isMajorVersion() { 603 return isEqualOnVersion(ZERO, KEY_MINOR_VERSION); 604 } 605 606 @Override 607 public boolean isLatestMajorVersion() { 608 return isEqualOnVersion(TRUE, KEY_IS_LATEST_MAJOR_VERSION); 609 } 610 611 protected boolean isEqualOnVersion(Object ob, String key) { 612 if (isProxy()) { 613 // TODO avoid getting the target just to check if it's a version 614 // use another specific property instead 615 if (getTargetDocument().isVersion()) { 616 return ob.equals(docState.get(key)); 617 } else { 618 // if live version, return false 619 return false; 620 } 621 } else if (isVersion()) { 622 return ob.equals(docState.get(key)); 623 } else { 624 return false; 625 } 626 } 627 628 @Override 629 public boolean isVersionSeriesCheckedOut() { 630 if (isProxy() || isVersion()) { 631 Document workingCopy = getWorkingCopy(); 632 return workingCopy == null ? false : workingCopy.isCheckedOut(); 633 } else { 634 return isCheckedOut(); 635 } 636 } 637 638 @Override 639 public Document getWorkingCopy() { 640 if (isProxy() || isVersion()) { 641 String versionSeriesId = getVersionSeriesId(); 642 return versionSeriesId == null ? null : session.getDocument(versionSeriesId); 643 } else { 644 return this; 645 } 646 } 647 648 @Override 649 public boolean isFolder() { 650 return type == null // null document 651 || type.isFolder(); 652 } 653 654 @Override 655 public void setReadOnly(boolean readonly) { 656 this.readonly = readonly; 657 } 658 659 @Override 660 public boolean isReadOnly() { 661 return readonly; 662 } 663 664 @Override 665 public void remove() { 666 session.remove(id); 667 } 668 669 @Override 670 public String getLifeCycleState() { 671 DBSDocumentState docState = getStateOrTarget(); 672 return (String) docState.get(KEY_LIFECYCLE_STATE); 673 } 674 675 @Override 676 public void setCurrentLifeCycleState(String lifeCycleState) throws LifeCycleException { 677 DBSDocumentState docState = getStateOrTarget(); 678 docState.put(KEY_LIFECYCLE_STATE, lifeCycleState); 679 BlobManager blobManager = Framework.getService(BlobManager.class); 680 blobManager.notifyChanges(this, Collections.singleton(KEY_LIFECYCLE_STATE)); 681 } 682 683 @Override 684 public String getLifeCyclePolicy() { 685 DBSDocumentState docState = getStateOrTarget(); 686 return (String) docState.get(KEY_LIFECYCLE_POLICY); 687 } 688 689 @Override 690 public void setLifeCyclePolicy(String policy) throws LifeCycleException { 691 DBSDocumentState docState = getStateOrTarget(); 692 docState.put(KEY_LIFECYCLE_POLICY, policy); 693 BlobManager blobManager = Framework.getService(BlobManager.class); 694 blobManager.notifyChanges(this, Collections.singleton(KEY_LIFECYCLE_POLICY)); 695 } 696 697 // TODO generic 698 @Override 699 public void followTransition(String transition) throws LifeCycleException { 700 LifeCycleService service = NXCore.getLifeCycleService(); 701 if (service == null) { 702 throw new LifeCycleException("LifeCycleService not available"); 703 } 704 service.followTransition(this, transition); 705 } 706 707 // TODO generic 708 @Override 709 public Collection<String> getAllowedStateTransitions() throws LifeCycleException { 710 LifeCycleService service = NXCore.getLifeCycleService(); 711 if (service == null) { 712 throw new LifeCycleException("LifeCycleService not available"); 713 } 714 LifeCycle lifeCycle = service.getLifeCycleFor(this); 715 if (lifeCycle == null) { 716 return Collections.emptyList(); 717 } 718 return lifeCycle.getAllowedStateTransitionsFrom(getLifeCycleState()); 719 } 720 721 @Override 722 public void setSystemProp(String name, Serializable value) { 723 String propertyName; 724 if (name.startsWith(SYSPROP_FULLTEXT_SIMPLE)) { 725 propertyName = name.replace(SYSPROP_FULLTEXT_SIMPLE, KEY_FULLTEXT_SIMPLE); 726 } else if (name.startsWith(SYSPROP_FULLTEXT_BINARY)) { 727 propertyName = name.replace(SYSPROP_FULLTEXT_BINARY, KEY_FULLTEXT_BINARY); 728 } else { 729 propertyName = systemPropNameMap.get(name); 730 } 731 if (propertyName == null) { 732 throw new PropertyNotFoundException(name, "Unknown system property"); 733 } 734 setPropertyValue(propertyName, value); 735 } 736 737 @SuppressWarnings("unchecked") 738 @Override 739 public <T extends Serializable> T getSystemProp(String name, Class<T> type) { 740 String propertyName = systemPropNameMap.get(name); 741 if (propertyName == null) { 742 throw new PropertyNotFoundException(name, "Unknown system property: "); 743 } 744 Serializable value = getPropertyValue(propertyName); 745 if (value == null) { 746 if (type == Boolean.class) { 747 value = Boolean.FALSE; 748 } else if (type == Long.class) { 749 value = Long.valueOf(0); 750 } 751 } 752 return (T) value; 753 } 754 755 protected DBSDocumentState getStateOrTarget(Type type) throws PropertyException { 756 return getStateOrTargetForSchema(type.getName()); 757 } 758 759 protected DBSDocumentState getStateOrTarget(String xpath) { 760 return getStateOrTargetForSchema(getSchema(xpath)); 761 } 762 763 /** 764 * Checks if the given schema should be resolved on the proxy or the target. 765 */ 766 protected DBSDocumentState getStateOrTargetForSchema(String schema) { 767 if (isProxy() && !isSchemaForProxy(schema)) { 768 return getTargetDocument().docState; 769 } else { 770 return docState; 771 } 772 } 773 774 /** 775 * Gets the target state if this is a proxy, or the regular state otherwise. 776 */ 777 protected DBSDocumentState getStateOrTarget() { 778 if (isProxy()) { 779 return getTargetDocument().docState; 780 } else { 781 return docState; 782 } 783 } 784 785 protected boolean isSchemaForProxy(String schema) { 786 SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class); 787 return schemaManager.isProxySchema(schema, getType().getName()); 788 } 789 790 protected String getSchema(String xpath) { 791 switch (xpath) { 792 case KEY_MAJOR_VERSION: 793 case KEY_MINOR_VERSION: 794 case "major_version": 795 case "minor_version": 796 return "uid"; 797 case KEY_FULLTEXT_JOBID: 798 case KEY_LIFECYCLE_POLICY: 799 case KEY_LIFECYCLE_STATE: 800 return "__ecm__"; 801 } 802 if (xpath.startsWith(KEY_FULLTEXT_SIMPLE) || xpath.startsWith(KEY_FULLTEXT_BINARY)) { 803 return "__ecm__"; 804 } 805 String[] segments = xpath.split("/"); 806 String segment = segments[0]; 807 Field field = type.getField(segment); 808 if (field == null) { 809 // check facets 810 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 811 for (String facet : getFacets()) { 812 CompositeType facetType = schemaManager.getFacet(facet); 813 field = facetType.getField(segment); 814 if (field != null) { 815 break; 816 } 817 } 818 } 819 if (field == null && getProxySchemas() != null) { 820 // check proxy schemas 821 for (Schema schema : getProxySchemas()) { 822 field = schema.getField(segment); 823 if (field != null) { 824 break; 825 } 826 } 827 } 828 if (field == null) { 829 throw new PropertyNotFoundException(xpath); 830 } 831 return field.getDeclaringType().getName(); 832 } 833 834 @Override 835 public void readDocumentPart(DocumentPart dp) throws PropertyException { 836 DBSDocumentState docState = getStateOrTarget(dp.getType()); 837 readComplexProperty(docState.getState(), (ComplexProperty) dp); 838 } 839 840 @Override 841 protected String internalName(String name) { 842 switch (name) { 843 case "major_version": 844 return KEY_MAJOR_VERSION; 845 case "minor_version": 846 return KEY_MINOR_VERSION; 847 } 848 return name; 849 } 850 851 @Override 852 public Map<String, Serializable> readPrefetch(ComplexType complexType, Set<String> xpaths) 853 throws PropertyException { 854 DBSDocumentState docState = getStateOrTarget(complexType); 855 return readPrefetch(docState.getState(), complexType, xpaths); 856 } 857 858 @Override 859 public boolean writeDocumentPart(DocumentPart dp, WriteContext writeContext) throws PropertyException { 860 DBSDocumentState docState = getStateOrTarget(dp.getType()); 861 // markDirty has to be called *before* we change the state 862 docState.markDirty(); 863 boolean changed = writeComplexProperty(docState.getState(), (ComplexProperty) dp, writeContext); 864 clearDirtyFlags(dp); 865 return changed; 866 } 867 868 @Override 869 public Set<String> getAllFacets() { 870 Set<String> facets = new HashSet<String>(getType().getFacets()); 871 facets.addAll(Arrays.asList(getFacets())); 872 return facets; 873 } 874 875 @Override 876 public String[] getFacets() { 877 DBSDocumentState docState = getStateOrTarget(); 878 Object[] mixins = (Object[]) docState.get(KEY_MIXIN_TYPES); 879 if (mixins == null) { 880 return EMPTY_STRING_ARRAY; 881 } else { 882 String[] res = new String[mixins.length]; 883 System.arraycopy(mixins, 0, res, 0, mixins.length); 884 return res; 885 } 886 } 887 888 @Override 889 public boolean hasFacet(String facet) { 890 return getAllFacets().contains(facet); 891 } 892 893 @Override 894 public boolean addFacet(String facet) { 895 if (getType().getFacets().contains(facet)) { 896 return false; // already present in type 897 } 898 DBSDocumentState docState = getStateOrTarget(); 899 Object[] mixins = (Object[]) docState.get(KEY_MIXIN_TYPES); 900 if (mixins == null) { 901 mixins = new Object[] { facet }; 902 } else { 903 List<Object> list = Arrays.asList(mixins); 904 if (list.contains(facet)) { 905 return false; // already present in doc 906 } 907 list = new ArrayList<Object>(list); 908 list.add(facet); 909 mixins = list.toArray(new Object[list.size()]); 910 } 911 docState.put(KEY_MIXIN_TYPES, mixins); 912 return true; 913 } 914 915 @Override 916 public boolean removeFacet(String facet) { 917 DBSDocumentState docState = getStateOrTarget(); 918 Object[] mixins = (Object[]) docState.get(KEY_MIXIN_TYPES); 919 if (mixins == null) { 920 return false; 921 } 922 List<Object> list = new ArrayList<Object>(Arrays.asList(mixins)); 923 if (!list.remove(facet)) { 924 return false; // not present in doc 925 } 926 mixins = list.toArray(new Object[list.size()]); 927 if (mixins.length == 0) { 928 mixins = null; 929 } 930 docState.put(KEY_MIXIN_TYPES, mixins); 931 // remove the fields belonging to the facet 932 // except for schemas still present due to the primary type or another facet 933 SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class); 934 CompositeType ft = schemaManager.getFacet(facet); 935 Set<String> otherSchemas = getSchemas(getType(), list); 936 for (Schema schema : ft.getSchemas()) { 937 if (otherSchemas.contains(schema.getName())) { 938 continue; 939 } 940 for (Field field : schema.getFields()) { 941 String name = field.getName().getPrefixedName(); 942 if (docState.containsKey(name)) { 943 docState.put(name, null); 944 } 945 } 946 } 947 return true; 948 } 949 950 protected static Set<String> getSchemas(DocumentType type, List<Object> facets) { 951 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 952 Set<String> schemas = new HashSet<>(Arrays.asList(type.getSchemaNames())); 953 for (Object facet : facets) { 954 CompositeType ft = schemaManager.getFacet((String) facet); 955 if (ft != null) { 956 schemas.addAll(Arrays.asList(ft.getSchemaNames())); 957 } 958 } 959 return schemas; 960 } 961 962 @Override 963 public DBSDocument getTargetDocument() { 964 if (isProxy()) { 965 String targetId = (String) docState.get(KEY_PROXY_TARGET_ID); 966 return session.getDocument(targetId); 967 } else { 968 return null; 969 } 970 } 971 972 @Override 973 public void setTargetDocument(Document target) { 974 if (isProxy()) { 975 if (isReadOnly()) { 976 throw new ReadOnlyPropertyException("Cannot write proxy: " + this); 977 } 978 if (!target.getVersionSeriesId().equals(getVersionSeriesId())) { 979 throw new ReadOnlyPropertyException("Cannot set proxy target to different version series"); 980 } 981 session.setProxyTarget(this, target); 982 } else { 983 throw new NuxeoException("Cannot set proxy target on non-proxy"); 984 } 985 } 986 987 @Override 988 protected Lock getDocumentLock() { 989 String owner = (String) docState.get(KEY_LOCK_OWNER); 990 if (owner == null) { 991 return null; 992 } 993 Calendar created = (Calendar) docState.get(KEY_LOCK_CREATED); 994 return new Lock(owner, created); 995 } 996 997 @Override 998 protected Lock setDocumentLock(Lock lock) { 999 String owner = (String) docState.get(KEY_LOCK_OWNER); 1000 if (owner != null) { 1001 // return old lock 1002 Calendar created = (Calendar) docState.get(KEY_LOCK_CREATED); 1003 return new Lock(owner, created); 1004 } 1005 docState.put(KEY_LOCK_OWNER, lock.getOwner()); 1006 docState.put(KEY_LOCK_CREATED, lock.getCreated()); 1007 return null; 1008 } 1009 1010 @Override 1011 protected Lock removeDocumentLock(String owner) { 1012 String oldOwner = (String) docState.get(KEY_LOCK_OWNER); 1013 if (oldOwner == null) { 1014 // no previous lock 1015 return null; 1016 } 1017 Calendar oldCreated = (Calendar) docState.get(KEY_LOCK_CREATED); 1018 if (!LockManager.canLockBeRemoved(oldOwner, owner)) { 1019 // existing mismatched lock, flag failure 1020 return new Lock(oldOwner, oldCreated, true); 1021 } 1022 // remove lock 1023 docState.put(KEY_LOCK_OWNER, null); 1024 docState.put(KEY_LOCK_CREATED, null); 1025 // return old lock 1026 return new Lock(oldOwner, oldCreated); 1027 } 1028 1029 @Override 1030 public String toString() { 1031 return getClass().getSimpleName() + '(' + getName() + ',' + getUUID() + ')'; 1032 } 1033 1034 @Override 1035 public boolean equals(Object other) { 1036 if (other == this) { 1037 return true; 1038 } 1039 if (other == null) { 1040 return false; 1041 } 1042 if (other.getClass() == getClass()) { 1043 return equals((DBSDocument) other); 1044 } 1045 return false; 1046 } 1047 1048 private boolean equals(DBSDocument other) { 1049 return id.equals(other.id); 1050 } 1051 1052 @Override 1053 public int hashCode() { 1054 return id.hashCode(); 1055 } 1056 1057}