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