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