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.LifeCycleConstants; 054import org.nuxeo.ecm.core.api.Lock; 055import org.nuxeo.ecm.core.api.NuxeoException; 056import org.nuxeo.ecm.core.api.PathRef; 057import org.nuxeo.ecm.core.api.PropertyException; 058import org.nuxeo.ecm.core.api.VersioningOption; 059import org.nuxeo.ecm.core.api.adapter.DocumentAdapterDescriptor; 060import org.nuxeo.ecm.core.api.adapter.DocumentAdapterService; 061import org.nuxeo.ecm.core.api.model.DocumentPart; 062import org.nuxeo.ecm.core.api.model.Property; 063import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; 064import org.nuxeo.ecm.core.api.model.PropertyVisitor; 065import org.nuxeo.ecm.core.api.model.impl.DocumentPartImpl; 066import org.nuxeo.ecm.core.api.model.resolver.DocumentPropertyObjectResolverImpl; 067import org.nuxeo.ecm.core.api.model.resolver.PropertyObjectResolver; 068import org.nuxeo.ecm.core.api.security.ACP; 069import org.nuxeo.ecm.core.schema.DocumentType; 070import org.nuxeo.ecm.core.schema.FacetNames; 071import org.nuxeo.ecm.core.schema.PropertyDeprecationHandler; 072import org.nuxeo.ecm.core.schema.SchemaManager; 073import org.nuxeo.ecm.core.schema.TypeConstants; 074import org.nuxeo.ecm.core.schema.TypeProvider; 075import org.nuxeo.ecm.core.schema.types.ComplexType; 076import org.nuxeo.ecm.core.schema.types.CompositeType; 077import org.nuxeo.ecm.core.schema.types.Field; 078import org.nuxeo.ecm.core.schema.types.JavaTypes; 079import org.nuxeo.ecm.core.schema.types.ListType; 080import org.nuxeo.ecm.core.schema.types.Schema; 081import org.nuxeo.ecm.core.schema.types.Type; 082import org.nuxeo.runtime.api.Framework; 083import org.nuxeo.runtime.transaction.TransactionHelper; 084 085/** 086 * Standard implementation of a {@link DocumentModel}. 087 */ 088public class DocumentModelImpl implements DocumentModel, Cloneable { 089 090 private static final long serialVersionUID = 1L; 091 092 public static final long F_VERSION = 16L; 093 094 public static final long F_PROXY = 32L; 095 096 public static final long F_IMMUTABLE = 256L; 097 098 private static final Log log = LogFactory.getLog(DocumentModelImpl.class); 099 100 protected String sid; 101 102 protected DocumentRef ref; 103 104 protected DocumentType type; 105 106 // for tests, keep the type name even if no actual type is registered 107 protected String typeName; 108 109 /** Schemas including those from instance facets. */ 110 protected Set<String> schemas; 111 112 /** Schemas including those from instance facets when the doc was read */ 113 protected Set<String> schemasOrig; 114 115 /** Facets including those on instance. */ 116 protected Set<String> facets; 117 118 /** Instance facets. */ 119 public Set<String> instanceFacets; 120 121 /** Instance facets when the document was read. */ 122 public Set<String> instanceFacetsOrig; 123 124 protected String id; 125 126 protected Path path; 127 128 protected Long pos; 129 130 protected Map<String, DataModel> dataModels; 131 132 protected DocumentRef parentRef; 133 134 protected static final Lock LOCK_UNKNOWN = new Lock(null, null); 135 136 protected Lock lock = LOCK_UNKNOWN; 137 138 /** state is lifecycle, version stuff. */ 139 protected boolean isStateLoaded; 140 141 // loaded if isStateLoaded 142 protected String currentLifeCycleState; 143 144 // loaded if isStateLoaded 145 protected String lifeCyclePolicy; 146 147 // loaded if isStateLoaded 148 protected boolean isCheckedOut = true; 149 150 // loaded if isStateLoaded 151 protected String versionSeriesId; 152 153 // loaded if isStateLoaded 154 protected boolean isLatestVersion; 155 156 // loaded if isStateLoaded 157 protected boolean isMajorVersion; 158 159 // loaded if isStateLoaded 160 protected boolean isLatestMajorVersion; 161 162 // loaded if isStateLoaded 163 protected boolean isVersionSeriesCheckedOut; 164 165 // loaded if isStateLoaded 166 protected String checkinComment; 167 168 // acp is not send between client/server 169 // it will be loaded lazy first time it is accessed 170 // and discarded when object is serialized 171 protected transient ACP acp; 172 173 // whether the acp was cached 174 protected transient boolean isACPLoaded = false; 175 176 // the adapters registered for this document - only valid on client 177 protected transient HashMap<Class<?>, Object> adapters; 178 179 /** 180 * Flags: bitwise combination of {@link #F_VERSION}, {@link #F_PROXY}, {@link #F_IMMUTABLE}. 181 */ 182 private long flags = 0L; 183 184 protected String repositoryName; 185 186 protected String sourceId; 187 188 protected Map<String, Serializable> contextData = new HashMap<>(); 189 190 private String detachedVersionLabel; 191 192 // always refetched when a session is accessible, but also available without one 193 protected String changeToken; 194 195 protected DocumentModelImpl() { 196 } 197 198 /** 199 * Constructor to use a document model client side without referencing a document. 200 * <p> 201 * It must at least contain the type. 202 */ 203 public DocumentModelImpl(String typeName) { 204 this.type = getSchemaManager().getDocumentType(typeName); 205 this.typeName = typeName; 206 dataModels = new HashMap<>(); 207 instanceFacets = new HashSet<>(); 208 instanceFacetsOrig = new HashSet<>(); 209 facets = new HashSet<>(); 210 schemas = new HashSet<>(); 211 schemasOrig = new HashSet<>(); 212 } 213 214 /** 215 * Constructor to be used by clients. 216 * <p> 217 * A client constructed data model must contain at least the path and the type. 218 */ 219 public DocumentModelImpl(String parentPath, String name, String type) { 220 this(type); 221 String fullPath = parentPath == null ? name : parentPath + (parentPath.endsWith("/") ? "" : "/") + name; 222 path = new Path(fullPath); 223 ref = new PathRef(fullPath); 224 if (getDocumentType() != null) { 225 facets.addAll(getDocumentType().getFacets()); 226 } 227 schemas = computeSchemas(getDocumentType(), instanceFacets, false); 228 schemasOrig = new HashSet<>(schemas); 229 } 230 231 /** 232 * Constructor. 233 * <p> 234 * The lock parameter is unused since 5.4.2. 235 * 236 * @param facets the per-instance facets 237 */ 238 // TODO check if we use it 239 public DocumentModelImpl(String sid, String type, String id, Path path, Lock lock, DocumentRef docRef, 240 DocumentRef parentRef, String[] schemas, Set<String> facets, String sourceId, String repositoryName) { 241 this(sid, type, id, path, docRef, parentRef, schemas, facets, sourceId, repositoryName, false); 242 } 243 244 public DocumentModelImpl(String sid, String type, String id, Path path, DocumentRef docRef, DocumentRef parentRef, 245 String[] schemas, Set<String> facets, String sourceId, String repositoryName, boolean isProxy) { 246 this(type); 247 this.sid = sid; 248 this.id = id; 249 this.path = path; 250 ref = docRef; 251 this.parentRef = parentRef; 252 instanceFacets = facets == null ? new HashSet<>() : new HashSet<>(facets); 253 instanceFacetsOrig = new HashSet<>(instanceFacets); 254 this.facets = new HashSet<>(instanceFacets); 255 if (getDocumentType() != null) { 256 this.facets.addAll(getDocumentType().getFacets()); 257 } 258 if (schemas == null) { 259 this.schemas = computeSchemas(getDocumentType(), instanceFacets, isProxy); 260 } else { 261 this.schemas = new HashSet<>(Arrays.asList(schemas)); 262 } 263 schemasOrig = new HashSet<>(this.schemas); 264 this.repositoryName = repositoryName; 265 this.sourceId = sourceId; 266 setIsProxy(isProxy); 267 } 268 269 /** 270 * Recomputes effective schemas from a type + instance facets. 271 */ 272 public static Set<String> computeSchemas(DocumentType type, Collection<String> instanceFacets, boolean isProxy) { 273 Set<String> schemas = new HashSet<>(); 274 if (type != null) { 275 schemas.addAll(Arrays.asList(type.getSchemaNames())); 276 } 277 TypeProvider typeProvider = getSchemaManager(); 278 for (String facet : instanceFacets) { 279 CompositeType facetType = typeProvider.getFacet(facet); 280 if (facetType != null) { // ignore pseudo-facets like Immutable 281 schemas.addAll(Arrays.asList(facetType.getSchemaNames())); 282 } 283 } 284 if (isProxy) { 285 for (Schema schema : typeProvider.getProxySchemas(type.getName())) { 286 schemas.add(schema.getName()); 287 } 288 } 289 return schemas; 290 } 291 292 public DocumentModelImpl(DocumentModel parent, String name, String type) { 293 this(parent.getPathAsString(), name, type); 294 } 295 296 @Override 297 public DocumentType getDocumentType() { 298 return type; 299 } 300 301 /** 302 * Gets the title from the dublincore schema. 303 * 304 * @see DocumentModel#getTitle() 305 */ 306 @Override 307 public String getTitle() { 308 String title = (String) getProperty("dublincore", "title"); 309 if (title != null) { 310 return title; 311 } 312 title = getName(); 313 if (title != null) { 314 return title; 315 } 316 return id; 317 } 318 319 @Override 320 public String getSessionId() { 321 return sid; 322 } 323 324 @Override 325 public DocumentRef getRef() { 326 return ref; 327 } 328 329 @Override 330 public DocumentRef getParentRef() { 331 if (parentRef == null && path != null) { 332 if (path.isAbsolute()) { 333 Path parentPath = path.removeLastSegments(1); 334 parentRef = new PathRef(parentPath.toString()); 335 } 336 // else keep parentRef null 337 } 338 return parentRef; 339 } 340 341 @Override 342 public CoreSession getCoreSession() { 343 if (sid == null) { 344 return null; 345 } 346 return Framework.getService(CoreSessionService.class).getCoreSession(sid); 347 } 348 349 protected boolean hasSession() { 350 return getCoreSession() != null; 351 } 352 353 /** 354 * Gets the CoreSession, or fails if it's not available. 355 * 356 * @since 9.1 357 */ 358 protected CoreSession getSession() { 359 CoreSession session = getCoreSession(); 360 if (session != null) { 361 return session; 362 } 363 throw new NuxeoException("The DocumentModel is not associated to an open CoreSession: " + this); 364 } 365 366 @Override 367 public void detach(boolean loadAll) { 368 if (sid == null) { 369 return; 370 } 371 try { 372 if (loadAll) { 373 for (String schema : schemas) { 374 if (!isSchemaLoaded(schema)) { 375 loadDataModel(schema); 376 } 377 } 378 // fetch ACP too if possible 379 if (ref != null) { 380 getACP(); 381 } 382 detachedVersionLabel = getVersionLabel(); 383 // load some system info 384 isCheckedOut(); 385 getCurrentLifeCycleState(); 386 getLockInfo(); 387 getChangeToken(); 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 // TODO move TrashService to nuxeo-core-api in order to not rely on session here ? 753 return getSession().isTrashed(ref); 754 } 755 756 @Override 757 public boolean isFolder() { 758 return hasFacet(FacetNames.FOLDERISH); 759 } 760 761 @Override 762 public boolean isVersionable() { 763 return hasFacet(FacetNames.VERSIONABLE); 764 } 765 766 @Override 767 public boolean isDownloadable() { 768 if (hasFacet(FacetNames.DOWNLOADABLE)) { 769 // TODO find a better way to check size that does not depend on the 770 // document schema 771 Long size = (Long) getProperty("common", "size"); 772 if (size != null) { 773 return size.longValue() != 0; 774 } 775 } 776 return false; 777 } 778 779 @Override 780 public void accept(PropertyVisitor visitor, Object arg) { 781 for (DocumentPart dp : getParts()) { 782 ((DocumentPartImpl) dp).visitChildren(visitor, arg); 783 } 784 } 785 786 @Override 787 @SuppressWarnings("unchecked") 788 public <T> T getAdapter(Class<T> itf) { 789 T facet = (T) getAdapters().get(itf); 790 if (facet == null) { 791 facet = findAdapter(itf); 792 if (facet != null) { 793 adapters.put(itf, facet); 794 } 795 } 796 return facet; 797 } 798 799 /** 800 * Lazy initialization for adapters because they don't survive the serialization. 801 */ 802 private Map<Class<?>, Object> getAdapters() { 803 if (adapters == null) { 804 adapters = new HashMap<>(); 805 } 806 807 return adapters; 808 } 809 810 @Override 811 public <T> T getAdapter(Class<T> itf, boolean refreshCache) { 812 T facet; 813 814 if (!refreshCache) { 815 facet = getAdapter(itf); 816 } else { 817 facet = findAdapter(itf); 818 } 819 820 if (facet != null) { 821 getAdapters().put(itf, facet); 822 } 823 return facet; 824 } 825 826 @SuppressWarnings("unchecked") 827 private <T> T findAdapter(Class<T> itf) { 828 DocumentAdapterService svc = Framework.getService(DocumentAdapterService.class); 829 if (svc != null) { 830 DocumentAdapterDescriptor dae = svc.getAdapterDescriptor(itf); 831 if (dae != null) { 832 String facet = dae.getFacet(); 833 if (facet == null) { 834 // if no facet is specified, accept the adapter 835 return (T) dae.getFactory().getAdapter(this, itf); 836 } else if (hasFacet(facet)) { 837 return (T) dae.getFactory().getAdapter(this, itf); 838 } else { 839 // TODO: throw an exception 840 log.error("Document model cannot be adapted to " + itf + " because it has no facet " + facet); 841 } 842 } 843 } else { 844 log.warn("DocumentAdapterService not available. Cannot get document model adaptor for " + itf); 845 } 846 return null; 847 } 848 849 @Override 850 public boolean followTransition(final String transition) { 851 // TODO is it better to make public followTransition(DocumentRef, String, Map<String, Serializable>) ? 852 // give this DocumentModel in order to pass context data 853 boolean res = getSession().followTransition(this, transition); 854 // Invalidate the prefetched value in this case. 855 if (res) { 856 currentLifeCycleState = null; 857 } 858 return res; 859 } 860 861 @Override 862 public Collection<String> getAllowedStateTransitions() { 863 return getSession().getAllowedStateTransitions(ref); 864 } 865 866 @Override 867 public String getCurrentLifeCycleState() { 868 if (currentLifeCycleState != null) { 869 return currentLifeCycleState; 870 } 871 if (!hasSession()) { 872 // document was just created => not life cycle yet 873 return null; 874 } 875 currentLifeCycleState = getSession().getCurrentLifeCycleState(ref); 876 return currentLifeCycleState; 877 } 878 879 @Override 880 public String getLifeCyclePolicy() { 881 if (lifeCyclePolicy != null) { 882 return lifeCyclePolicy; 883 } 884 // String lifeCyclePolicy = null; 885 lifeCyclePolicy = getSession().getLifeCyclePolicy(ref); 886 return lifeCyclePolicy; 887 } 888 889 @Override 890 public boolean isVersion() { 891 return (flags & F_VERSION) != 0; 892 } 893 894 @Override 895 public boolean isProxy() { 896 return (flags & F_PROXY) != 0; 897 } 898 899 @Override 900 public boolean isImmutable() { 901 return (flags & F_IMMUTABLE) != 0; 902 } 903 904 public void setIsVersion(boolean isVersion) { 905 if (isVersion) { 906 flags |= F_VERSION; 907 } else { 908 flags &= ~F_VERSION; 909 } 910 } 911 912 public void setIsProxy(boolean isProxy) { 913 if (isProxy) { 914 flags |= F_PROXY; 915 } else { 916 flags &= ~F_PROXY; 917 } 918 } 919 920 public void setIsImmutable(boolean isImmutable) { 921 if (isImmutable) { 922 flags |= F_IMMUTABLE; 923 } else { 924 flags &= ~F_IMMUTABLE; 925 } 926 } 927 928 @Override 929 public boolean isDirty() { 930 for (DataModel dm : dataModels.values()) { 931 DocumentPart part = ((DataModelImpl) dm).getDocumentPart(); 932 if (part.isDirty()) { 933 return true; 934 } 935 } 936 return false; 937 } 938 939 @Override 940 public Map<String, Serializable> getContextData() { 941 return contextData; 942 } 943 944 @Override 945 public Serializable getContextData(String key) { 946 return contextData.get(key); 947 } 948 949 @Override 950 public void putContextData(String key, Serializable value) { 951 contextData.put(key, value); 952 } 953 954 @Override 955 public void copyContextData(DocumentModel otherDocument) { 956 contextData.putAll(otherDocument.getContextData()); 957 } 958 959 @Override 960 public void copyContent(DocumentModel sourceDoc) { 961 if (sourceDoc instanceof DocumentModelImpl) { 962 computeFacetsAndSchemas(((DocumentModelImpl) sourceDoc).instanceFacets); 963 } 964 Map<String, DataModel> newDataModels = new HashMap<>(); 965 for (String key : schemas) { 966 DataModel oldDM = sourceDoc.getDataModel(key); 967 DataModel newDM; 968 if (oldDM != null) { 969 newDM = cloneDataModel(oldDM); 970 } else { 971 // create an empty datamodel 972 Schema schema = Framework.getService(SchemaManager.class).getSchema(key); 973 newDM = new DataModelImpl(new DocumentPartImpl(schema)); 974 } 975 newDataModels.put(key, newDM); 976 } 977 dataModels = newDataModels; 978 } 979 980 @SuppressWarnings("unchecked") 981 public static Object cloneField(Field field, String key, Object value) { 982 // key is unused 983 Object clone; 984 Type type = field.getType(); 985 if (type.isSimpleType()) { 986 // CLONE TODO 987 if (value instanceof Calendar) { 988 Calendar newValue = (Calendar) value; 989 clone = newValue.clone(); 990 } else { 991 clone = value; 992 } 993 } else if (type.isListType()) { 994 ListType ltype = (ListType) type; 995 Field lfield = ltype.getField(); 996 Type ftype = lfield.getType(); 997 List<Object> list; 998 if (value instanceof Object[]) { // these are stored as arrays 999 list = Arrays.asList((Object[]) value); 1000 } else { 1001 list = (List<Object>) value; 1002 } 1003 if (ftype.isComplexType()) { 1004 List<Object> clonedList = new ArrayList<>(list.size()); 1005 for (Object o : list) { 1006 clonedList.add(cloneField(lfield, null, o)); 1007 } 1008 clone = clonedList; 1009 } else { 1010 Class<?> klass = JavaTypes.getClass(ftype); 1011 if (klass.isPrimitive()) { 1012 clone = PrimitiveArrays.toPrimitiveArray(list, klass); 1013 } else { 1014 clone = list.toArray((Object[]) Array.newInstance(klass, list.size())); 1015 } 1016 } 1017 } else { 1018 // complex type 1019 ComplexType ctype = (ComplexType) type; 1020 if (TypeConstants.isContentType(ctype)) { // if a blob 1021 Blob blob = (Blob) value; // TODO 1022 clone = blob; 1023 } else { 1024 // a map, regular complex type 1025 Map<String, Object> map = (Map<String, Object>) value; 1026 Map<String, Object> clonedMap = new HashMap<>(); 1027 for (Map.Entry<String, Object> entry : map.entrySet()) { 1028 Object v = entry.getValue(); 1029 String k = entry.getKey(); 1030 if (v == null) { 1031 continue; 1032 } 1033 clonedMap.put(k, cloneField(ctype.getField(k), k, v)); 1034 } 1035 clone = clonedMap; 1036 } 1037 } 1038 return clone; 1039 } 1040 1041 public static DataModel cloneDataModel(Schema schema, DataModel data) { 1042 DataModel dm = new DataModelImpl(schema.getName()); 1043 for (Field field : schema.getFields()) { 1044 String key = field.getName().getLocalName(); 1045 Object value; 1046 try { 1047 value = data.getData(key); 1048 } catch (PropertyException e1) { 1049 continue; 1050 } 1051 if (value == null) { 1052 continue; 1053 } 1054 Object clone = cloneField(field, key, value); 1055 dm.setData(key, clone); 1056 } 1057 return dm; 1058 } 1059 1060 public DataModel cloneDataModel(DataModel data) { 1061 TypeProvider typeProvider = getSchemaManager(); 1062 return cloneDataModel(typeProvider.getSchema(data.getSchema()), data); 1063 } 1064 1065 @Override 1066 public String getCacheKey() { 1067 // UUID - sessionId 1068 String key = id + '-' + sid + '-' + getPathAsString(); 1069 // assume the doc holds the dublincore schema (enough for us right now) 1070 if (hasSchema("dublincore")) { 1071 Calendar timeStamp = (Calendar) getProperty("dublincore", "modified"); 1072 if (timeStamp != null) { 1073 // remove milliseconds as they are not stored in some 1074 // databases, which could make the comparison fail just after a 1075 // document creation (see NXP-8783) 1076 timeStamp.set(Calendar.MILLISECOND, 0); 1077 key += '-' + String.valueOf(timeStamp.getTimeInMillis()); 1078 } 1079 } 1080 return key; 1081 } 1082 1083 @Override 1084 public String getRepositoryName() { 1085 return repositoryName; 1086 } 1087 1088 @Override 1089 public String getSourceId() { 1090 return sourceId; 1091 } 1092 1093 public boolean isSchemaLoaded(String name) { 1094 return dataModels.containsKey(name); 1095 } 1096 1097 @Override 1098 public boolean isPrefetched(String xpath) { 1099 return false; 1100 } 1101 1102 @Override 1103 public boolean isPrefetched(String schemaName, String name) { 1104 return false; 1105 } 1106 1107 @Override 1108 public void prefetchCurrentLifecycleState(String lifecycle) { 1109 currentLifeCycleState = lifecycle; 1110 } 1111 1112 @Override 1113 public void prefetchLifeCyclePolicy(String lifeCyclePolicy) { 1114 this.lifeCyclePolicy = lifeCyclePolicy; 1115 } 1116 1117 @Override 1118 // need this for tree in RCP clients 1119 public boolean equals(Object obj) { 1120 if (obj == this) { 1121 return true; 1122 } 1123 if (obj instanceof DocumentModelImpl) { 1124 DocumentModel documentModel = (DocumentModel) obj; 1125 String id = documentModel.getId(); 1126 if (id != null) { 1127 return id.equals(this.id); 1128 } 1129 } 1130 return false; 1131 } 1132 1133 @Override 1134 public int hashCode() { 1135 return id == null ? 0 : id.hashCode(); 1136 } 1137 1138 @Override 1139 public String toString() { 1140 String title = id; 1141 if (getDataModels().containsKey("dublincore")) { 1142 title = getTitle(); 1143 } 1144 return getClass().getSimpleName() + '(' + id + ", path=" + path + ", title=" + title + ')'; 1145 } 1146 1147 @Override 1148 public <T extends Serializable> T getSystemProp(final String systemProperty, final Class<T> type) { 1149 return getSession().getDocumentSystemProp(ref, systemProperty, type); 1150 } 1151 1152 @Override 1153 public boolean isLifeCycleLoaded() { 1154 return currentLifeCycleState != null; 1155 } 1156 1157 @Override 1158 @Deprecated 1159 public DocumentPart getPart(String schema) { 1160 DataModel dm = getDataModel(schema); 1161 if (dm != null) { 1162 return ((DataModelImpl) dm).getDocumentPart(); 1163 } 1164 return null; // TODO thrown an exception? 1165 } 1166 1167 @Override 1168 @Deprecated 1169 public DocumentPart[] getParts() { 1170 // DocumentType type = getDocumentType(); 1171 // type = Framework.getService(SchemaManager.class).getDocumentType( 1172 // getType()); 1173 // Collection<Schema> schemas = type.getSchemas(); 1174 // Set<String> allSchemas = getAllSchemas(); 1175 DocumentPart[] parts = new DocumentPart[schemas.size()]; 1176 int i = 0; 1177 for (String schema : schemas) { 1178 DataModel dm = getDataModel(schema); 1179 parts[i++] = ((DataModelImpl) dm).getDocumentPart(); 1180 } 1181 return parts; 1182 } 1183 1184 @Override 1185 public Collection<Property> getPropertyObjects(String schema) { 1186 DocumentPart part = getPart(schema); 1187 return part == null ? Collections.emptyList() : part.getChildren(); 1188 } 1189 1190 @Override 1191 public Property getProperty(String xpath) { 1192 if (xpath == null) { 1193 throw new PropertyNotFoundException("null", "Invalid null xpath"); 1194 } 1195 String cxpath = canonicalXPath(xpath); 1196 if (cxpath.isEmpty()) { 1197 throw new PropertyNotFoundException(xpath, "Schema not specified"); 1198 } 1199 String schemaName = getXPathSchemaName(cxpath, schemas, null); 1200 if (schemaName == null) { 1201 if (cxpath.indexOf(':') != -1) { 1202 throw new PropertyNotFoundException(xpath, "No such schema"); 1203 } else { 1204 throw new PropertyNotFoundException(xpath); 1205 } 1206 1207 } 1208 DocumentPart part = getPart(schemaName); 1209 if (part == null) { 1210 throw new PropertyNotFoundException(xpath); 1211 } 1212 // cut prefix 1213 String partPath = cxpath.substring(cxpath.indexOf(':') + 1); 1214 try { 1215 return part.resolvePath(partPath); 1216 } catch (PropertyNotFoundException e) { 1217 throw new PropertyNotFoundException(xpath, e.getDetail()); 1218 } 1219 } 1220 1221 public static String getXPathSchemaName(String xpath, Set<String> docSchemas, String[] returnName) { 1222 SchemaManager schemaManager = getSchemaManager(); 1223 // find first segment 1224 int i = xpath.indexOf('/'); 1225 String prop = i == -1 ? xpath : xpath.substring(0, i); 1226 int p = prop.indexOf(':'); 1227 if (p != -1) { 1228 // prefixed 1229 String prefix = prop.substring(0, p); 1230 Schema schema = schemaManager.getSchemaFromPrefix(prefix); 1231 if (schema == null) { 1232 // try directly with prefix as a schema name 1233 schema = schemaManager.getSchema(prefix); 1234 if (schema == null) { 1235 return null; 1236 } 1237 } 1238 if (returnName != null) { 1239 returnName[0] = prop.substring(p + 1); 1240 } 1241 return schema.getName(); 1242 } else { 1243 // unprefixed 1244 // search for the first matching schema having a property 1245 // with the same name as the first path segment 1246 for (String schemaName : docSchemas) { 1247 Schema schema = schemaManager.getSchema(schemaName); 1248 if (schema != null && schema.hasField(prop)) { 1249 if (returnName != null) { 1250 returnName[0] = prop; 1251 } 1252 return schema.getName(); 1253 } 1254 } 1255 // no property found, maybe it's a removed property 1256 // search for the first matching removed property 1257 // as removed schema is not yet support we can rely on docSchemas 1258 PropertyDeprecationHandler removedProperties = schemaManager.getRemovedProperties(); 1259 for (String schemaName : docSchemas) { 1260 if (removedProperties.isMarked(schemaName, prop)) { 1261 if (returnName != null) { 1262 returnName[0] = prop; 1263 } 1264 return schemaName; 1265 } 1266 } 1267 return null; 1268 } 1269 } 1270 1271 @Override 1272 public Serializable getPropertyValue(String xpath) throws PropertyException { 1273 return getProperty(xpath).getValue(); 1274 } 1275 1276 @Override 1277 public void setPropertyValue(String xpath, Serializable value) throws PropertyException { 1278 getProperty(xpath).setValue(value); 1279 } 1280 1281 @Override 1282 public DocumentModel clone() throws CloneNotSupportedException { 1283 DocumentModelImpl dm = (DocumentModelImpl) super.clone(); 1284 // dm.id =id; 1285 // dm.acp = acp; 1286 // dm.currentLifeCycleState = currentLifeCycleState; 1287 // dm.lifeCyclePolicy = lifeCyclePolicy; 1288 // dm.declaredSchemas = declaredSchemas; // schemas are immutable so we 1289 // don't clone the array 1290 // dm.flags = flags; 1291 // dm.repositoryName = repositoryName; 1292 // dm.ref = ref; 1293 // dm.parentRef = parentRef; 1294 // dm.path = path; // path is immutable 1295 // dm.isACPLoaded = isACPLoaded; 1296 // dm.lock = lock; 1297 // dm.sourceId =sourceId; 1298 // dm.sid = sid; 1299 // dm.type = type; 1300 dm.facets = new HashSet<>(facets); // facets 1301 // should be 1302 // clones too - 1303 // they are not 1304 // immutable 1305 // context data is keeping contextual info so it is reset 1306 dm.contextData = new HashMap<>(); 1307 1308 // copy parts 1309 dm.dataModels = new HashMap<>(); 1310 for (Map.Entry<String, DataModel> entry : dataModels.entrySet()) { 1311 String key = entry.getKey(); 1312 DataModel data = entry.getValue(); 1313 DataModelImpl newData = new DataModelImpl(key, data.getMap()); 1314 for (String name : data.getDirtyFields()) { 1315 newData.setDirty(name); 1316 } 1317 dm.dataModels.put(key, newData); 1318 } 1319 return dm; 1320 } 1321 1322 @Override 1323 public void reset() { 1324 if (dataModels != null) { 1325 dataModels.clear(); 1326 } 1327 isACPLoaded = false; 1328 acp = null; 1329 currentLifeCycleState = null; 1330 lifeCyclePolicy = null; 1331 } 1332 1333 @Override 1334 public void refresh() { 1335 detachedVersionLabel = null; 1336 1337 refresh(REFRESH_DEFAULT, null); 1338 } 1339 1340 @Override 1341 public void refresh(int refreshFlags, String[] schemas) { 1342 if (id == null) { 1343 // not yet saved 1344 return; 1345 } 1346 if ((refreshFlags & REFRESH_ACP_IF_LOADED) != 0 && isACPLoaded) { 1347 refreshFlags |= REFRESH_ACP; 1348 // we must not clean the REFRESH_ACP_IF_LOADED flag since it is 1349 // used 1350 // below on the client 1351 } 1352 1353 if ((refreshFlags & REFRESH_CONTENT_IF_LOADED) != 0) { 1354 refreshFlags |= REFRESH_CONTENT; 1355 Collection<String> keys = dataModels.keySet(); 1356 schemas = keys.toArray(new String[keys.size()]); 1357 } 1358 1359 DocumentModelRefresh refresh = getSession().refreshDocument(ref, refreshFlags, schemas); 1360 1361 if ((refreshFlags & REFRESH_STATE) != 0) { 1362 currentLifeCycleState = refresh.lifeCycleState; 1363 lifeCyclePolicy = refresh.lifeCyclePolicy; 1364 isCheckedOut = refresh.isCheckedOut; 1365 isLatestVersion = refresh.isLatestVersion; 1366 isMajorVersion = refresh.isMajorVersion; 1367 isLatestMajorVersion = refresh.isLatestMajorVersion; 1368 isVersionSeriesCheckedOut = refresh.isVersionSeriesCheckedOut; 1369 versionSeriesId = refresh.versionSeriesId; 1370 checkinComment = refresh.checkinComment; 1371 isStateLoaded = true; 1372 } 1373 acp = null; 1374 isACPLoaded = false; 1375 if ((refreshFlags & REFRESH_ACP) != 0) { 1376 acp = refresh.acp; 1377 isACPLoaded = true; 1378 } 1379 1380 if ((refreshFlags & (REFRESH_CONTENT | REFRESH_CONTENT_LAZY)) != 0) { 1381 dataModels.clear(); 1382 computeFacetsAndSchemas(refresh.instanceFacets); 1383 } 1384 if ((refreshFlags & REFRESH_CONTENT) != 0) { 1385 DocumentPart[] parts = refresh.documentParts; 1386 if (parts != null) { 1387 for (DocumentPart part : parts) { 1388 DataModelImpl dm = new DataModelImpl(part); 1389 dataModels.put(dm.getSchema(), dm); 1390 } 1391 } 1392 } 1393 } 1394 1395 /** 1396 * Recomputes all facets and schemas from the instance facets. 1397 * 1398 * @since 7.1 1399 */ 1400 protected void computeFacetsAndSchemas(Set<String> instanceFacets) { 1401 if (getDocumentType() == null) { 1402 return; 1403 } 1404 1405 this.instanceFacets = instanceFacets; 1406 instanceFacetsOrig = new HashSet<>(instanceFacets); 1407 facets = new HashSet<>(instanceFacets); 1408 facets.addAll(getDocumentType().getFacets()); 1409 if (isImmutable()) { 1410 facets.add(FacetNames.IMMUTABLE); 1411 } 1412 schemas = computeSchemas(getDocumentType(), instanceFacets, isProxy()); 1413 schemasOrig = new HashSet<>(schemas); 1414 } 1415 1416 @Override 1417 public String getChangeToken() { 1418 if (ref == null) { 1419 // not an actual connected document 1420 if (changeToken == null) { 1421 Calendar modified; 1422 try { 1423 modified = (Calendar) getPropertyValue("dc:modified"); 1424 } catch (PropertyNotFoundException e) { 1425 modified = null; 1426 } 1427 changeToken = modified == null ? null : String.valueOf(modified.getTimeInMillis()); 1428 } 1429 return changeToken; 1430 } 1431 if (hasSession()) { 1432 changeToken = getSession().getChangeToken(ref); 1433 } 1434 return changeToken; 1435 } 1436 1437 /** 1438 * Sets the document id. May be useful when detaching from a repo and attaching to another one or when unmarshalling 1439 * a documentModel from a XML or JSON representation 1440 * 1441 * @since 5.7.2 1442 */ 1443 public void setId(String id) { 1444 this.id = id; 1445 } 1446 1447 @Override 1448 public Map<String, String> getBinaryFulltext() { 1449 if (!hasSession()) { 1450 return null; 1451 } 1452 return getSession().getBinaryFulltext(ref); 1453 } 1454 1455 @Override 1456 public PropertyObjectResolver getObjectResolver(String xpath) { 1457 return DocumentPropertyObjectResolverImpl.create(this, xpath); 1458 } 1459 1460 /** 1461 * Replace the content by it's the reference if the document is live and not dirty. 1462 * 1463 * @see org.nuxeo.ecm.core.event.EventContext 1464 * @since 7.10 1465 */ 1466 private Object writeReplace() throws ObjectStreamException { 1467 if (!TransactionHelper.isTransactionActive()) { // protect from no transaction 1468 Transaction tx = TransactionHelper.suspendTransaction(); 1469 try { 1470 TransactionHelper.startTransaction(); 1471 try { 1472 return writeReplace(); 1473 } finally { 1474 TransactionHelper.commitOrRollbackTransaction(); 1475 } 1476 } finally { 1477 if (tx != null) { 1478 TransactionHelper.resumeTransaction(tx); 1479 } 1480 } 1481 } 1482 if (isDirty()) { 1483 return this; 1484 } 1485 if (!hasSession()) { 1486 return this; 1487 } 1488 CoreSession session = getSession(); 1489 if (!session.exists(ref)) { 1490 return this; 1491 } 1492 return new InstanceRef(this, session.getPrincipal()); 1493 } 1494 1495 /** 1496 * Legacy code: Explicitly detach the document to send the document as an event context parameter. 1497 * 1498 * @see org.nuxeo.ecm.core.event.EventContext 1499 * @since 7.10 1500 */ 1501 private void writeObject(ObjectOutputStream stream) throws IOException { 1502 detach(ref != null && hasSession() && getSession().exists(ref)); 1503 stream.defaultWriteObject(); 1504 } 1505 1506 /** 1507 * @return {@link SchemaManager} service or throws an exception if no one is available 1508 * @since 9.3 1509 */ 1510 protected static SchemaManager getSchemaManager() { 1511 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 1512 if (schemaManager == null) { 1513 throw new NullPointerException("No registered SchemaManager"); 1514 } 1515 return schemaManager; 1516 } 1517 1518}