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