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