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()) {
541            return getTargetDocument().getBaseVersion();
542        } else if (isVersion()) {
543            return null;
544        } else {
545            if (isCheckedOut()) {
546                return null;
547            } else {
548                String id = (String) docState.get(KEY_BASE_VERSION_ID);
549                if (id == null) {
550                    // shouldn't happen
551                    return null;
552                }
553                return session.getDocument(id);
554            }
555        }
556    }
557
558    @Override
559    public boolean isCheckedOut() {
560        if (isProxy()) {
561            return getTargetDocument().isCheckedOut();
562        } else if (isVersion()) {
563            return false;
564        } else {
565            return !TRUE.equals(docState.get(KEY_IS_CHECKED_IN));
566        }
567    }
568
569    @Override
570    public String getVersionSeriesId() {
571        if (isProxy()) {
572            return (String) docState.get(KEY_PROXY_VERSION_SERIES_ID);
573        } else if (isVersion()) {
574            return (String) docState.get(KEY_VERSION_SERIES_ID);
575        } else {
576            return getUUID();
577        }
578    }
579
580    @Override
581    public Calendar getVersionCreationDate() {
582        DBSDocumentState docState = getStateOrTarget();
583        return (Calendar) docState.get(KEY_VERSION_CREATED);
584    }
585
586    @Override
587    public String getVersionLabel() {
588        DBSDocumentState docState = getStateOrTarget();
589        return (String) docState.get(KEY_VERSION_LABEL);
590    }
591
592    @Override
593    public String getCheckinComment() {
594        DBSDocumentState docState = getStateOrTarget();
595        return (String) docState.get(KEY_VERSION_DESCRIPTION);
596    }
597
598    @Override
599    public boolean isLatestVersion() {
600        return isEqualOnVersion(TRUE, KEY_IS_LATEST_VERSION);
601    }
602
603    @Override
604    public boolean isMajorVersion() {
605        return isEqualOnVersion(ZERO, KEY_MINOR_VERSION);
606    }
607
608    @Override
609    public boolean isLatestMajorVersion() {
610        return isEqualOnVersion(TRUE, KEY_IS_LATEST_MAJOR_VERSION);
611    }
612
613    protected boolean isEqualOnVersion(Object ob, String key) {
614        if (isProxy()) {
615            // TODO avoid getting the target just to check if it's a version
616            // use another specific property instead
617            if (getTargetDocument().isVersion()) {
618                return ob.equals(docState.get(key));
619            } else {
620                // if live version, return false
621                return false;
622            }
623        } else if (isVersion()) {
624            return ob.equals(docState.get(key));
625        } else {
626            return false;
627        }
628    }
629
630    @Override
631    public boolean isVersionSeriesCheckedOut() {
632        if (isProxy() || isVersion()) {
633            Document workingCopy = getWorkingCopy();
634            return workingCopy == null ? false : workingCopy.isCheckedOut();
635        } else {
636            return isCheckedOut();
637        }
638    }
639
640    @Override
641    public Document getWorkingCopy() {
642        if (isProxy() || isVersion()) {
643            String versionSeriesId = getVersionSeriesId();
644            return versionSeriesId == null ? null : session.getDocument(versionSeriesId);
645        } else {
646            return this;
647        }
648    }
649
650    @Override
651    public boolean isFolder() {
652        return type == null // null document
653                || type.isFolder();
654    }
655
656    @Override
657    public void setReadOnly(boolean readonly) {
658        this.readonly = readonly;
659    }
660
661    @Override
662    public boolean isReadOnly() {
663        return readonly;
664    }
665
666    @Override
667    public void remove() {
668        session.remove(id);
669    }
670
671    @Override
672    public String getLifeCycleState() {
673        DBSDocumentState docState = getStateOrTarget();
674        return (String) docState.get(KEY_LIFECYCLE_STATE);
675    }
676
677    @Override
678    public void setCurrentLifeCycleState(String lifeCycleState) throws LifeCycleException {
679        DBSDocumentState docState = getStateOrTarget();
680        docState.put(KEY_LIFECYCLE_STATE, lifeCycleState);
681        BlobManager blobManager = Framework.getService(BlobManager.class);
682        blobManager.notifyChanges(this, Collections.singleton(KEY_LIFECYCLE_STATE));
683    }
684
685    @Override
686    public String getLifeCyclePolicy() {
687        DBSDocumentState docState = getStateOrTarget();
688        return (String) docState.get(KEY_LIFECYCLE_POLICY);
689    }
690
691    @Override
692    public void setLifeCyclePolicy(String policy) throws LifeCycleException {
693        DBSDocumentState docState = getStateOrTarget();
694        docState.put(KEY_LIFECYCLE_POLICY, policy);
695        BlobManager blobManager = Framework.getService(BlobManager.class);
696        blobManager.notifyChanges(this, Collections.singleton(KEY_LIFECYCLE_POLICY));
697    }
698
699    // TODO generic
700    @Override
701    public void followTransition(String transition) throws LifeCycleException {
702        LifeCycleService service = NXCore.getLifeCycleService();
703        if (service == null) {
704            throw new LifeCycleException("LifeCycleService not available");
705        }
706        service.followTransition(this, transition);
707    }
708
709    // TODO generic
710    @Override
711    public Collection<String> getAllowedStateTransitions() throws LifeCycleException {
712        LifeCycleService service = NXCore.getLifeCycleService();
713        if (service == null) {
714            throw new LifeCycleException("LifeCycleService not available");
715        }
716        LifeCycle lifeCycle = service.getLifeCycleFor(this);
717        if (lifeCycle == null) {
718            return Collections.emptyList();
719        }
720        return lifeCycle.getAllowedStateTransitionsFrom(getLifeCycleState());
721    }
722
723    @Override
724    public void setSystemProp(String name, Serializable value) {
725        String propertyName;
726        if (name.startsWith(SYSPROP_FULLTEXT_SIMPLE)) {
727            propertyName = name.replace(SYSPROP_FULLTEXT_SIMPLE, KEY_FULLTEXT_SIMPLE);
728        } else if (name.startsWith(SYSPROP_FULLTEXT_BINARY)) {
729            propertyName = name.replace(SYSPROP_FULLTEXT_BINARY, KEY_FULLTEXT_BINARY);
730        } else {
731            propertyName = systemPropNameMap.get(name);
732        }
733        if (propertyName == null) {
734            throw new PropertyNotFoundException(name, "Unknown system property");
735        }
736        setPropertyValue(propertyName, value);
737    }
738
739    @SuppressWarnings("unchecked")
740    @Override
741    public <T extends Serializable> T getSystemProp(String name, Class<T> type) {
742        String propertyName = systemPropNameMap.get(name);
743        if (propertyName == null) {
744            throw new PropertyNotFoundException(name, "Unknown system property: ");
745        }
746        Serializable value = getPropertyValue(propertyName);
747        if (value == null) {
748            if (type == Boolean.class) {
749                value = Boolean.FALSE;
750            } else if (type == Long.class) {
751                value = Long.valueOf(0);
752            }
753        }
754        return (T) value;
755    }
756
757    protected DBSDocumentState getStateOrTarget(Type type) throws PropertyException {
758        return getStateOrTargetForSchema(type.getName());
759    }
760
761    protected DBSDocumentState getStateOrTarget(String xpath) {
762        return getStateOrTargetForSchema(getSchema(xpath));
763    }
764
765    /**
766     * Checks if the given schema should be resolved on the proxy or the target.
767     */
768    protected DBSDocumentState getStateOrTargetForSchema(String schema) {
769        if (isProxy() && !isSchemaForProxy(schema)) {
770            return getTargetDocument().docState;
771        } else {
772            return docState;
773        }
774    }
775
776    /**
777     * Gets the target state if this is a proxy, or the regular state otherwise.
778     */
779    protected DBSDocumentState getStateOrTarget() {
780        if (isProxy()) {
781            return getTargetDocument().docState;
782        } else {
783            return docState;
784        }
785    }
786
787    protected boolean isSchemaForProxy(String schema) {
788        SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
789        return schemaManager.isProxySchema(schema, getType().getName());
790    }
791
792    protected String getSchema(String xpath) {
793        switch (xpath) {
794        case KEY_MAJOR_VERSION:
795        case KEY_MINOR_VERSION:
796        case "major_version":
797        case "minor_version":
798            return "uid";
799        case KEY_FULLTEXT_JOBID:
800        case KEY_LIFECYCLE_POLICY:
801        case KEY_LIFECYCLE_STATE:
802            return "__ecm__";
803        }
804        if (xpath.startsWith(KEY_FULLTEXT_SIMPLE) || xpath.startsWith(KEY_FULLTEXT_BINARY)) {
805            return "__ecm__";
806        }
807        String[] segments = xpath.split("/");
808        String segment = segments[0];
809        Field field = type.getField(segment);
810        if (field == null) {
811            // check facets
812            SchemaManager schemaManager = Framework.getService(SchemaManager.class);
813            for (String facet : getFacets()) {
814                CompositeType facetType = schemaManager.getFacet(facet);
815                field = facetType.getField(segment);
816                if (field != null) {
817                    break;
818                }
819            }
820        }
821        if (field == null && getProxySchemas() != null) {
822            // check proxy schemas
823            for (Schema schema : getProxySchemas()) {
824                field = schema.getField(segment);
825                if (field != null) {
826                    break;
827                }
828            }
829        }
830        if (field == null) {
831            throw new PropertyNotFoundException(xpath);
832        }
833        return field.getDeclaringType().getName();
834    }
835
836    @Override
837    public void readDocumentPart(DocumentPart dp) throws PropertyException {
838        DBSDocumentState docState = getStateOrTarget(dp.getType());
839        readComplexProperty(docState.getState(), (ComplexProperty) dp);
840    }
841
842    @Override
843    protected String internalName(String name) {
844        switch (name) {
845        case "major_version":
846            return KEY_MAJOR_VERSION;
847        case "minor_version":
848            return KEY_MINOR_VERSION;
849        }
850        return name;
851    }
852
853    @Override
854    public Map<String, Serializable> readPrefetch(ComplexType complexType, Set<String> xpaths)
855            throws PropertyException {
856        DBSDocumentState docState = getStateOrTarget(complexType);
857        return readPrefetch(docState.getState(), complexType, xpaths);
858    }
859
860    @Override
861    public boolean writeDocumentPart(DocumentPart dp, WriteContext writeContext) throws PropertyException {
862        DBSDocumentState docState = getStateOrTarget(dp.getType());
863        // markDirty has to be called *before* we change the state
864        docState.markDirty();
865        boolean changed = writeComplexProperty(docState.getState(), (ComplexProperty) dp, writeContext);
866        clearDirtyFlags(dp);
867        return changed;
868    }
869
870    @Override
871    public Set<String> getAllFacets() {
872        Set<String> facets = new HashSet<String>(getType().getFacets());
873        facets.addAll(Arrays.asList(getFacets()));
874        return facets;
875    }
876
877    @Override
878    public String[] getFacets() {
879        DBSDocumentState docState = getStateOrTarget();
880        Object[] mixins = (Object[]) docState.get(KEY_MIXIN_TYPES);
881        if (mixins == null) {
882            return EMPTY_STRING_ARRAY;
883        } else {
884            String[] res = new String[mixins.length];
885            System.arraycopy(mixins, 0, res, 0, mixins.length);
886            return res;
887        }
888    }
889
890    @Override
891    public boolean hasFacet(String facet) {
892        return getAllFacets().contains(facet);
893    }
894
895    @Override
896    public boolean addFacet(String facet) {
897        if (getType().getFacets().contains(facet)) {
898            return false; // already present in type
899        }
900        DBSDocumentState docState = getStateOrTarget();
901        Object[] mixins = (Object[]) docState.get(KEY_MIXIN_TYPES);
902        if (mixins == null) {
903            mixins = new Object[] { facet };
904        } else {
905            List<Object> list = Arrays.asList(mixins);
906            if (list.contains(facet)) {
907                return false; // already present in doc
908            }
909            list = new ArrayList<Object>(list);
910            list.add(facet);
911            mixins = list.toArray(new Object[list.size()]);
912        }
913        docState.put(KEY_MIXIN_TYPES, mixins);
914        return true;
915    }
916
917    @Override
918    public boolean removeFacet(String facet) {
919        DBSDocumentState docState = getStateOrTarget();
920        Object[] mixins = (Object[]) docState.get(KEY_MIXIN_TYPES);
921        if (mixins == null) {
922            return false;
923        }
924        List<Object> list = new ArrayList<Object>(Arrays.asList(mixins));
925        if (!list.remove(facet)) {
926            return false; // not present in doc
927        }
928        mixins = list.toArray(new Object[list.size()]);
929        if (mixins.length == 0) {
930            mixins = null;
931        }
932        docState.put(KEY_MIXIN_TYPES, mixins);
933        // remove the fields belonging to the facet
934        // except for schemas still present due to the primary type or another facet
935        SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
936        CompositeType ft = schemaManager.getFacet(facet);
937        Set<String> otherSchemas = getSchemas(getType(), list);
938        for (Schema schema : ft.getSchemas()) {
939            if (otherSchemas.contains(schema.getName())) {
940                continue;
941            }
942            for (Field field : schema.getFields()) {
943                String name = field.getName().getPrefixedName();
944                if (docState.containsKey(name)) {
945                    docState.put(name, null);
946                }
947            }
948        }
949        return true;
950    }
951
952    protected static Set<String> getSchemas(DocumentType type, List<Object> facets) {
953        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
954        Set<String> schemas = new HashSet<>(Arrays.asList(type.getSchemaNames()));
955        for (Object facet : facets) {
956            CompositeType ft = schemaManager.getFacet((String) facet);
957            if (ft != null) {
958                schemas.addAll(Arrays.asList(ft.getSchemaNames()));
959            }
960        }
961        return schemas;
962    }
963
964    @Override
965    public DBSDocument getTargetDocument() {
966        if (isProxy()) {
967            String targetId = (String) docState.get(KEY_PROXY_TARGET_ID);
968            return session.getDocument(targetId);
969        } else {
970            return null;
971        }
972    }
973
974    @Override
975    public void setTargetDocument(Document target) {
976        if (isProxy()) {
977            if (isReadOnly()) {
978                throw new ReadOnlyPropertyException("Cannot write proxy: " + this);
979            }
980            if (!target.getVersionSeriesId().equals(getVersionSeriesId())) {
981                throw new ReadOnlyPropertyException("Cannot set proxy target to different version series");
982            }
983            session.setProxyTarget(this, target);
984        } else {
985            throw new NuxeoException("Cannot set proxy target on non-proxy");
986        }
987    }
988
989    @Override
990    protected Lock getDocumentLock() {
991        String owner = (String) docState.get(KEY_LOCK_OWNER);
992        if (owner == null) {
993            return null;
994        }
995        Calendar created = (Calendar) docState.get(KEY_LOCK_CREATED);
996        return new Lock(owner, created);
997    }
998
999    @Override
1000    protected Lock setDocumentLock(Lock lock) {
1001        String owner = (String) docState.get(KEY_LOCK_OWNER);
1002        if (owner != null) {
1003            // return old lock
1004            Calendar created = (Calendar) docState.get(KEY_LOCK_CREATED);
1005            return new Lock(owner, created);
1006        }
1007        docState.put(KEY_LOCK_OWNER, lock.getOwner());
1008        docState.put(KEY_LOCK_CREATED, lock.getCreated());
1009        return null;
1010    }
1011
1012    @Override
1013    protected Lock removeDocumentLock(String owner) {
1014        String oldOwner = (String) docState.get(KEY_LOCK_OWNER);
1015        if (oldOwner == null) {
1016            // no previous lock
1017            return null;
1018        }
1019        Calendar oldCreated = (Calendar) docState.get(KEY_LOCK_CREATED);
1020        if (!LockManager.canLockBeRemoved(oldOwner, owner)) {
1021            // existing mismatched lock, flag failure
1022            return new Lock(oldOwner, oldCreated, true);
1023        }
1024        // remove lock
1025        docState.put(KEY_LOCK_OWNER, null);
1026        docState.put(KEY_LOCK_CREATED, null);
1027        // return old lock
1028        return new Lock(oldOwner, oldCreated);
1029    }
1030
1031    @Override
1032    public String toString() {
1033        return getClass().getSimpleName() + '(' + getName() + ',' + getUUID() + ')';
1034    }
1035
1036    @Override
1037    public boolean equals(Object other) {
1038        if (other == this) {
1039            return true;
1040        }
1041        if (other == null) {
1042            return false;
1043        }
1044        if (other.getClass() == getClass()) {
1045            return equals((DBSDocument) other);
1046        }
1047        return false;
1048    }
1049
1050    private boolean equals(DBSDocument other) {
1051        return id.equals(other.id);
1052    }
1053
1054    @Override
1055    public int hashCode() {
1056        return id.hashCode();
1057    }
1058
1059}