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