001/* 002 * (C) Copyright 2006-2016 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 * Bogdan Stefanescu 018 * Florent Guillaume 019 */ 020package org.nuxeo.ecm.core.api.impl; 021 022import static org.apache.commons.lang.ObjectUtils.NULL; 023import static org.nuxeo.ecm.core.schema.types.ComplexTypeImpl.canonicalXPath; 024 025import java.io.IOException; 026import java.io.ObjectOutputStream; 027import java.io.ObjectStreamException; 028import java.io.Serializable; 029import java.lang.reflect.Array; 030import java.text.DateFormat; 031import java.util.ArrayList; 032import java.util.Arrays; 033import java.util.Calendar; 034import java.util.Collection; 035import java.util.Collections; 036import java.util.Date; 037import java.util.HashMap; 038import java.util.HashSet; 039import java.util.List; 040import java.util.Map; 041import java.util.Set; 042 043import javax.transaction.Transaction; 044 045import org.apache.commons.logging.Log; 046import org.apache.commons.logging.LogFactory; 047import org.nuxeo.common.collections.PrimitiveArrays; 048import org.nuxeo.common.collections.ScopeType; 049import org.nuxeo.common.collections.ScopedMap; 050import org.nuxeo.common.utils.Path; 051import org.nuxeo.ecm.core.api.Blob; 052import org.nuxeo.ecm.core.api.CoreSession; 053import org.nuxeo.ecm.core.api.CoreSessionService; 054import org.nuxeo.ecm.core.api.DataModel; 055import org.nuxeo.ecm.core.api.DocumentModel; 056import org.nuxeo.ecm.core.api.DocumentRef; 057import org.nuxeo.ecm.core.api.InstanceRef; 058import org.nuxeo.ecm.core.api.Lock; 059import org.nuxeo.ecm.core.api.NuxeoException; 060import org.nuxeo.ecm.core.api.PathRef; 061import org.nuxeo.ecm.core.api.PropertyException; 062import org.nuxeo.ecm.core.api.VersioningOption; 063import org.nuxeo.ecm.core.api.adapter.DocumentAdapterDescriptor; 064import org.nuxeo.ecm.core.api.adapter.DocumentAdapterService; 065import org.nuxeo.ecm.core.api.model.DocumentPart; 066import org.nuxeo.ecm.core.api.model.Property; 067import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; 068import org.nuxeo.ecm.core.api.model.PropertyVisitor; 069import org.nuxeo.ecm.core.api.model.impl.DocumentPartImpl; 070import org.nuxeo.ecm.core.api.model.resolver.DocumentPropertyObjectResolverImpl; 071import org.nuxeo.ecm.core.api.model.resolver.PropertyObjectResolver; 072import org.nuxeo.ecm.core.api.security.ACP; 073import org.nuxeo.ecm.core.schema.DocumentType; 074import org.nuxeo.ecm.core.schema.FacetNames; 075import org.nuxeo.ecm.core.schema.Prefetch; 076import org.nuxeo.ecm.core.schema.SchemaManager; 077import org.nuxeo.ecm.core.schema.TypeConstants; 078import org.nuxeo.ecm.core.schema.TypeProvider; 079import org.nuxeo.ecm.core.schema.types.ComplexType; 080import org.nuxeo.ecm.core.schema.types.CompositeType; 081import org.nuxeo.ecm.core.schema.types.Field; 082import org.nuxeo.ecm.core.schema.types.JavaTypes; 083import org.nuxeo.ecm.core.schema.types.ListType; 084import org.nuxeo.ecm.core.schema.types.Schema; 085import org.nuxeo.ecm.core.schema.types.Type; 086import org.nuxeo.runtime.api.Framework; 087import org.nuxeo.runtime.transaction.TransactionHelper; 088 089/** 090 * Standard implementation of a {@link DocumentModel}. 091 */ 092public class DocumentModelImpl implements DocumentModel, Cloneable { 093 094 private static final long serialVersionUID = 1L; 095 096 public static final long F_VERSION = 16L; 097 098 public static final long F_PROXY = 32L; 099 100 public static final long F_IMMUTABLE = 256L; 101 102 private static final Log log = LogFactory.getLog(DocumentModelImpl.class); 103 104 protected String sid; 105 106 protected DocumentRef ref; 107 108 protected DocumentType type; 109 110 // for tests, keep the type name even if no actual type is registered 111 protected String typeName; 112 113 /** Schemas including those from instance facets. */ 114 protected Set<String> schemas; 115 116 /** Schemas including those from instance facets when the doc was read */ 117 protected Set<String> schemasOrig; 118 119 /** Facets including those on instance. */ 120 protected Set<String> facets; 121 122 /** Instance facets. */ 123 public Set<String> instanceFacets; 124 125 /** Instance facets when the document was read. */ 126 public Set<String> instanceFacetsOrig; 127 128 protected String id; 129 130 protected Path path; 131 132 protected Long pos; 133 134 protected Map<String, DataModel> dataModels; 135 136 protected DocumentRef parentRef; 137 138 protected static final Lock LOCK_UNKNOWN = new Lock(null, null); 139 140 protected Lock lock = LOCK_UNKNOWN; 141 142 /** state is lifecycle, version stuff. */ 143 protected boolean isStateLoaded; 144 145 // loaded if isStateLoaded 146 protected String currentLifeCycleState; 147 148 // loaded if isStateLoaded 149 protected String lifeCyclePolicy; 150 151 // loaded if isStateLoaded 152 protected boolean isCheckedOut = true; 153 154 // loaded if isStateLoaded 155 protected String versionSeriesId; 156 157 // loaded if isStateLoaded 158 protected boolean isLatestVersion; 159 160 // loaded if isStateLoaded 161 protected boolean isMajorVersion; 162 163 // loaded if isStateLoaded 164 protected boolean isLatestMajorVersion; 165 166 // loaded if isStateLoaded 167 protected boolean isVersionSeriesCheckedOut; 168 169 // loaded if isStateLoaded 170 protected String checkinComment; 171 172 // acp is not send between client/server 173 // it will be loaded lazy first time it is accessed 174 // and discarded when object is serialized 175 protected transient ACP acp; 176 177 // whether the acp was cached 178 protected transient boolean isACPLoaded = false; 179 180 // the adapters registered for this document - only valid on client 181 protected transient HashMap<Class<?>, Object> adapters; 182 183 /** 184 * Flags: bitwise combination of {@link #F_VERSION}, {@link #F_PROXY}, {@link #F_IMMUTABLE}. 185 */ 186 private long flags = 0L; 187 188 protected String repositoryName; 189 190 protected String sourceId; 191 192 protected ScopedMap contextData = new ScopedMap(); 193 194 // public for unit tests 195 public Prefetch prefetch; 196 197 private String detachedVersionLabel; 198 199 // always refetched when a session is accessible, but also available without one 200 protected String changeToken; 201 202 protected DocumentModelImpl() { 203 } 204 205 /** 206 * Constructor to use a document model client side without referencing a document. 207 * <p> 208 * It must at least contain the type. 209 */ 210 public DocumentModelImpl(String typeName) { 211 SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class); 212 if (schemaManager == null) { 213 throw new NullPointerException("No registered SchemaManager"); 214 } 215 type = schemaManager.getDocumentType(typeName); 216 this.typeName = typeName; 217 dataModels = new HashMap<>(); 218 instanceFacets = new HashSet<>(); 219 instanceFacetsOrig = new HashSet<>(); 220 facets = new HashSet<>(); 221 schemas = new HashSet<>(); 222 schemasOrig = new HashSet<>(); 223 } 224 225 /** 226 * Constructor to be used by clients. 227 * <p> 228 * A client constructed data model must contain at least the path and the type. 229 */ 230 public DocumentModelImpl(String parentPath, String name, String type) { 231 this(type); 232 String fullPath = parentPath == null ? name : parentPath + (parentPath.endsWith("/") ? "" : "/") + name; 233 path = new Path(fullPath); 234 ref = new PathRef(fullPath); 235 instanceFacets = new HashSet<>(); 236 instanceFacetsOrig = new HashSet<>(); 237 facets = new HashSet<>(); 238 schemas = new HashSet<>(); 239 if (getDocumentType() != null) { 240 facets.addAll(getDocumentType().getFacets()); 241 } 242 schemas = computeSchemas(getDocumentType(), instanceFacets, false); 243 schemasOrig = new HashSet<>(schemas); 244 } 245 246 /** 247 * Constructor. 248 * <p> 249 * The lock parameter is unused since 5.4.2. 250 * 251 * @param facets the per-instance facets 252 */ 253 // TODO check if we use it 254 public DocumentModelImpl(String sid, String type, String id, Path path, Lock lock, DocumentRef docRef, 255 DocumentRef parentRef, String[] schemas, Set<String> facets, String sourceId, String repositoryName) { 256 this(sid, type, id, path, docRef, parentRef, schemas, facets, sourceId, repositoryName, false); 257 } 258 259 public DocumentModelImpl(String sid, String type, String id, Path path, DocumentRef docRef, DocumentRef parentRef, 260 String[] schemas, Set<String> facets, String sourceId, String repositoryName, boolean isProxy) { 261 this(type); 262 this.sid = sid; 263 this.id = id; 264 this.path = path; 265 ref = docRef; 266 this.parentRef = parentRef; 267 instanceFacets = facets == null ? new HashSet<>() : new HashSet<>(facets); 268 instanceFacetsOrig = new HashSet<>(instanceFacets); 269 this.facets = new HashSet<>(instanceFacets); 270 if (getDocumentType() != null) { 271 this.facets.addAll(getDocumentType().getFacets()); 272 } 273 if (schemas == null) { 274 this.schemas = computeSchemas(getDocumentType(), instanceFacets, isProxy); 275 } else { 276 this.schemas = new HashSet<>(Arrays.asList(schemas)); 277 } 278 schemasOrig = new HashSet<>(this.schemas); 279 this.repositoryName = repositoryName; 280 this.sourceId = sourceId; 281 setIsProxy(isProxy); 282 } 283 284 /** 285 * Recomputes effective schemas from a type + instance facets. 286 */ 287 public static Set<String> computeSchemas(DocumentType type, Collection<String> instanceFacets, boolean isProxy) { 288 Set<String> schemas = new HashSet<>(); 289 if (type != null) { 290 schemas.addAll(Arrays.asList(type.getSchemaNames())); 291 } 292 TypeProvider typeProvider = Framework.getLocalService(SchemaManager.class); 293 for (String facet : instanceFacets) { 294 CompositeType facetType = typeProvider.getFacet(facet); 295 if (facetType != null) { // ignore pseudo-facets like Immutable 296 schemas.addAll(Arrays.asList(facetType.getSchemaNames())); 297 } 298 } 299 if (isProxy) { 300 for (Schema schema : typeProvider.getProxySchemas(type.getName())) { 301 schemas.add(schema.getName()); 302 } 303 } 304 return schemas; 305 } 306 307 public DocumentModelImpl(DocumentModel parent, String name, String type) { 308 this(parent.getPathAsString(), name, type); 309 } 310 311 @Override 312 public DocumentType getDocumentType() { 313 return type; 314 } 315 316 /** 317 * Gets the title from the dublincore schema. 318 * 319 * @see DocumentModel#getTitle() 320 */ 321 @Override 322 public String getTitle() { 323 String title = (String) getProperty("dublincore", "title"); 324 if (title != null) { 325 return title; 326 } 327 title = getName(); 328 if (title != null) { 329 return title; 330 } 331 return id; 332 } 333 334 @Override 335 public String getSessionId() { 336 return sid; 337 } 338 339 @Override 340 public DocumentRef getRef() { 341 return ref; 342 } 343 344 @Override 345 public DocumentRef getParentRef() { 346 if (parentRef == null && path != null) { 347 if (path.isAbsolute()) { 348 Path parentPath = path.removeLastSegments(1); 349 parentRef = new PathRef(parentPath.toString()); 350 } 351 // else keep parentRef null 352 } 353 return parentRef; 354 } 355 356 @Override 357 public CoreSession getCoreSession() { 358 if (sid == null) { 359 return null; 360 } 361 return Framework.getService(CoreSessionService.class).getCoreSession(sid); 362 } 363 364 protected boolean hasSession() { 365 return getCoreSession() != null; 366 } 367 368 /** 369 * Gets the CoreSession, or fails if it's not available. 370 * 371 * @since 9.1 372 */ 373 protected CoreSession getSession() { 374 CoreSession session = getCoreSession(); 375 if (session != null) { 376 return session; 377 } 378 throw new NuxeoException("The DocumentModel is not associated to an open CoreSession: " + this); 379 } 380 381 382 @Override 383 public void detach(boolean loadAll) { 384 if (sid == null) { 385 return; 386 } 387 try { 388 if (loadAll) { 389 for (String schema : schemas) { 390 if (!isSchemaLoaded(schema)) { 391 loadDataModel(schema); 392 } 393 } 394 // fetch ACP too if possible 395 if (ref != null) { 396 getACP(); 397 } 398 detachedVersionLabel = getVersionLabel(); 399 // load some system info 400 isCheckedOut(); 401 getCurrentLifeCycleState(); 402 getLockInfo(); 403 getChangeToken(); 404 } 405 } finally { 406 sid = null; 407 } 408 } 409 410 @Override 411 public void attach(String sid) { 412 if (this.sid != null) { 413 throw new NuxeoException("Cannot attach a document that is already attached"); 414 } 415 this.sid = sid; 416 } 417 418 /** 419 * Lazily loads the given data model. 420 */ 421 protected DataModel loadDataModel(String schema) { 422 423 if (log.isTraceEnabled()) { 424 log.trace("lazy loading of schema " + schema + " for doc " + toString()); 425 } 426 427 if (!schemas.contains(schema)) { 428 return null; 429 } 430 if (!schemasOrig.contains(schema)) { 431 // not present yet in persistent document 432 DataModel dataModel = new DataModelImpl(schema); 433 dataModels.put(schema, dataModel); 434 return dataModel; 435 } 436 if (sid == null) { 437 // supports detached docs 438 DataModel dataModel = new DataModelImpl(schema); 439 dataModels.put(schema, dataModel); 440 return dataModel; 441 } 442 if (ref == null) { 443 return null; 444 } 445 // load from session 446 TypeProvider typeProvider = Framework.getLocalService(SchemaManager.class); 447 final Schema schemaType = typeProvider.getSchema(schema); 448 DataModel dataModel = getSession().getDataModel(ref, schemaType); 449 dataModels.put(schema, dataModel); 450 return dataModel; 451 } 452 453 @Override 454 @Deprecated 455 public DataModel getDataModel(String schema) { 456 DataModel dataModel = dataModels.get(schema); 457 if (dataModel == null) { 458 dataModel = loadDataModel(schema); 459 } 460 return dataModel; 461 } 462 463 @Override 464 @Deprecated 465 public Collection<DataModel> getDataModelsCollection() { 466 return dataModels.values(); 467 } 468 469 public void addDataModel(DataModel dataModel) { 470 dataModels.put(dataModel.getSchema(), dataModel); 471 } 472 473 @Override 474 public String[] getSchemas() { 475 return schemas.toArray(new String[schemas.size()]); 476 } 477 478 @Override 479 @Deprecated 480 public String[] getDeclaredSchemas() { 481 return getSchemas(); 482 } 483 484 @Override 485 public boolean hasSchema(String schema) { 486 return schemas.contains(schema); 487 } 488 489 @Override 490 public Set<String> getFacets() { 491 return Collections.unmodifiableSet(facets); 492 } 493 494 @Override 495 public boolean hasFacet(String facet) { 496 return facets.contains(facet); 497 } 498 499 @Override 500 @Deprecated 501 public Set<String> getDeclaredFacets() { 502 return getFacets(); 503 } 504 505 @Override 506 public boolean addFacet(String facet) { 507 if (facet == null) { 508 throw new IllegalArgumentException("Null facet"); 509 } 510 if (facets.contains(facet)) { 511 return false; 512 } 513 TypeProvider typeProvider = Framework.getLocalService(SchemaManager.class); 514 CompositeType facetType = typeProvider.getFacet(facet); 515 if (facetType == null) { 516 throw new IllegalArgumentException("No such facet: " + facet); 517 } 518 // add it 519 facets.add(facet); 520 instanceFacets.add(facet); 521 schemas.addAll(Arrays.asList(facetType.getSchemaNames())); 522 return true; 523 } 524 525 @Override 526 public boolean removeFacet(String facet) { 527 if (facet == null) { 528 throw new IllegalArgumentException("Null facet"); 529 } 530 if (!instanceFacets.contains(facet)) { 531 return false; 532 } 533 // remove it 534 facets.remove(facet); 535 instanceFacets.remove(facet); 536 537 // find the schemas that were dropped 538 Set<String> droppedSchemas = new HashSet<>(schemas); 539 schemas = computeSchemas(getDocumentType(), instanceFacets, isProxy()); 540 droppedSchemas.removeAll(schemas); 541 542 // clear these datamodels 543 for (String s : droppedSchemas) { 544 dataModels.remove(s); 545 } 546 547 return true; 548 } 549 550 protected static Set<String> inferFacets(Set<String> facets, DocumentType documentType) { 551 if (facets == null) { 552 facets = new HashSet<>(); 553 if (documentType != null) { 554 facets.addAll(documentType.getFacets()); 555 } 556 } 557 return facets; 558 } 559 560 @Override 561 public String getId() { 562 return id; 563 } 564 565 @Override 566 public String getName() { 567 if (path != null) { 568 return path.lastSegment(); 569 } 570 return null; 571 } 572 573 @Override 574 public Long getPos() { 575 return pos; 576 } 577 578 /** 579 * Sets the document's position in its containing folder (if ordered). Used internally during construction. 580 * 581 * @param pos the position 582 * @since 6.0 583 */ 584 public void setPosInternal(Long pos) { 585 this.pos = pos; 586 } 587 588 @Override 589 public String getPathAsString() { 590 if (path != null) { 591 return path.toString(); 592 } 593 return null; 594 } 595 596 @Override 597 public Map<String, Object> getProperties(String schemaName) { 598 DataModel dm = getDataModel(schemaName); 599 return dm == null ? null : dm.getMap(); 600 } 601 602 @Override 603 public Object getProperty(String schemaName, String name) { 604 // look in prefetch 605 if (prefetch != null) { 606 Serializable value = prefetch.get(schemaName, name); 607 if (value != NULL) { 608 return value; 609 } 610 } 611 // look in datamodels 612 DataModel dm = dataModels.get(schemaName); 613 if (dm == null) { 614 dm = getDataModel(schemaName); 615 } 616 return dm == null ? null : dm.getData(name); 617 } 618 619 @Override 620 public Property getPropertyObject(String schema, String name) { 621 DocumentPart part = getPart(schema); 622 return part == null ? null : part.get(name); 623 } 624 625 @Override 626 public void setPathInfo(String parentPath, String name) { 627 path = new Path(parentPath == null ? name : parentPath + '/' + name); 628 ref = new PathRef(parentPath, name); 629 } 630 631 @Override 632 public boolean isLocked() { 633 return getLockInfo() != null; 634 } 635 636 @Override 637 public Lock setLock() { 638 lock = getSession().setLock(ref); 639 return lock; 640 } 641 642 @Override 643 public Lock getLockInfo() { 644 if (lock != LOCK_UNKNOWN) { 645 return lock; 646 } 647 // no lock if not tied to a session 648 if (!hasSession()) { 649 return null; 650 } 651 lock = getSession().getLockInfo(ref); 652 return lock; 653 } 654 655 @Override 656 public Lock removeLock() { 657 Lock oldLock = getSession().removeLock(ref); 658 lock = null; 659 return oldLock; 660 } 661 662 @Override 663 public boolean isCheckedOut() { 664 if (!isStateLoaded) { 665 if (!hasSession()) { 666 return true; 667 } 668 refresh(REFRESH_STATE, null); 669 } 670 return isCheckedOut; 671 } 672 673 @Override 674 public void checkOut() { 675 getSession().checkOut(ref); 676 isStateLoaded = false; 677 // new version number, refresh content 678 refresh(REFRESH_CONTENT_IF_LOADED, null); 679 } 680 681 @Override 682 public DocumentRef checkIn(VersioningOption option, String description) { 683 DocumentRef versionRef = getSession().checkIn(ref, option, description); 684 isStateLoaded = false; 685 // new version number, refresh content 686 refresh(REFRESH_CONTENT_IF_LOADED, null); 687 return versionRef; 688 } 689 690 @Override 691 public String getVersionLabel() { 692 if (detachedVersionLabel != null) { 693 return detachedVersionLabel; 694 } 695 if (!hasSession()) { 696 return null; 697 } 698 return getSession().getVersionLabel(this); 699 } 700 701 @Override 702 public String getVersionSeriesId() { 703 if (!isStateLoaded) { 704 refresh(REFRESH_STATE, null); 705 } 706 return versionSeriesId; 707 } 708 709 @Override 710 public boolean isLatestVersion() { 711 if (!isStateLoaded) { 712 refresh(REFRESH_STATE, null); 713 } 714 return isLatestVersion; 715 } 716 717 @Override 718 public boolean isMajorVersion() { 719 if (!isStateLoaded) { 720 refresh(REFRESH_STATE, null); 721 } 722 return isMajorVersion; 723 } 724 725 @Override 726 public boolean isLatestMajorVersion() { 727 if (!isStateLoaded) { 728 refresh(REFRESH_STATE, null); 729 } 730 return isLatestMajorVersion; 731 } 732 733 @Override 734 public boolean isVersionSeriesCheckedOut() { 735 if (!isStateLoaded) { 736 refresh(REFRESH_STATE, null); 737 } 738 return isVersionSeriesCheckedOut; 739 } 740 741 @Override 742 public String getCheckinComment() { 743 if (!isStateLoaded) { 744 refresh(REFRESH_STATE, null); 745 } 746 return checkinComment; 747 } 748 749 @Override 750 public ACP getACP() { 751 if (!isACPLoaded) { // lazy load 752 acp = getSession().getACP(ref); 753 isACPLoaded = true; 754 } 755 return acp; 756 } 757 758 @Override 759 public void setACP(final ACP acp, final boolean overwrite) { 760 getSession().setACP(ref, acp, overwrite); 761 isACPLoaded = false; 762 } 763 764 @Override 765 public String getType() { 766 return typeName; 767 } 768 769 @Override 770 public void setProperties(String schemaName, Map<String, Object> data) { 771 DataModel dm = getDataModel(schemaName); 772 if (dm != null) { 773 dm.setMap(data); 774 clearPrefetch(schemaName); 775 } 776 } 777 778 @Override 779 public void setProperty(String schemaName, String name, Object value) { 780 DataModel dm = getDataModel(schemaName); 781 if (dm == null) { 782 return; 783 } 784 dm.setData(name, value); 785 clearPrefetch(schemaName); 786 } 787 788 @Override 789 public Path getPath() { 790 return path; 791 } 792 793 @Override 794 public Map<String, DataModel> getDataModels() { 795 return dataModels; 796 } 797 798 @Override 799 public boolean isFolder() { 800 return hasFacet(FacetNames.FOLDERISH); 801 } 802 803 @Override 804 public boolean isVersionable() { 805 return hasFacet(FacetNames.VERSIONABLE); 806 } 807 808 @Override 809 public boolean isDownloadable() { 810 if (hasFacet(FacetNames.DOWNLOADABLE)) { 811 // TODO find a better way to check size that does not depend on the 812 // document schema 813 Long size = (Long) getProperty("common", "size"); 814 if (size != null) { 815 return size.longValue() != 0; 816 } 817 } 818 return false; 819 } 820 821 @Override 822 public void accept(PropertyVisitor visitor, Object arg) { 823 for (DocumentPart dp : getParts()) { 824 ((DocumentPartImpl) dp).visitChildren(visitor, arg); 825 } 826 } 827 828 @Override 829 @SuppressWarnings("unchecked") 830 public <T> T getAdapter(Class<T> itf) { 831 T facet = (T) getAdapters().get(itf); 832 if (facet == null) { 833 facet = findAdapter(itf); 834 if (facet != null) { 835 adapters.put(itf, facet); 836 } 837 } 838 return facet; 839 } 840 841 /** 842 * Lazy initialization for adapters because they don't survive the serialization. 843 */ 844 private Map<Class<?>, Object> getAdapters() { 845 if (adapters == null) { 846 adapters = new HashMap<>(); 847 } 848 849 return adapters; 850 } 851 852 @Override 853 public <T> T getAdapter(Class<T> itf, boolean refreshCache) { 854 T facet; 855 856 if (!refreshCache) { 857 facet = getAdapter(itf); 858 } else { 859 facet = findAdapter(itf); 860 } 861 862 if (facet != null) { 863 getAdapters().put(itf, facet); 864 } 865 return facet; 866 } 867 868 @SuppressWarnings("unchecked") 869 private <T> T findAdapter(Class<T> itf) { 870 DocumentAdapterService svc = Framework.getService(DocumentAdapterService.class); 871 if (svc != null) { 872 DocumentAdapterDescriptor dae = svc.getAdapterDescriptor(itf); 873 if (dae != null) { 874 String facet = dae.getFacet(); 875 if (facet == null) { 876 // if no facet is specified, accept the adapter 877 return (T) dae.getFactory().getAdapter(this, itf); 878 } else if (hasFacet(facet)) { 879 return (T) dae.getFactory().getAdapter(this, itf); 880 } else { 881 // TODO: throw an exception 882 log.error("Document model cannot be adapted to " + itf + " because it has no facet " + facet); 883 } 884 } 885 } else { 886 log.warn("DocumentAdapterService not available. Cannot get document model adaptor for " + itf); 887 } 888 return null; 889 } 890 891 @Override 892 public boolean followTransition(final String transition) { 893 boolean res = getSession().followTransition(ref, transition); 894 // Invalidate the prefetched value in this case. 895 if (res) { 896 currentLifeCycleState = null; 897 } 898 return res; 899 } 900 901 @Override 902 public Collection<String> getAllowedStateTransitions() { 903 return getSession().getAllowedStateTransitions(ref); 904 } 905 906 @Override 907 public String getCurrentLifeCycleState() { 908 if (currentLifeCycleState != null) { 909 return currentLifeCycleState; 910 } 911 if (!hasSession()) { 912 // document was just created => not life cycle yet 913 return null; 914 } 915 currentLifeCycleState = getSession().getCurrentLifeCycleState(ref); 916 return currentLifeCycleState; 917 } 918 919 @Override 920 public String getLifeCyclePolicy() { 921 if (lifeCyclePolicy != null) { 922 return lifeCyclePolicy; 923 } 924 // String lifeCyclePolicy = null; 925 lifeCyclePolicy = getSession().getLifeCyclePolicy(ref); 926 return lifeCyclePolicy; 927 } 928 929 @Override 930 public boolean isVersion() { 931 return (flags & F_VERSION) != 0; 932 } 933 934 @Override 935 public boolean isProxy() { 936 return (flags & F_PROXY) != 0; 937 } 938 939 @Override 940 public boolean isImmutable() { 941 return (flags & F_IMMUTABLE) != 0; 942 } 943 944 public void setIsVersion(boolean isVersion) { 945 if (isVersion) { 946 flags |= F_VERSION; 947 } else { 948 flags &= ~F_VERSION; 949 } 950 } 951 952 public void setIsProxy(boolean isProxy) { 953 if (isProxy) { 954 flags |= F_PROXY; 955 } else { 956 flags &= ~F_PROXY; 957 } 958 } 959 960 public void setIsImmutable(boolean isImmutable) { 961 if (isImmutable) { 962 flags |= F_IMMUTABLE; 963 } else { 964 flags &= ~F_IMMUTABLE; 965 } 966 } 967 968 @Override 969 public boolean isDirty() { 970 for (DataModel dm : dataModels.values()) { 971 DocumentPart part = ((DataModelImpl) dm).getDocumentPart(); 972 if (part.isDirty()) { 973 return true; 974 } 975 } 976 return false; 977 } 978 979 @Override 980 public ScopedMap getContextData() { 981 return contextData; 982 } 983 984 @Override 985 public Serializable getContextData(ScopeType scope, String key) { 986 return contextData.get(key); 987 } 988 989 @Override 990 public void putContextData(ScopeType scope, String key, Serializable value) { 991 contextData.put(key, value); 992 } 993 994 @Override 995 public Serializable getContextData(String key) { 996 return contextData.get(key); 997 } 998 999 @Override 1000 public void putContextData(String key, Serializable value) { 1001 contextData.put(key, value); 1002 } 1003 1004 @Override 1005 public void copyContextData(DocumentModel otherDocument) { 1006 contextData.putAll(otherDocument.getContextData()); 1007 } 1008 1009 @Override 1010 public void copyContent(DocumentModel sourceDoc) { 1011 computeFacetsAndSchemas(((DocumentModelImpl) sourceDoc).instanceFacets); 1012 Map<String, DataModel> newDataModels = new HashMap<>(); 1013 for (String key : schemas) { 1014 DataModel oldDM = sourceDoc.getDataModel(key); 1015 DataModel newDM; 1016 if (oldDM != null) { 1017 newDM = cloneDataModel(oldDM); 1018 } else { 1019 // create an empty datamodel 1020 Schema schema = Framework.getService(SchemaManager.class).getSchema(key); 1021 newDM = new DataModelImpl(new DocumentPartImpl(schema)); 1022 } 1023 newDataModels.put(key, newDM); 1024 } 1025 dataModels = newDataModels; 1026 } 1027 1028 @SuppressWarnings("unchecked") 1029 public static Object cloneField(Field field, String key, Object value) { 1030 // key is unused 1031 Object clone; 1032 Type type = field.getType(); 1033 if (type.isSimpleType()) { 1034 // CLONE TODO 1035 if (value instanceof Calendar) { 1036 Calendar newValue = (Calendar) value; 1037 clone = newValue.clone(); 1038 } else { 1039 clone = value; 1040 } 1041 } else if (type.isListType()) { 1042 ListType ltype = (ListType) type; 1043 Field lfield = ltype.getField(); 1044 Type ftype = lfield.getType(); 1045 List<Object> list; 1046 if (value instanceof Object[]) { // these are stored as arrays 1047 list = Arrays.asList((Object[]) value); 1048 } else { 1049 list = (List<Object>) value; 1050 } 1051 if (ftype.isComplexType()) { 1052 List<Object> clonedList = new ArrayList<>(list.size()); 1053 for (Object o : list) { 1054 clonedList.add(cloneField(lfield, null, o)); 1055 } 1056 clone = clonedList; 1057 } else { 1058 Class<?> klass = JavaTypes.getClass(ftype); 1059 if (klass.isPrimitive()) { 1060 clone = PrimitiveArrays.toPrimitiveArray(list, klass); 1061 } else { 1062 clone = list.toArray((Object[]) Array.newInstance(klass, list.size())); 1063 } 1064 } 1065 } else { 1066 // complex type 1067 ComplexType ctype = (ComplexType) type; 1068 if (TypeConstants.isContentType(ctype)) { // if a blob 1069 Blob blob = (Blob) value; // TODO 1070 clone = blob; 1071 } else { 1072 // a map, regular complex type 1073 Map<String, Object> map = (Map<String, Object>) value; 1074 Map<String, Object> clonedMap = new HashMap<>(); 1075 for (Map.Entry<String, Object> entry : map.entrySet()) { 1076 Object v = entry.getValue(); 1077 String k = entry.getKey(); 1078 if (v == null) { 1079 continue; 1080 } 1081 clonedMap.put(k, cloneField(ctype.getField(k), k, v)); 1082 } 1083 clone = clonedMap; 1084 } 1085 } 1086 return clone; 1087 } 1088 1089 public static DataModel cloneDataModel(Schema schema, DataModel data) { 1090 DataModel dm = new DataModelImpl(schema.getName()); 1091 for (Field field : schema.getFields()) { 1092 String key = field.getName().getLocalName(); 1093 Object value; 1094 try { 1095 value = data.getData(key); 1096 } catch (PropertyException e1) { 1097 continue; 1098 } 1099 if (value == null) { 1100 continue; 1101 } 1102 Object clone = cloneField(field, key, value); 1103 dm.setData(key, clone); 1104 } 1105 return dm; 1106 } 1107 1108 public DataModel cloneDataModel(DataModel data) { 1109 TypeProvider typeProvider = Framework.getLocalService(SchemaManager.class); 1110 return cloneDataModel(typeProvider.getSchema(data.getSchema()), data); 1111 } 1112 1113 @Override 1114 public String getCacheKey() { 1115 // UUID - sessionId 1116 String key = id + '-' + sid + '-' + getPathAsString(); 1117 // assume the doc holds the dublincore schema (enough for us right now) 1118 if (hasSchema("dublincore")) { 1119 Calendar timeStamp = (Calendar) getProperty("dublincore", "modified"); 1120 if (timeStamp != null) { 1121 // remove milliseconds as they are not stored in some 1122 // databases, which could make the comparison fail just after a 1123 // document creation (see NXP-8783) 1124 timeStamp.set(Calendar.MILLISECOND, 0); 1125 key += '-' + String.valueOf(timeStamp.getTimeInMillis()); 1126 } 1127 } 1128 return key; 1129 } 1130 1131 @Override 1132 public String getRepositoryName() { 1133 return repositoryName; 1134 } 1135 1136 @Override 1137 public String getSourceId() { 1138 return sourceId; 1139 } 1140 1141 public boolean isSchemaLoaded(String name) { 1142 return dataModels.containsKey(name); 1143 } 1144 1145 @Override 1146 public boolean isPrefetched(String xpath) { 1147 return prefetch != null && prefetch.isPrefetched(xpath); 1148 } 1149 1150 @Override 1151 public boolean isPrefetched(String schemaName, String name) { 1152 return prefetch != null && prefetch.isPrefetched(schemaName, name); 1153 } 1154 1155 /** 1156 * Sets prefetch information. 1157 * <p> 1158 * INTERNAL: This method is not in the public interface. 1159 * 1160 * @since 5.5 1161 */ 1162 public void setPrefetch(Prefetch prefetch) { 1163 this.prefetch = prefetch; 1164 } 1165 1166 @Override 1167 public void prefetchCurrentLifecycleState(String lifecycle) { 1168 currentLifeCycleState = lifecycle; 1169 } 1170 1171 @Override 1172 public void prefetchLifeCyclePolicy(String lifeCyclePolicy) { 1173 this.lifeCyclePolicy = lifeCyclePolicy; 1174 } 1175 1176 @Override 1177 // need this for tree in RCP clients 1178 public boolean equals(Object obj) { 1179 if (obj == this) { 1180 return true; 1181 } 1182 if (obj instanceof DocumentModelImpl) { 1183 DocumentModel documentModel = (DocumentModel) obj; 1184 String id = documentModel.getId(); 1185 if (id != null) { 1186 return id.equals(this.id); 1187 } 1188 } 1189 return false; 1190 } 1191 1192 @Override 1193 public int hashCode() { 1194 return id == null ? 0 : id.hashCode(); 1195 } 1196 1197 @Override 1198 public String toString() { 1199 String title = id; 1200 if (getDataModels().containsKey("dublincore")) { 1201 title = getTitle(); 1202 } 1203 return getClass().getSimpleName() + '(' + id + ", path=" + path + ", title=" + title + ')'; 1204 } 1205 1206 @Override 1207 public <T extends Serializable> T getSystemProp(final String systemProperty, final Class<T> type) { 1208 return getSession().getDocumentSystemProp(ref, systemProperty, type); 1209 } 1210 1211 @Override 1212 public boolean isLifeCycleLoaded() { 1213 return currentLifeCycleState != null; 1214 } 1215 1216 @Override 1217 @Deprecated 1218 public DocumentPart getPart(String schema) { 1219 DataModel dm = getDataModel(schema); 1220 if (dm != null) { 1221 return ((DataModelImpl) dm).getDocumentPart(); 1222 } 1223 return null; // TODO thrown an exception? 1224 } 1225 1226 @Override 1227 @Deprecated 1228 public DocumentPart[] getParts() { 1229 // DocumentType type = getDocumentType(); 1230 // type = Framework.getService(SchemaManager.class).getDocumentType( 1231 // getType()); 1232 // Collection<Schema> schemas = type.getSchemas(); 1233 // Set<String> allSchemas = getAllSchemas(); 1234 DocumentPart[] parts = new DocumentPart[schemas.size()]; 1235 int i = 0; 1236 for (String schema : schemas) { 1237 DataModel dm = getDataModel(schema); 1238 parts[i++] = ((DataModelImpl) dm).getDocumentPart(); 1239 } 1240 return parts; 1241 } 1242 1243 @Override 1244 public Collection<Property> getPropertyObjects(String schema) { 1245 DocumentPart part = getPart(schema); 1246 return part == null ? Collections.emptyList() : part.getChildren(); 1247 } 1248 1249 @Override 1250 public Property getProperty(String xpath) { 1251 if (xpath == null) { 1252 throw new PropertyNotFoundException("null", "Invalid null xpath"); 1253 } 1254 String cxpath = canonicalXPath(xpath); 1255 if (cxpath.isEmpty()) { 1256 throw new PropertyNotFoundException(xpath, "Schema not specified"); 1257 } 1258 String schemaName = getXPathSchemaName(cxpath, schemas, null); 1259 if (schemaName == null) { 1260 if (cxpath.indexOf(':') != -1) { 1261 throw new PropertyNotFoundException(xpath, "No such schema"); 1262 } else { 1263 throw new PropertyNotFoundException(xpath); 1264 } 1265 1266 } 1267 DocumentPart part = getPart(schemaName); 1268 if (part == null) { 1269 throw new PropertyNotFoundException(xpath); 1270 } 1271 // cut prefix 1272 String partPath = cxpath.substring(cxpath.indexOf(':') + 1); 1273 try { 1274 return part.resolvePath(partPath); 1275 } catch (PropertyNotFoundException e) { 1276 throw new PropertyNotFoundException(xpath, e.getDetail()); 1277 } 1278 } 1279 1280 public static String getXPathSchemaName(String xpath, Set<String> docSchemas, String[] returnName) { 1281 SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class); 1282 // find first segment 1283 int i = xpath.indexOf('/'); 1284 String prop = i == -1 ? xpath : xpath.substring(0, i); 1285 int p = prop.indexOf(':'); 1286 if (p != -1) { 1287 // prefixed 1288 String prefix = prop.substring(0, p); 1289 Schema schema = schemaManager.getSchemaFromPrefix(prefix); 1290 if (schema == null) { 1291 // try directly with prefix as a schema name 1292 schema = schemaManager.getSchema(prefix); 1293 if (schema == null) { 1294 return null; 1295 } 1296 } 1297 if (returnName != null) { 1298 returnName[0] = prop.substring(p + 1); 1299 } 1300 return schema.getName(); 1301 } else { 1302 // unprefixed 1303 // search for the first matching schema having a property 1304 // with the same name as the first path segment 1305 for (String schemaName : docSchemas) { 1306 Schema schema = schemaManager.getSchema(schemaName); 1307 if (schema != null && schema.hasField(prop)) { 1308 if (returnName != null) { 1309 returnName[0] = prop; 1310 } 1311 return schema.getName(); 1312 } 1313 } 1314 return null; 1315 } 1316 } 1317 1318 @Override 1319 public Serializable getPropertyValue(String xpath) throws PropertyException { 1320 if (prefetch != null) { 1321 Serializable value = prefetch.get(xpath); 1322 if (value != NULL) { 1323 return value; 1324 } 1325 } 1326 return getProperty(xpath).getValue(); 1327 } 1328 1329 @Override 1330 public void setPropertyValue(String xpath, Serializable value) throws PropertyException { 1331 getProperty(xpath).setValue(value); 1332 clearPrefetchXPath(xpath); 1333 } 1334 1335 private void clearPrefetch(String schemaName) { 1336 if (prefetch != null) { 1337 prefetch.clearPrefetch(schemaName); 1338 if (prefetch.isEmpty()) { 1339 prefetch = null; 1340 } 1341 } 1342 } 1343 1344 protected void clearPrefetchXPath(String xpath) { 1345 if (prefetch != null) { 1346 String schemaName = prefetch.getXPathSchema(xpath, getDocumentType()); 1347 if (schemaName != null) { 1348 clearPrefetch(schemaName); 1349 } 1350 } 1351 } 1352 1353 @Override 1354 public DocumentModel clone() throws CloneNotSupportedException { 1355 DocumentModelImpl dm = (DocumentModelImpl) super.clone(); 1356 // dm.id =id; 1357 // dm.acp = acp; 1358 // dm.currentLifeCycleState = currentLifeCycleState; 1359 // dm.lifeCyclePolicy = lifeCyclePolicy; 1360 // dm.declaredSchemas = declaredSchemas; // schemas are immutable so we 1361 // don't clone the array 1362 // dm.flags = flags; 1363 // dm.repositoryName = repositoryName; 1364 // dm.ref = ref; 1365 // dm.parentRef = parentRef; 1366 // dm.path = path; // path is immutable 1367 // dm.isACPLoaded = isACPLoaded; 1368 // dm.prefetch = dm.prefetch; // prefetch can be shared 1369 // dm.lock = lock; 1370 // dm.sourceId =sourceId; 1371 // dm.sid = sid; 1372 // dm.type = type; 1373 dm.facets = new HashSet<String>(facets); // facets 1374 // should be 1375 // clones too - 1376 // they are not 1377 // immutable 1378 // context data is keeping contextual info so it is reset 1379 dm.contextData = new ScopedMap(); 1380 1381 // copy parts 1382 dm.dataModels = new HashMap<>(); 1383 for (Map.Entry<String, DataModel> entry : dataModels.entrySet()) { 1384 String key = entry.getKey(); 1385 DataModel data = entry.getValue(); 1386 DataModelImpl newData = new DataModelImpl(key, data.getMap()); 1387 for (String name : data.getDirtyFields()) { 1388 newData.setDirty(name); 1389 } 1390 dm.dataModels.put(key, newData); 1391 } 1392 return dm; 1393 } 1394 1395 @Override 1396 public void reset() { 1397 if (dataModels != null) { 1398 dataModels.clear(); 1399 } 1400 prefetch = null; 1401 isACPLoaded = false; 1402 acp = null; 1403 currentLifeCycleState = null; 1404 lifeCyclePolicy = null; 1405 } 1406 1407 @Override 1408 public void refresh() { 1409 detachedVersionLabel = null; 1410 1411 refresh(REFRESH_DEFAULT, null); 1412 } 1413 1414 @Override 1415 public void refresh(int refreshFlags, String[] schemas) { 1416 if (id == null) { 1417 // not yet saved 1418 return; 1419 } 1420 if ((refreshFlags & REFRESH_ACP_IF_LOADED) != 0 && isACPLoaded) { 1421 refreshFlags |= REFRESH_ACP; 1422 // we must not clean the REFRESH_ACP_IF_LOADED flag since it is 1423 // used 1424 // below on the client 1425 } 1426 1427 if ((refreshFlags & REFRESH_CONTENT_IF_LOADED) != 0) { 1428 refreshFlags |= REFRESH_CONTENT; 1429 Collection<String> keys = dataModels.keySet(); 1430 schemas = keys.toArray(new String[keys.size()]); 1431 } 1432 1433 DocumentModelRefresh refresh = getSession().refreshDocument(ref, refreshFlags, schemas); 1434 1435 if ((refreshFlags & REFRESH_PREFETCH) != 0) { 1436 prefetch = refresh.prefetch; 1437 } 1438 if ((refreshFlags & REFRESH_STATE) != 0) { 1439 currentLifeCycleState = refresh.lifeCycleState; 1440 lifeCyclePolicy = refresh.lifeCyclePolicy; 1441 isCheckedOut = refresh.isCheckedOut; 1442 isLatestVersion = refresh.isLatestVersion; 1443 isMajorVersion = refresh.isMajorVersion; 1444 isLatestMajorVersion = refresh.isLatestMajorVersion; 1445 isVersionSeriesCheckedOut = refresh.isVersionSeriesCheckedOut; 1446 versionSeriesId = refresh.versionSeriesId; 1447 checkinComment = refresh.checkinComment; 1448 isStateLoaded = true; 1449 } 1450 acp = null; 1451 isACPLoaded = false; 1452 if ((refreshFlags & REFRESH_ACP) != 0) { 1453 acp = refresh.acp; 1454 isACPLoaded = true; 1455 } 1456 1457 if ((refreshFlags & (REFRESH_CONTENT | REFRESH_CONTENT_LAZY)) != 0) { 1458 dataModels.clear(); 1459 computeFacetsAndSchemas(refresh.instanceFacets); 1460 } 1461 if ((refreshFlags & REFRESH_CONTENT) != 0) { 1462 DocumentPart[] parts = refresh.documentParts; 1463 if (parts != null) { 1464 for (DocumentPart part : parts) { 1465 DataModelImpl dm = new DataModelImpl(part); 1466 dataModels.put(dm.getSchema(), dm); 1467 } 1468 } 1469 } 1470 } 1471 1472 /** 1473 * Recomputes all facets and schemas from the instance facets. 1474 * 1475 * @since 7.1 1476 */ 1477 protected void computeFacetsAndSchemas(Set<String> instanceFacets) { 1478 this.instanceFacets = instanceFacets; 1479 instanceFacetsOrig = new HashSet<>(instanceFacets); 1480 facets = new HashSet<>(instanceFacets); 1481 facets.addAll(getDocumentType().getFacets()); 1482 if (isImmutable()) { 1483 facets.add(FacetNames.IMMUTABLE); 1484 } 1485 schemas = computeSchemas(getDocumentType(), instanceFacets, isProxy()); 1486 schemasOrig = new HashSet<>(schemas); 1487 } 1488 1489 @Override 1490 public String getChangeToken() { 1491 if (ref == null) { 1492 // not an actual connected document 1493 if (changeToken == null) { 1494 Calendar modified; 1495 try { 1496 modified = (Calendar) getPropertyValue("dc:modified"); 1497 } catch (PropertyNotFoundException e) { 1498 modified = null; 1499 } 1500 changeToken = modified == null ? null : String.valueOf(modified.getTimeInMillis()); 1501 } 1502 return changeToken; 1503 } 1504 if (hasSession()) { 1505 changeToken = getSession().getChangeToken(ref); 1506 } 1507 return changeToken; 1508 } 1509 1510 /** 1511 * Sets the document id. May be useful when detaching from a repo and attaching to another one or when unmarshalling 1512 * a documentModel from a XML or JSON representation 1513 * 1514 * @since 5.7.2 1515 */ 1516 public void setId(String id) { 1517 this.id = id; 1518 } 1519 1520 @Override 1521 public Map<String, String> getBinaryFulltext() { 1522 if (!hasSession()) { 1523 return null; 1524 } 1525 return getSession().getBinaryFulltext(ref); 1526 } 1527 1528 @Override 1529 public PropertyObjectResolver getObjectResolver(String xpath) { 1530 return DocumentPropertyObjectResolverImpl.create(this, xpath); 1531 } 1532 1533 /** 1534 * Replace the content by it's the reference if the document is live and not dirty. 1535 * 1536 * @see org.nuxeo.ecm.core.event.EventContext 1537 * @since 7.10 1538 */ 1539 private Object writeReplace() throws ObjectStreamException { 1540 if (!TransactionHelper.isTransactionActive()) { // protect from no transaction 1541 Transaction tx = TransactionHelper.suspendTransaction(); 1542 try { 1543 TransactionHelper.startTransaction(); 1544 try { 1545 return writeReplace(); 1546 } finally { 1547 TransactionHelper.commitOrRollbackTransaction(); 1548 } 1549 } finally { 1550 if (tx != null) { 1551 TransactionHelper.resumeTransaction(tx); 1552 } 1553 } 1554 } 1555 if (isDirty()) { 1556 return this; 1557 } 1558 if (!hasSession()) { 1559 return this; 1560 } 1561 CoreSession session = getSession(); 1562 if (!session.exists(ref)) { 1563 return this; 1564 } 1565 return new InstanceRef(this, session.getPrincipal()); 1566 } 1567 1568 /** 1569 * Legacy code: Explicitly detach the document to send the document as an event context parameter. 1570 * 1571 * @see org.nuxeo.ecm.core.event.EventContext 1572 * @since 7.10 1573 */ 1574 private void writeObject(ObjectOutputStream stream) throws IOException { 1575 detach(ref != null && hasSession() && getSession().exists(ref)); 1576 stream.defaultWriteObject(); 1577 } 1578 1579}