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