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