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