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