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