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