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