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