001/*
002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.core.storage.dbs;
020
021import static java.lang.Boolean.TRUE;
022
023import java.io.Serializable;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.Calendar;
027import java.util.Collection;
028import java.util.Collections;
029import java.util.HashMap;
030import java.util.HashSet;
031import java.util.LinkedList;
032import java.util.List;
033import java.util.Map;
034import java.util.Set;
035import java.util.function.Consumer;
036
037import org.apache.commons.lang.StringUtils;
038import org.nuxeo.ecm.core.NXCore;
039import org.nuxeo.ecm.core.api.DocumentNotFoundException;
040import org.nuxeo.ecm.core.api.LifeCycleException;
041import org.nuxeo.ecm.core.api.Lock;
042import org.nuxeo.ecm.core.api.NuxeoException;
043import org.nuxeo.ecm.core.api.PropertyException;
044import org.nuxeo.ecm.core.api.model.DocumentPart;
045import org.nuxeo.ecm.core.api.model.Property;
046import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
047import org.nuxeo.ecm.core.api.model.ReadOnlyPropertyException;
048import org.nuxeo.ecm.core.api.model.impl.ComplexProperty;
049import org.nuxeo.ecm.core.blob.BlobManager;
050import org.nuxeo.ecm.core.lifecycle.LifeCycle;
051import org.nuxeo.ecm.core.lifecycle.LifeCycleService;
052import org.nuxeo.ecm.core.model.Document;
053import org.nuxeo.ecm.core.model.LockManager;
054import org.nuxeo.ecm.core.model.Session;
055import org.nuxeo.ecm.core.schema.DocumentType;
056import org.nuxeo.ecm.core.schema.SchemaManager;
057import org.nuxeo.ecm.core.schema.types.ComplexType;
058import org.nuxeo.ecm.core.schema.types.CompositeType;
059import org.nuxeo.ecm.core.schema.types.Field;
060import org.nuxeo.ecm.core.schema.types.Schema;
061import org.nuxeo.ecm.core.schema.types.Type;
062import org.nuxeo.ecm.core.storage.BaseDocument;
063import org.nuxeo.ecm.core.storage.State;
064import org.nuxeo.ecm.core.storage.sql.coremodel.SQLDocumentVersion.VersionNotModifiableException;
065import org.nuxeo.runtime.api.Framework;
066
067/**
068 * Implementation of a {@link Document} for Document-Based Storage. The document is stored as a JSON-like Map. The keys
069 * of the Map are the property names (including special names for system properties), and the values Map are
070 * Serializable values, either:
071 * <ul>
072 * <li>a scalar (String, Long, Double, Boolean, Calendar, Binary),
073 * <li>an array of scalars,
074 * <li>a List of Maps, recursively,
075 * <li>or another Map, recursively.
076 * </ul>
077 * An ACP value is stored as a list of maps. Each map has a keys for the ACL name and the actual ACL which is a list of
078 * ACEs. An ACE is a map having as keys username, permission, and grant.
079 *
080 * @since 5.9.4
081 */
082public class DBSDocument extends BaseDocument<State> {
083
084    private static final Long ZERO = Long.valueOf(0);
085
086    public static final String SYSPROP_FULLTEXT_SIMPLE = "fulltextSimple";
087
088    public static final String SYSPROP_FULLTEXT_BINARY = "fulltextBinary";
089
090    public static final String SYSPROP_FULLTEXT_JOBID = "fulltextJobId";
091
092    public static final String KEY_PREFIX = "ecm:";
093
094    public static final String KEY_ID = "ecm:id";
095
096    public static final String KEY_PARENT_ID = "ecm:parentId";
097
098    public static final String KEY_ANCESTOR_IDS = "ecm:ancestorIds";
099
100    public static final String KEY_PRIMARY_TYPE = "ecm:primaryType";
101
102    public static final String KEY_MIXIN_TYPES = "ecm:mixinTypes";
103
104    public static final String KEY_NAME = "ecm:name";
105
106    public static final String KEY_POS = "ecm:pos";
107
108    public static final String KEY_ACP = "ecm:acp";
109
110    public static final String KEY_ACL_NAME = "name";
111
112    public static final String KEY_PATH_INTERNAL = "ecm:__path";
113
114    public static final String KEY_ACL = "acl";
115
116    public static final String KEY_ACE_USER = "user";
117
118    public static final String KEY_ACE_PERMISSION = "perm";
119
120    public static final String KEY_ACE_GRANT = "grant";
121
122    public static final String KEY_ACE_CREATOR = "creator";
123
124    public static final String KEY_ACE_BEGIN = "begin";
125
126    public static final String KEY_ACE_END = "end";
127
128    public static final String KEY_ACE_STATUS = "status";
129
130    public static final String KEY_READ_ACL = "ecm:racl";
131
132    public static final String KEY_IS_CHECKED_IN = "ecm:isCheckedIn";
133
134    public static final String KEY_IS_VERSION = "ecm:isVersion";
135
136    public static final String KEY_IS_LATEST_VERSION = "ecm:isLatestVersion";
137
138    public static final String KEY_IS_LATEST_MAJOR_VERSION = "ecm:isLatestMajorVersion";
139
140    public static final String KEY_MAJOR_VERSION = "ecm:majorVersion";
141
142    public static final String KEY_MINOR_VERSION = "ecm:minorVersion";
143
144    public static final String KEY_VERSION_SERIES_ID = "ecm:versionSeriesId";
145
146    public static final String KEY_VERSION_CREATED = "ecm:versionCreated";
147
148    public static final String KEY_VERSION_LABEL = "ecm:versionLabel";
149
150    public static final String KEY_VERSION_DESCRIPTION = "ecm:versionDescription";
151
152    public static final String KEY_BASE_VERSION_ID = "ecm:baseVersionId";
153
154    public static final String KEY_IS_PROXY = "ecm:isProxy";
155
156    public static final String KEY_PROXY_TARGET_ID = "ecm:proxyTargetId";
157
158    public static final String KEY_PROXY_VERSION_SERIES_ID = "ecm:proxyVersionSeriesId";
159
160    public static final String KEY_PROXY_IDS = "ecm:proxyIds";
161
162    public static final String KEY_LIFECYCLE_POLICY = "ecm:lifeCyclePolicy";
163
164    public static final String KEY_LIFECYCLE_STATE = "ecm:lifeCycleState";
165
166    public static final String KEY_LOCK_OWNER = "ecm:lockOwner";
167
168    public static final String KEY_LOCK_CREATED = "ecm:lockCreated";
169
170    public static final String KEY_BLOB_NAME = "name";
171
172    public static final String KEY_BLOB_MIME_TYPE = "mime-type";
173
174    public static final String KEY_BLOB_ENCODING = "encoding";
175
176    public static final String KEY_BLOB_DIGEST = "digest";
177
178    public static final String KEY_BLOB_LENGTH = "length";
179
180    public static final String KEY_BLOB_DATA = "data";
181
182    public static final String KEY_FULLTEXT_SIMPLE = "ecm:fulltextSimple";
183
184    public static final String KEY_FULLTEXT_BINARY = "ecm:fulltextBinary";
185
186    public static final String KEY_FULLTEXT_JOBID = "ecm:fulltextJobId";
187
188    public static final String KEY_FULLTEXT_SCORE = "ecm:fulltextScore";
189
190    public static final String APPLICATION_OCTET_STREAM = "application/octet-stream";
191
192    protected final String id;
193
194    protected final DBSDocumentState docState;
195
196    protected final DocumentType type;
197
198    protected final List<Schema> proxySchemas;
199
200    protected final DBSSession session;
201
202    protected boolean readonly;
203
204    protected static final Map<String, String> systemPropNameMap;
205
206    static {
207        systemPropNameMap = new HashMap<String, String>();
208        systemPropNameMap.put(SYSPROP_FULLTEXT_JOBID, KEY_FULLTEXT_JOBID);
209    }
210
211    public DBSDocument(DBSDocumentState docState, DocumentType type, DBSSession session, boolean readonly) {
212        // no state for NullDocument (parent of placeless children)
213        this.id = docState == null ? null : (String) docState.get(KEY_ID);
214        this.docState = docState;
215        this.type = type;
216        this.session = session;
217        if (docState != null && isProxy()) {
218            SchemaManager schemaManager = Framework.getService(SchemaManager.class);
219            proxySchemas = schemaManager.getProxySchemas(type.getName());
220        } else {
221            proxySchemas = null;
222        }
223        this.readonly = readonly;
224    }
225
226    @Override
227    public DocumentType getType() {
228        return type;
229    }
230
231    @Override
232    public Session getSession() {
233        return session;
234    }
235
236    @Override
237    public String getRepositoryName() {
238        return session.getRepositoryName();
239    }
240
241    @Override
242    protected List<Schema> getProxySchemas() {
243        return proxySchemas;
244    }
245
246    @Override
247    public String getUUID() {
248        return id;
249    }
250
251    @Override
252    public String getName() {
253        return docState.getName();
254    }
255
256    @Override
257    public Long getPos() {
258        return (Long) docState.get(KEY_POS);
259    }
260
261    @Override
262    public Document getParent() {
263        if (isVersion()) {
264            Document workingCopy = session.getDocument(getVersionSeriesId());
265            return workingCopy == null ? null : workingCopy.getParent();
266        }
267        String parentId = docState.getParentId();
268        return parentId == null ? null : session.getDocument(parentId);
269    }
270
271    @Override
272    public boolean isProxy() {
273        return TRUE.equals(docState.get(KEY_IS_PROXY));
274    }
275
276    @Override
277    public boolean isVersion() {
278        return TRUE.equals(docState.get(KEY_IS_VERSION));
279    }
280
281    @Override
282    public String getPath() {
283        if (isVersion()) {
284            Document workingCopy = session.getDocument(getVersionSeriesId());
285            return workingCopy == null ? null : workingCopy.getPath();
286        }
287        String name = getName();
288        Document doc = getParent();
289        if (doc == null) {
290            if ("".equals(name)) {
291                return "/"; // root
292            } else {
293                return name; // placeless, no slash
294            }
295        }
296        LinkedList<String> list = new LinkedList<String>();
297        list.addFirst(name);
298        while (doc != null) {
299            list.addFirst(doc.getName());
300            doc = doc.getParent();
301        }
302        return StringUtils.join(list, '/');
303    }
304
305    @Override
306    public Document getChild(String name) {
307        return session.getChild(id, name);
308    }
309
310    @Override
311    public List<Document> getChildren() {
312        if (!isFolder()) {
313            return Collections.emptyList();
314        }
315        return session.getChildren(id);
316    }
317
318    @Override
319    public List<String> getChildrenIds() {
320        if (!isFolder()) {
321            return Collections.emptyList();
322        }
323        return session.getChildrenIds(id);
324    }
325
326    @Override
327    public boolean hasChild(String name) {
328        if (!isFolder()) {
329            return false;
330        }
331        return session.hasChild(id, name);
332    }
333
334    @Override
335    public boolean hasChildren() {
336        if (!isFolder()) {
337            return false;
338        }
339        return session.hasChildren(id);
340    }
341
342    @Override
343    public Document addChild(String name, String typeName) {
344        if (!isFolder()) {
345            throw new IllegalArgumentException("Not a folder");
346        }
347        return session.createChild(null, id, name, null, typeName);
348    }
349
350    @Override
351    public void orderBefore(String src, String dest) {
352        Document srcDoc = getChild(src);
353        if (srcDoc == null) {
354            throw new DocumentNotFoundException("Document " + this + " has no child: " + src);
355        }
356        Document destDoc;
357        if (dest == null) {
358            destDoc = null;
359        } else {
360            destDoc = getChild(dest);
361            if (destDoc == null) {
362                throw new DocumentNotFoundException("Document " + this + " has no child: " + dest);
363            }
364        }
365        session.orderBefore(id, srcDoc.getUUID(), destDoc == null ? null : destDoc.getUUID());
366    }
367
368    // simple property only
369    @Override
370    public Serializable getPropertyValue(String name) {
371        DBSDocumentState docState = getStateOrTarget(name);
372        return docState.get(name);
373    }
374
375    // simple property only
376    @Override
377    public void setPropertyValue(String name, Serializable value) {
378        DBSDocumentState docState = getStateOrTarget(name);
379        docState.put(name, value);
380    }
381
382    // helpers for getValue / setValue
383
384    @Override
385    protected State getChild(State state, String name, Type type) {
386        return (State) state.get(name);
387    }
388
389    @Override
390    protected State getChildForWrite(State state, String name, Type type) throws PropertyException {
391        State child = getChild(state, name, type);
392        if (child == null) {
393            state.put(name, child = new State());
394        }
395        return child;
396    }
397
398    @Override
399    protected List<State> getChildAsList(State state, String name) {
400        @SuppressWarnings("unchecked")
401        List<State> list = (List<State>) state.get(name);
402        if (list == null) {
403            list = new ArrayList<>();
404        }
405        return list;
406    }
407
408    @Override
409    protected void updateList(State state, String name, List<Object> values, Field field) {
410        List<State> childStates = new ArrayList<>(values.size());
411        for (Object v : values) {
412            State childState = new State();
413            setValueComplex(childState, field, v);
414            childStates.add(childState);
415        }
416        state.put(name, (Serializable) childStates);
417    }
418
419    @Override
420    protected List<State> updateList(State state, String name, Property property) throws PropertyException {
421        Collection<Property> properties = property.getChildren();
422        int newSize = properties.size();
423        @SuppressWarnings("unchecked")
424        List<State> childStates = (List<State>) state.get(name);
425        if (childStates == null) {
426            childStates = new ArrayList<>(newSize);
427            state.put(name, (Serializable) childStates);
428        }
429        int oldSize = childStates.size();
430        // remove extra list elements
431        if (oldSize > newSize) {
432            for (int i = oldSize - 1; i >= newSize; i--) {
433                childStates.remove(i);
434            }
435        }
436        // add new list elements
437        if (oldSize < newSize) {
438            for (int i = oldSize; i < newSize; i++) {
439                childStates.add(new State());
440            }
441        }
442        return childStates;
443    }
444
445    @Override
446    public Object getValue(String xpath) throws PropertyException {
447        DBSDocumentState docState = getStateOrTarget(xpath);
448        return getValueObject(docState.getState(), xpath);
449    }
450
451    @Override
452    public void setValue(String xpath, Object value) throws PropertyException {
453        DBSDocumentState docState = getStateOrTarget(xpath);
454        // markDirty has to be called *before* we change the state
455        docState.markDirty();
456        setValueObject(docState.getState(), xpath, value);
457    }
458
459    @Override
460    public void visitBlobs(Consumer<BlobAccessor> blobVisitor) throws PropertyException {
461        if (isProxy()) {
462            getTargetDocument().visitBlobs(blobVisitor);
463            // fall through for proxy schemas
464        }
465        Runnable markDirty = () -> docState.markDirty();
466        visitBlobs(docState.getState(), blobVisitor, markDirty);
467    }
468
469    @Override
470    public Document checkIn(String label, String checkinComment) {
471        if (isProxy()) {
472            return getTargetDocument().checkIn(label, checkinComment);
473        } else if (isVersion()) {
474            throw new VersionNotModifiableException();
475        } else {
476            Document version = session.checkIn(id, label, checkinComment);
477            Framework.getService(BlobManager.class).freezeVersion(version);
478            return version;
479        }
480    }
481
482    @Override
483    public void checkOut() {
484        if (isProxy()) {
485            getTargetDocument().checkOut();
486        } else if (isVersion()) {
487            throw new VersionNotModifiableException();
488        } else {
489            session.checkOut(id);
490        }
491    }
492
493    @Override
494    public List<String> getVersionsIds() {
495        return session.getVersionsIds(getVersionSeriesId());
496    }
497
498    @Override
499    public List<Document> getVersions() {
500        List<String> ids = session.getVersionsIds(getVersionSeriesId());
501        List<Document> versions = new ArrayList<Document>();
502        for (String id : ids) {
503            versions.add(session.getDocument(id));
504        }
505        return versions;
506    }
507
508    @Override
509    public Document getLastVersion() {
510        return session.getLastVersion(getVersionSeriesId());
511    }
512
513    @Override
514    public Document getSourceDocument() {
515        if (isProxy()) {
516            return getTargetDocument();
517        } else if (isVersion()) {
518            return getWorkingCopy();
519        } else {
520            return this;
521        }
522    }
523
524    @Override
525    public void restore(Document version) {
526        if (!version.isVersion()) {
527            throw new NuxeoException("Cannot restore a non-version: " + version);
528        }
529        session.restoreVersion(this, version);
530    }
531
532    @Override
533    public Document getVersion(String label) {
534        DBSDocumentState state = session.getVersionByLabel(getVersionSeriesId(), label);
535        return session.getDocument(state);
536    }
537
538    @Override
539    public Document getBaseVersion() {
540        if (isProxy() || isVersion()) {
541            return null;
542        } else {
543            if (isCheckedOut()) {
544                return null;
545            } else {
546                String id = (String) docState.get(KEY_BASE_VERSION_ID);
547                if (id == null) {
548                    // shouldn't happen
549                    return null;
550                }
551                return session.getDocument(id);
552            }
553        }
554    }
555
556    @Override
557    public boolean isCheckedOut() {
558        if (isProxy()) {
559            return getTargetDocument().isCheckedOut();
560        } else if (isVersion()) {
561            return false;
562        } else {
563            return !TRUE.equals(docState.get(KEY_IS_CHECKED_IN));
564        }
565    }
566
567    @Override
568    public String getVersionSeriesId() {
569        if (isProxy()) {
570            return (String) docState.get(KEY_PROXY_VERSION_SERIES_ID);
571        } else if (isVersion()) {
572            return (String) docState.get(KEY_VERSION_SERIES_ID);
573        } else {
574            return getUUID();
575        }
576    }
577
578    @Override
579    public Calendar getVersionCreationDate() {
580        DBSDocumentState docState = getStateOrTarget();
581        return (Calendar) docState.get(KEY_VERSION_CREATED);
582    }
583
584    @Override
585    public String getVersionLabel() {
586        DBSDocumentState docState = getStateOrTarget();
587        return (String) docState.get(KEY_VERSION_LABEL);
588    }
589
590    @Override
591    public String getCheckinComment() {
592        DBSDocumentState docState = getStateOrTarget();
593        return (String) docState.get(KEY_VERSION_DESCRIPTION);
594    }
595
596    @Override
597    public boolean isLatestVersion() {
598        return isEqualOnVersion(TRUE, KEY_IS_LATEST_VERSION);
599    }
600
601    @Override
602    public boolean isMajorVersion() {
603        return isEqualOnVersion(ZERO, KEY_MINOR_VERSION);
604    }
605
606    @Override
607    public boolean isLatestMajorVersion() {
608        return isEqualOnVersion(TRUE, KEY_IS_LATEST_MAJOR_VERSION);
609    }
610
611    protected boolean isEqualOnVersion(Object ob, String key) {
612        if (isProxy()) {
613            // TODO avoid getting the target just to check if it's a version
614            // use another specific property instead
615            if (getTargetDocument().isVersion()) {
616                return ob.equals(docState.get(key));
617            } else {
618                // if live version, return false
619                return false;
620            }
621        } else if (isVersion()) {
622            return ob.equals(docState.get(key));
623        } else {
624            return false;
625        }
626    }
627
628    @Override
629    public boolean isVersionSeriesCheckedOut() {
630        if (isProxy() || isVersion()) {
631            Document workingCopy = getWorkingCopy();
632            return workingCopy == null ? false : workingCopy.isCheckedOut();
633        } else {
634            return isCheckedOut();
635        }
636    }
637
638    @Override
639    public Document getWorkingCopy() {
640        if (isProxy() || isVersion()) {
641            String versionSeriesId = getVersionSeriesId();
642            return versionSeriesId == null ? null : session.getDocument(versionSeriesId);
643        } else {
644            return this;
645        }
646    }
647
648    @Override
649    public boolean isFolder() {
650        return type == null // null document
651                || type.isFolder();
652    }
653
654    @Override
655    public void setReadOnly(boolean readonly) {
656        this.readonly = readonly;
657    }
658
659    @Override
660    public boolean isReadOnly() {
661        return readonly;
662    }
663
664    @Override
665    public void remove() {
666        session.remove(id);
667    }
668
669    @Override
670    public String getLifeCycleState() {
671        DBSDocumentState docState = getStateOrTarget();
672        return (String) docState.get(KEY_LIFECYCLE_STATE);
673    }
674
675    @Override
676    public void setCurrentLifeCycleState(String lifeCycleState) throws LifeCycleException {
677        DBSDocumentState docState = getStateOrTarget();
678        docState.put(KEY_LIFECYCLE_STATE, lifeCycleState);
679        BlobManager blobManager = Framework.getService(BlobManager.class);
680        blobManager.notifyChanges(this, Collections.singleton(KEY_LIFECYCLE_STATE));
681    }
682
683    @Override
684    public String getLifeCyclePolicy() {
685        DBSDocumentState docState = getStateOrTarget();
686        return (String) docState.get(KEY_LIFECYCLE_POLICY);
687    }
688
689    @Override
690    public void setLifeCyclePolicy(String policy) throws LifeCycleException {
691        DBSDocumentState docState = getStateOrTarget();
692        docState.put(KEY_LIFECYCLE_POLICY, policy);
693        BlobManager blobManager = Framework.getService(BlobManager.class);
694        blobManager.notifyChanges(this, Collections.singleton(KEY_LIFECYCLE_POLICY));
695    }
696
697    // TODO generic
698    @Override
699    public void followTransition(String transition) throws LifeCycleException {
700        LifeCycleService service = NXCore.getLifeCycleService();
701        if (service == null) {
702            throw new LifeCycleException("LifeCycleService not available");
703        }
704        service.followTransition(this, transition);
705    }
706
707    // TODO generic
708    @Override
709    public Collection<String> getAllowedStateTransitions() throws LifeCycleException {
710        LifeCycleService service = NXCore.getLifeCycleService();
711        if (service == null) {
712            throw new LifeCycleException("LifeCycleService not available");
713        }
714        LifeCycle lifeCycle = service.getLifeCycleFor(this);
715        if (lifeCycle == null) {
716            return Collections.emptyList();
717        }
718        return lifeCycle.getAllowedStateTransitionsFrom(getLifeCycleState());
719    }
720
721    @Override
722    public void setSystemProp(String name, Serializable value) {
723        String propertyName;
724        if (name.startsWith(SYSPROP_FULLTEXT_SIMPLE)) {
725            propertyName = name.replace(SYSPROP_FULLTEXT_SIMPLE, KEY_FULLTEXT_SIMPLE);
726        } else if (name.startsWith(SYSPROP_FULLTEXT_BINARY)) {
727            propertyName = name.replace(SYSPROP_FULLTEXT_BINARY, KEY_FULLTEXT_BINARY);
728        } else {
729            propertyName = systemPropNameMap.get(name);
730        }
731        if (propertyName == null) {
732            throw new PropertyNotFoundException(name, "Unknown system property");
733        }
734        setPropertyValue(propertyName, value);
735    }
736
737    @SuppressWarnings("unchecked")
738    @Override
739    public <T extends Serializable> T getSystemProp(String name, Class<T> type) {
740        String propertyName = systemPropNameMap.get(name);
741        if (propertyName == null) {
742            throw new PropertyNotFoundException(name, "Unknown system property: ");
743        }
744        Serializable value = getPropertyValue(propertyName);
745        if (value == null) {
746            if (type == Boolean.class) {
747                value = Boolean.FALSE;
748            } else if (type == Long.class) {
749                value = Long.valueOf(0);
750            }
751        }
752        return (T) value;
753    }
754
755    protected DBSDocumentState getStateOrTarget(Type type) throws PropertyException {
756        return getStateOrTargetForSchema(type.getName());
757    }
758
759    protected DBSDocumentState getStateOrTarget(String xpath) {
760        return getStateOrTargetForSchema(getSchema(xpath));
761    }
762
763    /**
764     * Checks if the given schema should be resolved on the proxy or the target.
765     */
766    protected DBSDocumentState getStateOrTargetForSchema(String schema) {
767        if (isProxy() && !isSchemaForProxy(schema)) {
768            return getTargetDocument().docState;
769        } else {
770            return docState;
771        }
772    }
773
774    /**
775     * Gets the target state if this is a proxy, or the regular state otherwise.
776     */
777    protected DBSDocumentState getStateOrTarget() {
778        if (isProxy()) {
779            return getTargetDocument().docState;
780        } else {
781            return docState;
782        }
783    }
784
785    protected boolean isSchemaForProxy(String schema) {
786        SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
787        return schemaManager.isProxySchema(schema, getType().getName());
788    }
789
790    protected String getSchema(String xpath) {
791        switch (xpath) {
792        case KEY_MAJOR_VERSION:
793        case KEY_MINOR_VERSION:
794        case "major_version":
795        case "minor_version":
796            return "uid";
797        case KEY_FULLTEXT_JOBID:
798        case KEY_LIFECYCLE_POLICY:
799        case KEY_LIFECYCLE_STATE:
800            return "__ecm__";
801        }
802        if (xpath.startsWith(KEY_FULLTEXT_SIMPLE) || xpath.startsWith(KEY_FULLTEXT_BINARY)) {
803            return "__ecm__";
804        }
805        String[] segments = xpath.split("/");
806        String segment = segments[0];
807        Field field = type.getField(segment);
808        if (field == null) {
809            // check facets
810            SchemaManager schemaManager = Framework.getService(SchemaManager.class);
811            for (String facet : getFacets()) {
812                CompositeType facetType = schemaManager.getFacet(facet);
813                field = facetType.getField(segment);
814                if (field != null) {
815                    break;
816                }
817            }
818        }
819        if (field == null && getProxySchemas() != null) {
820            // check proxy schemas
821            for (Schema schema : getProxySchemas()) {
822                field = schema.getField(segment);
823                if (field != null) {
824                    break;
825                }
826            }
827        }
828        if (field == null) {
829            throw new PropertyNotFoundException(xpath);
830        }
831        return field.getDeclaringType().getName();
832    }
833
834    @Override
835    public void readDocumentPart(DocumentPart dp) throws PropertyException {
836        DBSDocumentState docState = getStateOrTarget(dp.getType());
837        readComplexProperty(docState.getState(), (ComplexProperty) dp);
838    }
839
840    @Override
841    protected String internalName(String name) {
842        switch (name) {
843        case "major_version":
844            return KEY_MAJOR_VERSION;
845        case "minor_version":
846            return KEY_MINOR_VERSION;
847        }
848        return name;
849    }
850
851    @Override
852    public Map<String, Serializable> readPrefetch(ComplexType complexType, Set<String> xpaths)
853            throws PropertyException {
854        DBSDocumentState docState = getStateOrTarget(complexType);
855        return readPrefetch(docState.getState(), complexType, xpaths);
856    }
857
858    @Override
859    public boolean writeDocumentPart(DocumentPart dp, WriteContext writeContext) throws PropertyException {
860        DBSDocumentState docState = getStateOrTarget(dp.getType());
861        // markDirty has to be called *before* we change the state
862        docState.markDirty();
863        boolean changed = writeComplexProperty(docState.getState(), (ComplexProperty) dp, writeContext);
864        clearDirtyFlags(dp);
865        return changed;
866    }
867
868    @Override
869    public Set<String> getAllFacets() {
870        Set<String> facets = new HashSet<String>(getType().getFacets());
871        facets.addAll(Arrays.asList(getFacets()));
872        return facets;
873    }
874
875    @Override
876    public String[] getFacets() {
877        DBSDocumentState docState = getStateOrTarget();
878        Object[] mixins = (Object[]) docState.get(KEY_MIXIN_TYPES);
879        if (mixins == null) {
880            return EMPTY_STRING_ARRAY;
881        } else {
882            String[] res = new String[mixins.length];
883            System.arraycopy(mixins, 0, res, 0, mixins.length);
884            return res;
885        }
886    }
887
888    @Override
889    public boolean hasFacet(String facet) {
890        return getAllFacets().contains(facet);
891    }
892
893    @Override
894    public boolean addFacet(String facet) {
895        if (getType().getFacets().contains(facet)) {
896            return false; // already present in type
897        }
898        DBSDocumentState docState = getStateOrTarget();
899        Object[] mixins = (Object[]) docState.get(KEY_MIXIN_TYPES);
900        if (mixins == null) {
901            mixins = new Object[] { facet };
902        } else {
903            List<Object> list = Arrays.asList(mixins);
904            if (list.contains(facet)) {
905                return false; // already present in doc
906            }
907            list = new ArrayList<Object>(list);
908            list.add(facet);
909            mixins = list.toArray(new Object[list.size()]);
910        }
911        docState.put(KEY_MIXIN_TYPES, mixins);
912        return true;
913    }
914
915    @Override
916    public boolean removeFacet(String facet) {
917        DBSDocumentState docState = getStateOrTarget();
918        Object[] mixins = (Object[]) docState.get(KEY_MIXIN_TYPES);
919        if (mixins == null) {
920            return false;
921        }
922        List<Object> list = new ArrayList<Object>(Arrays.asList(mixins));
923        if (!list.remove(facet)) {
924            return false; // not present in doc
925        }
926        mixins = list.toArray(new Object[list.size()]);
927        if (mixins.length == 0) {
928            mixins = null;
929        }
930        docState.put(KEY_MIXIN_TYPES, mixins);
931        // remove the fields belonging to the facet
932        // except for schemas still present due to the primary type or another facet
933        SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
934        CompositeType ft = schemaManager.getFacet(facet);
935        Set<String> otherSchemas = getSchemas(getType(), list);
936        for (Schema schema : ft.getSchemas()) {
937            if (otherSchemas.contains(schema.getName())) {
938                continue;
939            }
940            for (Field field : schema.getFields()) {
941                String name = field.getName().getPrefixedName();
942                if (docState.containsKey(name)) {
943                    docState.put(name, null);
944                }
945            }
946        }
947        return true;
948    }
949
950    protected static Set<String> getSchemas(DocumentType type, List<Object> facets) {
951        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
952        Set<String> schemas = new HashSet<>(Arrays.asList(type.getSchemaNames()));
953        for (Object facet : facets) {
954            CompositeType ft = schemaManager.getFacet((String) facet);
955            if (ft != null) {
956                schemas.addAll(Arrays.asList(ft.getSchemaNames()));
957            }
958        }
959        return schemas;
960    }
961
962    @Override
963    public DBSDocument getTargetDocument() {
964        if (isProxy()) {
965            String targetId = (String) docState.get(KEY_PROXY_TARGET_ID);
966            return session.getDocument(targetId);
967        } else {
968            return null;
969        }
970    }
971
972    @Override
973    public void setTargetDocument(Document target) {
974        if (isProxy()) {
975            if (isReadOnly()) {
976                throw new ReadOnlyPropertyException("Cannot write proxy: " + this);
977            }
978            if (!target.getVersionSeriesId().equals(getVersionSeriesId())) {
979                throw new ReadOnlyPropertyException("Cannot set proxy target to different version series");
980            }
981            session.setProxyTarget(this, target);
982        } else {
983            throw new NuxeoException("Cannot set proxy target on non-proxy");
984        }
985    }
986
987    @Override
988    protected Lock getDocumentLock() {
989        String owner = (String) docState.get(KEY_LOCK_OWNER);
990        if (owner == null) {
991            return null;
992        }
993        Calendar created = (Calendar) docState.get(KEY_LOCK_CREATED);
994        return new Lock(owner, created);
995    }
996
997    @Override
998    protected Lock setDocumentLock(Lock lock) {
999        String owner = (String) docState.get(KEY_LOCK_OWNER);
1000        if (owner != null) {
1001            // return old lock
1002            Calendar created = (Calendar) docState.get(KEY_LOCK_CREATED);
1003            return new Lock(owner, created);
1004        }
1005        docState.put(KEY_LOCK_OWNER, lock.getOwner());
1006        docState.put(KEY_LOCK_CREATED, lock.getCreated());
1007        return null;
1008    }
1009
1010    @Override
1011    protected Lock removeDocumentLock(String owner) {
1012        String oldOwner = (String) docState.get(KEY_LOCK_OWNER);
1013        if (oldOwner == null) {
1014            // no previous lock
1015            return null;
1016        }
1017        Calendar oldCreated = (Calendar) docState.get(KEY_LOCK_CREATED);
1018        if (!LockManager.canLockBeRemoved(oldOwner, owner)) {
1019            // existing mismatched lock, flag failure
1020            return new Lock(oldOwner, oldCreated, true);
1021        }
1022        // remove lock
1023        docState.put(KEY_LOCK_OWNER, null);
1024        docState.put(KEY_LOCK_CREATED, null);
1025        // return old lock
1026        return new Lock(oldOwner, oldCreated);
1027    }
1028
1029    @Override
1030    public String toString() {
1031        return getClass().getSimpleName() + '(' + getName() + ',' + getUUID() + ')';
1032    }
1033
1034    @Override
1035    public boolean equals(Object other) {
1036        if (other == this) {
1037            return true;
1038        }
1039        if (other == null) {
1040            return false;
1041        }
1042        if (other.getClass() == getClass()) {
1043            return equals((DBSDocument) other);
1044        }
1045        return false;
1046    }
1047
1048    private boolean equals(DBSDocument other) {
1049        return id.equals(other.id);
1050    }
1051
1052    @Override
1053    public int hashCode() {
1054        return id.hashCode();
1055    }
1056
1057}