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