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