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