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.CoreSessionService;
056import org.nuxeo.ecm.core.api.DataModel;
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 Map<String, DataModel> 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 HashMap<>();
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 Framework.getService(CoreSessionService.class).getCoreSession(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 Property getPropertyObject(String schema, String name) {
665        DocumentPart part = getPart(schema);
666        return part == null ? null : part.get(name);
667    }
668
669    @Override
670    public void setPathInfo(String parentPath, String name) {
671        path = new Path(parentPath == null ? name : parentPath + '/' + name);
672        ref = new PathRef(parentPath, name);
673    }
674
675    protected String oldLockKey(Lock lock) {
676        if (lock == null) {
677            return null;
678        }
679        // return deprecated format, like "someuser:Nov 29, 2010"
680        String lockCreationDate = (lock.getCreated() == null) ? null
681                : DateFormat.getDateInstance(DateFormat.MEDIUM).format(new Date(lock.getCreated().getTimeInMillis()));
682        return lock.getOwner() + ':' + lockCreationDate;
683    }
684
685    @Override
686    @Deprecated
687    public String getLock() {
688        return oldLockKey(getLockInfo());
689    }
690
691    @Override
692    public boolean isLocked() {
693        return getLockInfo() != null;
694    }
695
696    @Override
697    @Deprecated
698    public void setLock(String key) {
699        setLock();
700    }
701
702    @Override
703    public void unlock() {
704        removeLock();
705    }
706
707    @Override
708    public Lock setLock() {
709        Lock newLock = new RunWithCoreSession<Lock>() {
710            @Override
711            public Lock run() {
712                return session.setLock(ref);
713            }
714        }.execute();
715        lock = newLock;
716        return lock;
717    }
718
719    @Override
720    public Lock getLockInfo() {
721        if (lock != LOCK_UNKNOWN) {
722            return lock;
723        }
724        // no lock if not tied to a session
725        CoreSession session = getCoreSession();
726        if (session == null) {
727            return null;
728        }
729        lock = session.getLockInfo(ref);
730        return lock;
731    }
732
733    @Override
734    public Lock removeLock() {
735        Lock oldLock = new RunWithCoreSession<Lock>() {
736            @Override
737            public Lock run() {
738                return session.removeLock(ref);
739            }
740        }.execute();
741        lock = null;
742        return oldLock;
743    }
744
745    @Override
746    public boolean isCheckedOut() {
747        if (!isStateLoaded) {
748            if (getCoreSession() == null) {
749                return true;
750            }
751            refresh(REFRESH_STATE, null);
752        }
753        return isCheckedOut;
754    }
755
756    @Override
757    public void checkOut() {
758        getCoreSession().checkOut(ref);
759        isStateLoaded = false;
760        // new version number, refresh content
761        refresh(REFRESH_CONTENT_IF_LOADED, null);
762    }
763
764    @Override
765    public DocumentRef checkIn(VersioningOption option, String description) {
766        DocumentRef versionRef = getCoreSession().checkIn(ref, option, description);
767        isStateLoaded = false;
768        // new version number, refresh content
769        refresh(REFRESH_CONTENT_IF_LOADED, null);
770        return versionRef;
771    }
772
773    @Override
774    public String getVersionLabel() {
775        if (detachedVersionLabel != null) {
776            return detachedVersionLabel;
777        }
778        if (getCoreSession() == null) {
779            return null;
780        }
781        return getCoreSession().getVersionLabel(this);
782    }
783
784    @Override
785    public String getVersionSeriesId() {
786        if (!isStateLoaded) {
787            refresh(REFRESH_STATE, null);
788        }
789        return versionSeriesId;
790    }
791
792    @Override
793    public boolean isLatestVersion() {
794        if (!isStateLoaded) {
795            refresh(REFRESH_STATE, null);
796        }
797        return isLatestVersion;
798    }
799
800    @Override
801    public boolean isMajorVersion() {
802        if (!isStateLoaded) {
803            refresh(REFRESH_STATE, null);
804        }
805        return isMajorVersion;
806    }
807
808    @Override
809    public boolean isLatestMajorVersion() {
810        if (!isStateLoaded) {
811            refresh(REFRESH_STATE, null);
812        }
813        return isLatestMajorVersion;
814    }
815
816    @Override
817    public boolean isVersionSeriesCheckedOut() {
818        if (!isStateLoaded) {
819            refresh(REFRESH_STATE, null);
820        }
821        return isVersionSeriesCheckedOut;
822    }
823
824    @Override
825    public String getCheckinComment() {
826        if (!isStateLoaded) {
827            refresh(REFRESH_STATE, null);
828        }
829        return checkinComment;
830    }
831
832    @Override
833    public ACP getACP() {
834        if (!isACPLoaded) { // lazy load
835            acp = new RunWithCoreSession<ACP>() {
836                @Override
837                public ACP run() {
838                    return session.getACP(ref);
839                }
840            }.execute();
841            isACPLoaded = true;
842        }
843        return acp;
844    }
845
846    @Override
847    public void setACP(final ACP acp, final boolean overwrite) {
848        new RunWithCoreSession<Object>() {
849            @Override
850            public Object run() {
851                session.setACP(ref, acp, overwrite);
852                return null;
853            }
854        }.execute();
855        isACPLoaded = false;
856    }
857
858    @Override
859    public String getType() {
860        return typeName;
861    }
862
863    @Override
864    public void setProperties(String schemaName, Map<String, Object> data) {
865        DataModel dm = getDataModel(schemaName);
866        if (dm != null) {
867            dm.setMap(data);
868            clearPrefetch(schemaName);
869        }
870    }
871
872    @Override
873    public void setProperty(String schemaName, String name, Object value) {
874        DataModel dm = getDataModel(schemaName);
875        if (dm == null) {
876            return;
877        }
878        dm.setData(name, value);
879        clearPrefetch(schemaName);
880    }
881
882    @Override
883    public Path getPath() {
884        return path;
885    }
886
887    @Override
888    public Map<String, DataModel> getDataModels() {
889        return dataModels;
890    }
891
892    @Override
893    public boolean isFolder() {
894        return hasFacet(FacetNames.FOLDERISH);
895    }
896
897    @Override
898    public boolean isVersionable() {
899        return hasFacet(FacetNames.VERSIONABLE);
900    }
901
902    @Override
903    public boolean isDownloadable() {
904        if (hasFacet(FacetNames.DOWNLOADABLE)) {
905            // TODO find a better way to check size that does not depend on the
906            // document schema
907            Long size = (Long) getProperty("common", "size");
908            if (size != null) {
909                return size.longValue() != 0;
910            }
911        }
912        return false;
913    }
914
915    @Override
916    public void accept(PropertyVisitor visitor, Object arg) {
917        for (DocumentPart dp : getParts()) {
918            ((DocumentPartImpl) dp).visitChildren(visitor, arg);
919        }
920    }
921
922    @Override
923    @SuppressWarnings("unchecked")
924    public <T> T getAdapter(Class<T> itf) {
925        T facet = (T) getAdapters().get(itf);
926        if (facet == null) {
927            facet = findAdapter(itf);
928            if (facet != null) {
929                adapters.put(itf, facet);
930            }
931        }
932        return facet;
933    }
934
935    /**
936     * Lazy initialization for adapters because they don't survive the serialization.
937     */
938    private ArrayMap<Class<?>, Object> getAdapters() {
939        if (adapters == null) {
940            adapters = new ArrayMap<Class<?>, Object>();
941        }
942
943        return adapters;
944    }
945
946    @Override
947    public <T> T getAdapter(Class<T> itf, boolean refreshCache) {
948        T facet;
949
950        if (!refreshCache) {
951            facet = getAdapter(itf);
952        } else {
953            facet = findAdapter(itf);
954        }
955
956        if (facet != null) {
957            getAdapters().put(itf, facet);
958        }
959        return facet;
960    }
961
962    @SuppressWarnings("unchecked")
963    private <T> T findAdapter(Class<T> itf) {
964        DocumentAdapterService svc = Framework.getService(DocumentAdapterService.class);
965        if (svc != null) {
966            DocumentAdapterDescriptor dae = svc.getAdapterDescriptor(itf);
967            if (dae != null) {
968                String facet = dae.getFacet();
969                if (facet == null) {
970                    // if no facet is specified, accept the adapter
971                    return (T) dae.getFactory().getAdapter(this, itf);
972                } else if (hasFacet(facet)) {
973                    return (T) dae.getFactory().getAdapter(this, itf);
974                } else {
975                    // TODO: throw an exception
976                    log.error("Document model cannot be adapted to " + itf + " because it has no facet " + facet);
977                }
978            }
979        } else {
980            log.warn("DocumentAdapterService not available. Cannot get document model adaptor for " + itf);
981        }
982        return null;
983    }
984
985    @Override
986    public boolean followTransition(final String transition) {
987        boolean res = new RunWithCoreSession<Boolean>() {
988            @Override
989            public Boolean run() {
990                return Boolean.valueOf(session.followTransition(ref, transition));
991            }
992        }.execute().booleanValue();
993        // Invalidate the prefetched value in this case.
994        if (res) {
995            currentLifeCycleState = null;
996        }
997        return res;
998    }
999
1000    @Override
1001    public Collection<String> getAllowedStateTransitions() {
1002        return new RunWithCoreSession<Collection<String>>() {
1003            @Override
1004            public Collection<String> run() {
1005                return session.getAllowedStateTransitions(ref);
1006            }
1007        }.execute();
1008    }
1009
1010    @Override
1011    public String getCurrentLifeCycleState() {
1012        if (currentLifeCycleState != null) {
1013            return currentLifeCycleState;
1014        }
1015        // document was just created => not life cycle yet
1016        if (sid == null) {
1017            return null;
1018        }
1019        currentLifeCycleState = new RunWithCoreSession<String>() {
1020            @Override
1021            public String run() {
1022                return session.getCurrentLifeCycleState(ref);
1023            }
1024        }.execute();
1025        return currentLifeCycleState;
1026    }
1027
1028    @Override
1029    public String getLifeCyclePolicy() {
1030        if (lifeCyclePolicy != null) {
1031            return lifeCyclePolicy;
1032        }
1033        // String lifeCyclePolicy = null;
1034        lifeCyclePolicy = new RunWithCoreSession<String>() {
1035            @Override
1036            public String run() {
1037                return session.getLifeCyclePolicy(ref);
1038            }
1039        }.execute();
1040        return lifeCyclePolicy;
1041    }
1042
1043    @Override
1044    public boolean isVersion() {
1045        return (flags & F_VERSION) != 0;
1046    }
1047
1048    @Override
1049    public boolean isProxy() {
1050        return (flags & F_PROXY) != 0;
1051    }
1052
1053    @Override
1054    public boolean isImmutable() {
1055        return (flags & F_IMMUTABLE) != 0;
1056    }
1057
1058    public void setIsVersion(boolean isVersion) {
1059        if (isVersion) {
1060            flags |= F_VERSION;
1061        } else {
1062            flags &= ~F_VERSION;
1063        }
1064    }
1065
1066    public void setIsProxy(boolean isProxy) {
1067        if (isProxy) {
1068            flags |= F_PROXY;
1069        } else {
1070            flags &= ~F_PROXY;
1071        }
1072    }
1073
1074    public void setIsImmutable(boolean isImmutable) {
1075        if (isImmutable) {
1076            flags |= F_IMMUTABLE;
1077        } else {
1078            flags &= ~F_IMMUTABLE;
1079        }
1080    }
1081
1082    @Override
1083    public boolean isDirty() {
1084        for (DataModel dm : dataModels.values()) {
1085            DocumentPart part = ((DataModelImpl) dm).getDocumentPart();
1086            if (part.isDirty()) {
1087                return true;
1088            }
1089        }
1090        return false;
1091    }
1092
1093    @Override
1094    public ScopedMap getContextData() {
1095        return contextData;
1096    }
1097
1098    @Override
1099    public Serializable getContextData(ScopeType scope, String key) {
1100        return contextData.getScopedValue(scope, key);
1101    }
1102
1103    @Override
1104    public void putContextData(ScopeType scope, String key, Serializable value) {
1105        contextData.putScopedValue(scope, key, value);
1106    }
1107
1108    @Override
1109    public Serializable getContextData(String key) {
1110        return contextData.getScopedValue(key);
1111    }
1112
1113    @Override
1114    public void putContextData(String key, Serializable value) {
1115        contextData.putScopedValue(key, value);
1116    }
1117
1118    @Override
1119    public void copyContextData(DocumentModel otherDocument) {
1120        ScopedMap otherMap = otherDocument.getContextData();
1121        if (otherMap != null) {
1122            contextData.putAll(otherMap);
1123        }
1124    }
1125
1126    @Override
1127    public void copyContent(DocumentModel sourceDoc) {
1128        computeFacetsAndSchemas(((DocumentModelImpl) sourceDoc).instanceFacets);
1129        Map<String, DataModel> newDataModels = new HashMap<>();
1130        for (String key : schemas) {
1131            DataModel oldDM = sourceDoc.getDataModel(key);
1132            DataModel newDM;
1133            if (oldDM != null) {
1134                newDM = cloneDataModel(oldDM);
1135            } else {
1136                // create an empty datamodel
1137                Schema schema = Framework.getService(SchemaManager.class).getSchema(key);
1138                newDM = new DataModelImpl(new DocumentPartImpl(schema));
1139            }
1140            newDataModels.put(key, newDM);
1141        }
1142        dataModels = newDataModels;
1143    }
1144
1145    @SuppressWarnings("unchecked")
1146    public static Object cloneField(Field field, String key, Object value) {
1147        // key is unused
1148        Object clone;
1149        Type type = field.getType();
1150        if (type.isSimpleType()) {
1151            // CLONE TODO
1152            if (value instanceof Calendar) {
1153                Calendar newValue = (Calendar) value;
1154                clone = newValue.clone();
1155            } else {
1156                clone = value;
1157            }
1158        } else if (type.isListType()) {
1159            ListType ltype = (ListType) type;
1160            Field lfield = ltype.getField();
1161            Type ftype = lfield.getType();
1162            List<Object> list;
1163            if (value instanceof Object[]) { // these are stored as arrays
1164                list = Arrays.asList((Object[]) value);
1165            } else {
1166                list = (List<Object>) value;
1167            }
1168            if (ftype.isComplexType()) {
1169                List<Object> clonedList = new ArrayList<Object>(list.size());
1170                for (Object o : list) {
1171                    clonedList.add(cloneField(lfield, null, o));
1172                }
1173                clone = clonedList;
1174            } else {
1175                Class<?> klass = JavaTypes.getClass(ftype);
1176                if (klass.isPrimitive()) {
1177                    clone = PrimitiveArrays.toPrimitiveArray(list, klass);
1178                } else {
1179                    clone = list.toArray((Object[]) Array.newInstance(klass, list.size()));
1180                }
1181            }
1182        } else {
1183            // complex type
1184            ComplexType ctype = (ComplexType) type;
1185            if (TypeConstants.isContentType(ctype)) { // if a blob
1186                Blob blob = (Blob) value; // TODO
1187                clone = blob;
1188            } else {
1189                // a map, regular complex type
1190                Map<String, Object> map = (Map<String, Object>) value;
1191                Map<String, Object> clonedMap = new HashMap<String, Object>();
1192                for (Map.Entry<String, Object> entry : map.entrySet()) {
1193                    Object v = entry.getValue();
1194                    String k = entry.getKey();
1195                    if (v == null) {
1196                        continue;
1197                    }
1198                    clonedMap.put(k, cloneField(ctype.getField(k), k, v));
1199                }
1200                clone = clonedMap;
1201            }
1202        }
1203        return clone;
1204    }
1205
1206    public static DataModel cloneDataModel(Schema schema, DataModel data) {
1207        DataModel dm = new DataModelImpl(schema.getName());
1208        for (Field field : schema.getFields()) {
1209            String key = field.getName().getLocalName();
1210            Object value;
1211            try {
1212                value = data.getData(key);
1213            } catch (PropertyException e1) {
1214                continue;
1215            }
1216            if (value == null) {
1217                continue;
1218            }
1219            Object clone = cloneField(field, key, value);
1220            dm.setData(key, clone);
1221        }
1222        return dm;
1223    }
1224
1225    public DataModel cloneDataModel(DataModel data) {
1226        TypeProvider typeProvider = Framework.getLocalService(SchemaManager.class);
1227        return cloneDataModel(typeProvider.getSchema(data.getSchema()), data);
1228    }
1229
1230    @Override
1231    public String getCacheKey() {
1232        // UUID - sessionId
1233        String key = id + '-' + sid + '-' + getPathAsString();
1234        // assume the doc holds the dublincore schema (enough for us right now)
1235        if (hasSchema("dublincore")) {
1236            Calendar timeStamp = (Calendar) getProperty("dublincore", "modified");
1237            if (timeStamp != null) {
1238                // remove milliseconds as they are not stored in some
1239                // databases, which could make the comparison fail just after a
1240                // document creation (see NXP-8783)
1241                timeStamp.set(Calendar.MILLISECOND, 0);
1242                key += '-' + String.valueOf(timeStamp.getTimeInMillis());
1243            }
1244        }
1245        return key;
1246    }
1247
1248    @Override
1249    public String getRepositoryName() {
1250        return repositoryName;
1251    }
1252
1253    @Override
1254    public String getSourceId() {
1255        return sourceId;
1256    }
1257
1258    public boolean isSchemaLoaded(String name) {
1259        return dataModels.containsKey(name);
1260    }
1261
1262    @Override
1263    public boolean isPrefetched(String xpath) {
1264        return prefetch != null && prefetch.isPrefetched(xpath);
1265    }
1266
1267    @Override
1268    public boolean isPrefetched(String schemaName, String name) {
1269        return prefetch != null && prefetch.isPrefetched(schemaName, name);
1270    }
1271
1272    /**
1273     * Sets prefetch information.
1274     * <p>
1275     * INTERNAL: This method is not in the public interface.
1276     *
1277     * @since 5.5
1278     */
1279    public void setPrefetch(Prefetch prefetch) {
1280        this.prefetch = prefetch;
1281    }
1282
1283    @Override
1284    public void prefetchCurrentLifecycleState(String lifecycle) {
1285        currentLifeCycleState = lifecycle;
1286    }
1287
1288    @Override
1289    public void prefetchLifeCyclePolicy(String lifeCyclePolicy) {
1290        this.lifeCyclePolicy = lifeCyclePolicy;
1291    }
1292
1293    @Override
1294    // need this for tree in RCP clients
1295    public boolean equals(Object obj) {
1296        if (obj == this) {
1297            return true;
1298        }
1299        if (obj instanceof DocumentModelImpl) {
1300            DocumentModel documentModel = (DocumentModel) obj;
1301            String id = documentModel.getId();
1302            if (id != null) {
1303                return id.equals(this.id);
1304            }
1305        }
1306        return false;
1307    }
1308
1309    @Override
1310    public int hashCode() {
1311        return id == null ? 0 : id.hashCode();
1312    }
1313
1314    @Override
1315    public String toString() {
1316        String title = id;
1317        if (getDataModels().containsKey("dublincore")) {
1318            title = getTitle();
1319        }
1320        return getClass().getSimpleName() + '(' + id + ", path=" + path + ", title=" + title + ')';
1321    }
1322
1323    @Override
1324    public <T extends Serializable> T getSystemProp(final String systemProperty, final Class<T> type) {
1325        return new RunWithCoreSession<T>() {
1326            @Override
1327            public T run() {
1328                return session.getDocumentSystemProp(ref, systemProperty, type);
1329            }
1330        }.execute();
1331    }
1332
1333    @Override
1334    public boolean isLifeCycleLoaded() {
1335        return currentLifeCycleState != null;
1336    }
1337
1338    @Override
1339    public DocumentPart getPart(String schema) {
1340        DataModel dm = getDataModel(schema);
1341        if (dm != null) {
1342            return ((DataModelImpl) dm).getDocumentPart();
1343        }
1344        return null; // TODO thrown an exception?
1345    }
1346
1347    @Override
1348    public DocumentPart[] getParts() {
1349        // DocumentType type = getDocumentType();
1350        // type = Framework.getService(SchemaManager.class).getDocumentType(
1351        // getType());
1352        // Collection<Schema> schemas = type.getSchemas();
1353        // Set<String> allSchemas = getAllSchemas();
1354        DocumentPart[] parts = new DocumentPart[schemas.size()];
1355        int i = 0;
1356        for (String schema : schemas) {
1357            DataModel dm = getDataModel(schema);
1358            parts[i++] = ((DataModelImpl) dm).getDocumentPart();
1359        }
1360        return parts;
1361    }
1362
1363    @Override
1364    public Collection<Property> getPropertyObjects(String schema) {
1365        DocumentPart part = getPart(schema);
1366        return part == null ? Collections.emptyList() : part.getChildren();
1367    }
1368
1369    @Override
1370    public Property getProperty(String xpath) {
1371        if (xpath == null) {
1372            throw new PropertyNotFoundException("null", "Invalid null xpath");
1373        }
1374        String cxpath = canonicalXPath(xpath);
1375        if (cxpath.isEmpty()) {
1376            throw new PropertyNotFoundException(xpath, "Schema not specified");
1377        }
1378        String schemaName = getXPathSchemaName(cxpath, schemas, null);
1379        if (schemaName == null) {
1380            if (cxpath.indexOf(':') != -1) {
1381                throw new PropertyNotFoundException(xpath, "No such schema");
1382            } else {
1383                throw new PropertyNotFoundException(xpath);
1384            }
1385
1386        }
1387        DocumentPart part = getPart(schemaName);
1388        if (part == null) {
1389            throw new PropertyNotFoundException(xpath);
1390        }
1391        // cut prefix
1392        String partPath = cxpath.substring(cxpath.indexOf(':') + 1);
1393        try {
1394            return part.resolvePath(partPath);
1395        } catch (PropertyNotFoundException e) {
1396            throw new PropertyNotFoundException(xpath, e.getDetail());
1397        }
1398    }
1399
1400    public static String getXPathSchemaName(String xpath, Set<String> docSchemas, String[] returnName) {
1401        SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
1402        // find first segment
1403        int i = xpath.indexOf('/');
1404        String prop = i == -1 ? xpath : xpath.substring(0, i);
1405        int p = prop.indexOf(':');
1406        if (p != -1) {
1407            // prefixed
1408            String prefix = prop.substring(0, p);
1409            Schema schema = schemaManager.getSchemaFromPrefix(prefix);
1410            if (schema == null) {
1411                // try directly with prefix as a schema name
1412                schema = schemaManager.getSchema(prefix);
1413                if (schema == null) {
1414                    return null;
1415                }
1416            }
1417            if (returnName != null) {
1418                returnName[0] = prop.substring(p + 1);
1419            }
1420            return schema.getName();
1421        } else {
1422            // unprefixed
1423            // search for the first matching schema having a property
1424            // with the same name as the first path segment
1425            for (String schemaName : docSchemas) {
1426                Schema schema = schemaManager.getSchema(schemaName);
1427                if (schema != null && schema.hasField(prop)) {
1428                    if (returnName != null) {
1429                        returnName[0] = prop;
1430                    }
1431                    return schema.getName();
1432                }
1433            }
1434            return null;
1435        }
1436    }
1437
1438    @Override
1439    public Serializable getPropertyValue(String xpath) throws PropertyException {
1440        if (prefetch != null) {
1441            Serializable value = prefetch.get(xpath);
1442            if (value != NULL) {
1443                return value;
1444            }
1445        }
1446        return getProperty(xpath).getValue();
1447    }
1448
1449    @Override
1450    public void setPropertyValue(String xpath, Serializable value) throws PropertyException {
1451        getProperty(xpath).setValue(value);
1452        clearPrefetchXPath(xpath);
1453    }
1454
1455    private void clearPrefetch(String schemaName) {
1456        if (prefetch != null) {
1457            prefetch.clearPrefetch(schemaName);
1458            if (prefetch.isEmpty()) {
1459                prefetch = null;
1460            }
1461        }
1462    }
1463
1464    protected void clearPrefetchXPath(String xpath) {
1465        if (prefetch != null) {
1466            String schemaName = prefetch.getXPathSchema(xpath, getDocumentType());
1467            if (schemaName != null) {
1468                clearPrefetch(schemaName);
1469            }
1470        }
1471    }
1472
1473    @Override
1474    public DocumentModel clone() throws CloneNotSupportedException {
1475        DocumentModelImpl dm = (DocumentModelImpl) super.clone();
1476        // dm.id =id;
1477        // dm.acp = acp;
1478        // dm.currentLifeCycleState = currentLifeCycleState;
1479        // dm.lifeCyclePolicy = lifeCyclePolicy;
1480        // dm.declaredSchemas = declaredSchemas; // schemas are immutable so we
1481        // don't clone the array
1482        // dm.flags = flags;
1483        // dm.repositoryName = repositoryName;
1484        // dm.ref = ref;
1485        // dm.parentRef = parentRef;
1486        // dm.path = path; // path is immutable
1487        // dm.isACPLoaded = isACPLoaded;
1488        // dm.prefetch = dm.prefetch; // prefetch can be shared
1489        // dm.lock = lock;
1490        // dm.sourceId =sourceId;
1491        // dm.sid = sid;
1492        // dm.type = type;
1493        dm.facets = new HashSet<String>(facets); // facets
1494        // should be
1495        // clones too -
1496        // they are not
1497        // immutable
1498        // context data is keeping contextual info so it is reseted
1499        dm.contextData = new ScopedMap();
1500
1501        // copy parts
1502        dm.dataModels = new HashMap<>();
1503        for (Map.Entry<String, DataModel> entry : dataModels.entrySet()) {
1504            String key = entry.getKey();
1505            DataModel data = entry.getValue();
1506            DataModelImpl newData = new DataModelImpl(key, data.getMap());
1507            for (String name:data.getDirtyFields()) {
1508                newData.setDirty(name);
1509            }
1510            dm.dataModels.put(key, newData);
1511        }
1512        return dm;
1513    }
1514
1515    @Override
1516    public void reset() {
1517        if (dataModels != null) {
1518            dataModels.clear();
1519        }
1520        prefetch = null;
1521        isACPLoaded = false;
1522        acp = null;
1523        currentLifeCycleState = null;
1524        lifeCyclePolicy = null;
1525    }
1526
1527    @Override
1528    public void refresh() {
1529        detachedVersionLabel = null;
1530
1531        refresh(REFRESH_DEFAULT, null);
1532    }
1533
1534    @Override
1535    public void refresh(int refreshFlags, String[] schemas) {
1536        if (id == null) {
1537            // not yet saved
1538            return;
1539        }
1540        if ((refreshFlags & REFRESH_ACP_IF_LOADED) != 0 && isACPLoaded) {
1541            refreshFlags |= REFRESH_ACP;
1542            // we must not clean the REFRESH_ACP_IF_LOADED flag since it is
1543            // used
1544            // below on the client
1545        }
1546
1547        if ((refreshFlags & REFRESH_CONTENT_IF_LOADED) != 0) {
1548            refreshFlags |= REFRESH_CONTENT;
1549            Collection<String> keys = dataModels.keySet();
1550            schemas = keys.toArray(new String[keys.size()]);
1551        }
1552
1553        DocumentModelRefresh refresh = getCoreSession().refreshDocument(ref, refreshFlags, schemas);
1554
1555        if ((refreshFlags & REFRESH_PREFETCH) != 0) {
1556            prefetch = refresh.prefetch;
1557        }
1558        if ((refreshFlags & REFRESH_STATE) != 0) {
1559            currentLifeCycleState = refresh.lifeCycleState;
1560            lifeCyclePolicy = refresh.lifeCyclePolicy;
1561            isCheckedOut = refresh.isCheckedOut;
1562            isLatestVersion = refresh.isLatestVersion;
1563            isMajorVersion = refresh.isMajorVersion;
1564            isLatestMajorVersion = refresh.isLatestMajorVersion;
1565            isVersionSeriesCheckedOut = refresh.isVersionSeriesCheckedOut;
1566            versionSeriesId = refresh.versionSeriesId;
1567            checkinComment = refresh.checkinComment;
1568            isStateLoaded = true;
1569        }
1570        acp = null;
1571        isACPLoaded = false;
1572        if ((refreshFlags & REFRESH_ACP) != 0) {
1573            acp = refresh.acp;
1574            isACPLoaded = true;
1575        }
1576
1577        if ((refreshFlags & (REFRESH_CONTENT | REFRESH_CONTENT_LAZY)) != 0) {
1578            dataModels.clear();
1579            computeFacetsAndSchemas(refresh.instanceFacets);
1580        }
1581        if ((refreshFlags & REFRESH_CONTENT) != 0) {
1582            DocumentPart[] parts = refresh.documentParts;
1583            if (parts != null) {
1584                for (DocumentPart part : parts) {
1585                    DataModelImpl dm = new DataModelImpl(part);
1586                    dataModels.put(dm.getSchema(), dm);
1587                }
1588            }
1589        }
1590    }
1591
1592    /**
1593     * Recomputes all facets and schemas from the instance facets.
1594     *
1595     * @since 7.1
1596     */
1597    protected void computeFacetsAndSchemas(Set<String> instanceFacets) {
1598        this.instanceFacets = instanceFacets;
1599        instanceFacetsOrig = new HashSet<>(instanceFacets);
1600        facets = new HashSet<>(instanceFacets);
1601        facets.addAll(getDocumentType().getFacets());
1602        if (isImmutable()) {
1603            facets.add(FacetNames.IMMUTABLE);
1604        }
1605        schemas = computeSchemas(getDocumentType(), instanceFacets, isProxy());
1606        schemasOrig = new HashSet<>(schemas);
1607    }
1608
1609    @Override
1610    public String getChangeToken() {
1611        if (!hasSchema("dublincore")) {
1612            return null;
1613        }
1614        try {
1615            Calendar modified = (Calendar) getPropertyValue("dc:modified");
1616            if (modified != null) {
1617                return String.valueOf(modified.getTimeInMillis());
1618            }
1619        } catch (PropertyException e) {
1620            log.error("Error while retrieving dc:modified", e);
1621        }
1622        return null;
1623    }
1624
1625    /**
1626     * Sets the document id. May be useful when detaching from a repo and attaching to another one or when unmarshalling
1627     * a documentModel from a XML or JSON representation
1628     *
1629     * @param id
1630     * @since 5.7.2
1631     */
1632    public void setId(String id) {
1633        this.id = id;
1634    }
1635
1636    @Override
1637    public Map<String, String> getBinaryFulltext() {
1638        CoreSession session = getCoreSession();
1639        if (session == null) {
1640            return null;
1641        }
1642        return session.getBinaryFulltext(ref);
1643    }
1644
1645    @Override
1646    public PropertyObjectResolver getObjectResolver(String xpath) {
1647        return DocumentPropertyObjectResolverImpl.create(this, xpath);
1648    }
1649
1650    /**
1651     * Replace the content by it's the reference if the document is live and not dirty.
1652     *
1653     * @see org.nuxeo.ecm.core.event.EventContext
1654     * @since 7.10
1655     */
1656    private Object writeReplace() throws ObjectStreamException {
1657        if (!TransactionHelper.isTransactionActive()) { // protect from no transaction
1658            Transaction tx = TransactionHelper.suspendTransaction();
1659            try {
1660                TransactionHelper.startTransaction();
1661                try {
1662                    return writeReplace();
1663                } finally {
1664                    TransactionHelper.commitOrRollbackTransaction();
1665                }
1666            } finally {
1667                if (tx != null) {
1668                    TransactionHelper.resumeTransaction(tx);
1669                }
1670            }
1671        }
1672        if (isDirty()) {
1673            return this;
1674        }
1675        CoreSession session = getCoreSession();
1676        if (session == null) {
1677            return this;
1678        }
1679        if (!session.exists(ref)) {
1680            return this;
1681        }
1682        return new InstanceRef(this, session.getPrincipal());
1683    }
1684
1685    /**
1686     * Legacy code: Explicitly detach the document to send the document as an event context parameter.
1687     *
1688     * @see org.nuxeo.ecm.core.event.EventContext
1689     * @since 7.10
1690     */
1691    private void writeObject(ObjectOutputStream stream) throws IOException {
1692        CoreSession session = getCoreSession();
1693        detach(session != null && ref != null && session.exists(ref));
1694        stream.defaultWriteObject();
1695    }
1696}