001/*
002 * (C) Copyright 2014-2018 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 *     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.lang3.StringUtils;
038import org.nuxeo.ecm.core.api.DocumentNotFoundException;
039import org.nuxeo.ecm.core.api.LifeCycleException;
040import org.nuxeo.ecm.core.api.Lock;
041import org.nuxeo.ecm.core.api.NuxeoException;
042import org.nuxeo.ecm.core.api.PropertyException;
043import org.nuxeo.ecm.core.api.model.DocumentPart;
044import org.nuxeo.ecm.core.api.model.Property;
045import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
046import org.nuxeo.ecm.core.api.model.ReadOnlyPropertyException;
047import org.nuxeo.ecm.core.api.model.VersionNotModifiableException;
048import org.nuxeo.ecm.core.api.model.impl.ComplexProperty;
049import org.nuxeo.ecm.core.blob.DocumentBlobManager;
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.CompositeType;
058import org.nuxeo.ecm.core.schema.types.Field;
059import org.nuxeo.ecm.core.schema.types.Schema;
060import org.nuxeo.ecm.core.schema.types.Type;
061import org.nuxeo.ecm.core.storage.BaseDocument;
062import org.nuxeo.ecm.core.storage.State;
063import org.nuxeo.runtime.api.Framework;
064
065/**
066 * Implementation of a {@link Document} for Document-Based Storage. The document is stored as a JSON-like Map. The keys
067 * of the Map are the property names (including special names for system properties), and the values Map are
068 * Serializable values, either:
069 * <ul>
070 * <li>a scalar (String, Long, Double, Boolean, Calendar, Binary),
071 * <li>an array of scalars,
072 * <li>a List of Maps, recursively,
073 * <li>or another Map, recursively.
074 * </ul>
075 * 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
076 * ACEs. An ACE is a map having as keys username, permission, and grant.
077 *
078 * @since 5.9.4
079 */
080public class DBSDocument extends BaseDocument<State> {
081
082    private static final Long ZERO = Long.valueOf(0);
083
084    public static final String SYSPROP_FULLTEXT_SIMPLE = "fulltextSimple";
085
086    public static final String SYSPROP_FULLTEXT_BINARY = "fulltextBinary";
087
088    public static final String SYSPROP_FULLTEXT_JOBID = "fulltextJobId";
089
090    public static final String SYSPROP_IS_TRASHED = "isTrashed";
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_RETENTION_ACTIVE = "ecm:isRetentionActive";
133
134    public static final String KEY_IS_CHECKED_IN = "ecm:isCheckedIn";
135
136    public static final String KEY_IS_VERSION = "ecm:isVersion";
137
138    public static final String KEY_IS_LATEST_VERSION = "ecm:isLatestVersion";
139
140    public static final String KEY_IS_LATEST_MAJOR_VERSION = "ecm:isLatestMajorVersion";
141
142    public static final String KEY_MAJOR_VERSION = "ecm:majorVersion";
143
144    public static final String KEY_MINOR_VERSION = "ecm:minorVersion";
145
146    public static final String KEY_VERSION_SERIES_ID = "ecm:versionSeriesId";
147
148    public static final String KEY_VERSION_CREATED = "ecm:versionCreated";
149
150    public static final String KEY_VERSION_LABEL = "ecm:versionLabel";
151
152    public static final String KEY_VERSION_DESCRIPTION = "ecm:versionDescription";
153
154    public static final String KEY_BASE_VERSION_ID = "ecm:baseVersionId";
155
156    public static final String KEY_IS_PROXY = "ecm:isProxy";
157
158    public static final String KEY_PROXY_TARGET_ID = "ecm:proxyTargetId";
159
160    public static final String KEY_PROXY_VERSION_SERIES_ID = "ecm:proxyVersionSeriesId";
161
162    public static final String KEY_PROXY_IDS = "ecm:proxyIds";
163
164    public static final String KEY_LIFECYCLE_POLICY = "ecm:lifeCyclePolicy";
165
166    public static final String KEY_LIFECYCLE_STATE = "ecm:lifeCycleState";
167
168    /**
169     * @since 10.1
170     */
171    public static final String KEY_IS_TRASHED = "ecm:isTrashed";
172
173    public static final String KEY_LOCK_OWNER = "ecm:lockOwner";
174
175    public static final String KEY_LOCK_CREATED = "ecm:lockCreated";
176
177    public static final String KEY_SYS_CHANGE_TOKEN = "ecm:systemChangeToken";
178
179    public static final String KEY_CHANGE_TOKEN = "ecm:changeToken";
180
181    // used instead of ecm:changeToken when change tokens are disabled
182    public static final String KEY_DC_MODIFIED = "dc:modified";
183
184    public static final String KEY_BLOB_NAME = "name";
185
186    public static final String KEY_BLOB_MIME_TYPE = "mime-type";
187
188    public static final String KEY_BLOB_ENCODING = "encoding";
189
190    public static final String KEY_BLOB_DIGEST = "digest";
191
192    public static final String KEY_BLOB_LENGTH = "length";
193
194    public static final String KEY_BLOB_DATA = "data";
195
196    public static final String KEY_FULLTEXT_SIMPLE = "ecm:fulltextSimple";
197
198    public static final String KEY_FULLTEXT_BINARY = "ecm:fulltextBinary";
199
200    public static final String KEY_FULLTEXT_JOBID = "ecm:fulltextJobId";
201
202    public static final String KEY_FULLTEXT_SCORE = "ecm:fulltextScore";
203
204    public static final String APPLICATION_OCTET_STREAM = "application/octet-stream";
205
206    public static final String PROP_UID_MAJOR_VERSION = "uid:major_version";
207
208    public static final String PROP_UID_MINOR_VERSION = "uid:minor_version";
209
210    public static final String PROP_MAJOR_VERSION = "major_version";
211
212    public static final String PROP_MINOR_VERSION = "minor_version";
213
214    /**
215     * @since 9.3
216     */
217    public static final String FACETED_TAG = "nxtag:tags";
218
219    /**
220     * @since 9.3
221     */
222    public static final String FACETED_TAG_LABEL = "label";
223
224    public static final Long INITIAL_SYS_CHANGE_TOKEN = Long.valueOf(0);
225
226    public static final Long INITIAL_CHANGE_TOKEN = Long.valueOf(0);
227
228    protected final String id;
229
230    protected final DBSDocumentState docState;
231
232    protected final DocumentType type;
233
234    protected final List<Schema> proxySchemas;
235
236    protected final DBSSession session;
237
238    protected boolean readonly;
239
240    protected static final Map<String, String> systemPropNameMap;
241
242    static {
243        systemPropNameMap = new HashMap<>();
244        systemPropNameMap.put(SYSPROP_FULLTEXT_JOBID, KEY_FULLTEXT_JOBID);
245        systemPropNameMap.put(SYSPROP_IS_TRASHED, KEY_IS_TRASHED);
246    }
247
248    public DBSDocument(DBSDocumentState docState, DocumentType type, DBSSession session, boolean readonly) {
249        // no state for NullDocument (parent of placeless children)
250        this.id = docState == null ? null : (String) docState.get(KEY_ID);
251        this.docState = docState;
252        this.type = type;
253        this.session = session;
254        if (docState != null && isProxy()) {
255            SchemaManager schemaManager = Framework.getService(SchemaManager.class);
256            proxySchemas = schemaManager.getProxySchemas(type.getName());
257        } else {
258            proxySchemas = null;
259        }
260        this.readonly = readonly;
261    }
262
263    @Override
264    public DocumentType getType() {
265        return type;
266    }
267
268    @Override
269    public Session getSession() {
270        return session;
271    }
272
273    @Override
274    public String getRepositoryName() {
275        return session.getRepositoryName();
276    }
277
278    @Override
279    protected List<Schema> getProxySchemas() {
280        return proxySchemas;
281    }
282
283    @Override
284    public String getUUID() {
285        return id;
286    }
287
288    @Override
289    public String getName() {
290        return docState.getName();
291    }
292
293    @Override
294    public Long getPos() {
295        return (Long) docState.get(KEY_POS);
296    }
297
298    @Override
299    public Document getParent() {
300        if (isVersion()) {
301            Document workingCopy = session.getDocument(getVersionSeriesId());
302            return workingCopy == null ? null : workingCopy.getParent();
303        }
304        String parentId = docState.getParentId();
305        return parentId == null ? null : session.getDocument(parentId);
306    }
307
308    @Override
309    public boolean isProxy() {
310        return TRUE.equals(docState.get(KEY_IS_PROXY));
311    }
312
313    @Override
314    public boolean isVersion() {
315        return TRUE.equals(docState.get(KEY_IS_VERSION));
316    }
317
318    @Override
319    public String getPath() {
320        if (isVersion()) {
321            Document workingCopy = session.getDocument(getVersionSeriesId());
322            return workingCopy == null ? null : workingCopy.getPath();
323        }
324        String name = getName();
325        Document doc = getParent();
326        if (doc == null) {
327            if ("".equals(name)) {
328                return "/"; // root
329            } else {
330                return name; // placeless, no slash
331            }
332        }
333        LinkedList<String> list = new LinkedList<>();
334        list.addFirst(name);
335        while (doc != null) {
336            list.addFirst(doc.getName());
337            doc = doc.getParent();
338        }
339        return StringUtils.join(list, '/');
340    }
341
342    @Override
343    public Document getChild(String name) {
344        return session.getChild(id, name);
345    }
346
347    @Override
348    public List<Document> getChildren() {
349        if (!isFolder()) {
350            return Collections.emptyList();
351        }
352        return session.getChildren(id);
353    }
354
355    @Override
356    public List<String> getChildrenIds() {
357        if (!isFolder()) {
358            return Collections.emptyList();
359        }
360        return session.getChildrenIds(id);
361    }
362
363    @Override
364    public boolean hasChild(String name) {
365        if (!isFolder()) {
366            return false;
367        }
368        return session.hasChild(id, name);
369    }
370
371    @Override
372    public boolean hasChildren() {
373        if (!isFolder()) {
374            return false;
375        }
376        return session.hasChildren(id);
377    }
378
379    @Override
380    public Document addChild(String name, String typeName) {
381        if (!isFolder()) {
382            throw new IllegalArgumentException("Not a folder");
383        }
384        return session.createChild(null, id, name, null, typeName);
385    }
386
387    @Override
388    public void orderBefore(String src, String dest) {
389        Document srcDoc = getChild(src);
390        if (srcDoc == null) {
391            throw new DocumentNotFoundException("Document " + this + " has no child: " + src);
392        }
393        Document destDoc;
394        if (dest == null) {
395            destDoc = null;
396        } else {
397            destDoc = getChild(dest);
398            if (destDoc == null) {
399                throw new DocumentNotFoundException("Document " + this + " has no child: " + dest);
400            }
401        }
402        session.orderBefore(id, srcDoc.getUUID(), destDoc == null ? null : destDoc.getUUID());
403    }
404
405    // simple property only
406    @Override
407    public Serializable getPropertyValue(String name) {
408        DBSDocumentState docState = getStateOrTarget(name);
409        return docState.get(name);
410    }
411
412    // simple property only
413    @Override
414    public void setPropertyValue(String name, Serializable value) {
415        DBSDocumentState docState = getStateOrTarget(name);
416        docState.put(name, value);
417    }
418
419    // helpers for getValue / setValue
420
421    @Override
422    protected State getChild(State state, String name, Type type) {
423        return (State) state.get(name);
424    }
425
426    @Override
427    protected State getChildForWrite(State state, String name, Type type) throws PropertyException {
428        State child = getChild(state, name, type);
429        if (child == null) {
430            state.put(name, child = new State());
431        }
432        return child;
433    }
434
435    @Override
436    protected List<State> getChildAsList(State state, String name) {
437        @SuppressWarnings("unchecked")
438        List<State> list = (List<State>) state.get(name);
439        if (list == null) {
440            list = new ArrayList<>();
441        }
442        return list;
443    }
444
445    @Override
446    protected void updateList(State state, String name, Field field, String xpath, List<Object> values) {
447        List<State> childStates = new ArrayList<>(values.size());
448        int i = 0;
449        for (Object v : values) {
450            State childState = new State();
451            setValueComplex(childState, field, xpath + '/' + i, v);
452            childStates.add(childState);
453            i++;
454        }
455        state.put(name, (Serializable) childStates);
456    }
457
458    @Override
459    protected List<State> updateList(State state, String name, Property property) throws PropertyException {
460        Collection<Property> properties = property.getChildren();
461        int newSize = properties.size();
462        @SuppressWarnings("unchecked")
463        List<State> childStates = (List<State>) state.get(name);
464        if (childStates == null) {
465            childStates = new ArrayList<>(newSize);
466            state.put(name, (Serializable) childStates);
467        }
468        int oldSize = childStates.size();
469        // remove extra list elements
470        if (oldSize > newSize) {
471            for (int i = oldSize - 1; i >= newSize; i--) {
472                childStates.remove(i);
473            }
474        }
475        // add new list elements
476        if (oldSize < newSize) {
477            for (int i = oldSize; i < newSize; i++) {
478                childStates.add(new State());
479            }
480        }
481        return childStates;
482    }
483
484    @Override
485    public Object getValue(String xpath) throws PropertyException {
486        DBSDocumentState docState = getStateOrTarget(xpath);
487        return getValueObject(docState.getState(), xpath);
488    }
489
490    @Override
491    public void setValue(String xpath, Object value) throws PropertyException {
492        DBSDocumentState docState = getStateOrTarget(xpath);
493        // markDirty has to be called *before* we change the state
494        docState.markDirty();
495        setValueObject(docState.getState(), xpath, value);
496    }
497
498    @Override
499    public void visitBlobs(Consumer<BlobAccessor> blobVisitor) throws PropertyException {
500        if (isProxy()) {
501            getTargetDocument().visitBlobs(blobVisitor);
502            // fall through for proxy schemas
503        }
504        visitBlobs(docState.getState(), blobVisitor, docState::markDirty);
505    }
506
507    @Override
508    public boolean isRetentionActive() {
509        DBSDocumentState docState = getStateOrTarget();
510        return TRUE.equals(docState.get(KEY_IS_RETENTION_ACTIVE));
511    }
512
513    @Override
514    public void setRetentionActive(boolean retentionActive) {
515        DBSDocumentState docState = getStateOrTarget();
516        docState.put(KEY_IS_RETENTION_ACTIVE, retentionActive ? TRUE : null);
517    }
518
519    @Override
520    public Document checkIn(String label, String checkinComment) {
521        if (isProxy()) {
522            return getTargetDocument().checkIn(label, checkinComment);
523        } else if (isVersion()) {
524            throw new VersionNotModifiableException();
525        } else {
526            Document version = session.checkIn(id, label, checkinComment);
527            DocumentBlobManager blobManager = Framework.getService(DocumentBlobManager.class);
528            blobManager.freezeVersion(version);
529            return version;
530        }
531    }
532
533    @Override
534    public void checkOut() {
535        if (isProxy()) {
536            getTargetDocument().checkOut();
537        } else if (isVersion()) {
538            throw new VersionNotModifiableException();
539        } else {
540            session.checkOut(id);
541        }
542    }
543
544    @Override
545    public List<String> getVersionsIds() {
546        return session.getVersionsIds(getVersionSeriesId());
547    }
548
549    @Override
550    public List<Document> getVersions() {
551        List<String> ids = session.getVersionsIds(getVersionSeriesId());
552        List<Document> versions = new ArrayList<>();
553        for (String id : ids) {
554            versions.add(session.getDocument(id));
555        }
556        return versions;
557    }
558
559    @Override
560    public Document getLastVersion() {
561        return session.getLastVersion(getVersionSeriesId());
562    }
563
564    @Override
565    public Document getSourceDocument() {
566        if (isProxy()) {
567            return getTargetDocument();
568        } else if (isVersion()) {
569            return getWorkingCopy();
570        } else {
571            return this;
572        }
573    }
574
575    @Override
576    public void restore(Document version) {
577        if (!version.isVersion()) {
578            throw new NuxeoException("Cannot restore a non-version: " + version);
579        }
580        session.restoreVersion(this, version);
581    }
582
583    @Override
584    public Document getVersion(String label) {
585        DBSDocumentState state = session.getVersionByLabel(getVersionSeriesId(), label);
586        return session.getDocument(state);
587    }
588
589    @Override
590    public Document getBaseVersion() {
591        if (isProxy()) {
592            return getTargetDocument().getBaseVersion();
593        } else if (isVersion()) {
594            return null;
595        } else {
596            if (isCheckedOut()) {
597                return null;
598            } else {
599                String id = (String) docState.get(KEY_BASE_VERSION_ID);
600                if (id == null) {
601                    // shouldn't happen
602                    return null;
603                }
604                return session.getDocument(id);
605            }
606        }
607    }
608
609    @Override
610    public boolean isCheckedOut() {
611        if (isProxy()) {
612            return getTargetDocument().isCheckedOut();
613        } else if (isVersion()) {
614            return false;
615        } else {
616            return !TRUE.equals(docState.get(KEY_IS_CHECKED_IN));
617        }
618    }
619
620    @Override
621    public String getVersionSeriesId() {
622        if (isProxy()) {
623            return (String) docState.get(KEY_PROXY_VERSION_SERIES_ID);
624        } else if (isVersion()) {
625            return (String) docState.get(KEY_VERSION_SERIES_ID);
626        } else {
627            return getUUID();
628        }
629    }
630
631    @Override
632    public Calendar getVersionCreationDate() {
633        DBSDocumentState docState = getStateOrTarget();
634        return (Calendar) docState.get(KEY_VERSION_CREATED);
635    }
636
637    @Override
638    public String getVersionLabel() {
639        DBSDocumentState docState = getStateOrTarget();
640        return (String) docState.get(KEY_VERSION_LABEL);
641    }
642
643    @Override
644    public String getCheckinComment() {
645        DBSDocumentState docState = getStateOrTarget();
646        return (String) docState.get(KEY_VERSION_DESCRIPTION);
647    }
648
649    @Override
650    public boolean isLatestVersion() {
651        return isEqualOnVersion(TRUE, KEY_IS_LATEST_VERSION);
652    }
653
654    @Override
655    public boolean isMajorVersion() {
656        return isEqualOnVersion(ZERO, KEY_MINOR_VERSION);
657    }
658
659    @Override
660    public boolean isLatestMajorVersion() {
661        return isEqualOnVersion(TRUE, KEY_IS_LATEST_MAJOR_VERSION);
662    }
663
664    protected boolean isEqualOnVersion(Object ob, String key) {
665        if (isProxy()) {
666            // TODO avoid getting the target just to check if it's a version
667            // use another specific property instead
668            if (getTargetDocument().isVersion()) {
669                return ob.equals(docState.get(key));
670            } else {
671                // if live version, return false
672                return false;
673            }
674        } else if (isVersion()) {
675            return ob.equals(docState.get(key));
676        } else {
677            return false;
678        }
679    }
680
681    @Override
682    public boolean isVersionSeriesCheckedOut() {
683        if (isProxy() || isVersion()) {
684            Document workingCopy = getWorkingCopy();
685            return workingCopy != null && workingCopy.isCheckedOut();
686        } else {
687            return isCheckedOut();
688        }
689    }
690
691    @Override
692    public Document getWorkingCopy() {
693        if (isProxy() || isVersion()) {
694            String versionSeriesId = getVersionSeriesId();
695            return versionSeriesId == null ? null : session.getDocument(versionSeriesId);
696        } else {
697            return this;
698        }
699    }
700
701    @Override
702    public boolean isFolder() {
703        return type == null // null document
704                || type.isFolder();
705    }
706
707    @Override
708    public void setReadOnly(boolean readonly) {
709        this.readonly = readonly;
710    }
711
712    @Override
713    public boolean isReadOnly() {
714        return readonly;
715    }
716
717    @Override
718    public void remove() {
719        session.remove(id);
720    }
721
722    @Override
723    public String getLifeCycleState() {
724        DBSDocumentState docState = getStateOrTarget();
725        return (String) docState.get(KEY_LIFECYCLE_STATE);
726    }
727
728    @Override
729    public void setCurrentLifeCycleState(String lifeCycleState) throws LifeCycleException {
730        DBSDocumentState docState = getStateOrTarget();
731        docState.put(KEY_LIFECYCLE_STATE, lifeCycleState);
732        DocumentBlobManager blobManager = Framework.getService(DocumentBlobManager.class);
733        blobManager.notifyChanges(this, Collections.singleton(KEY_LIFECYCLE_STATE));
734    }
735
736    @Override
737    public String getLifeCyclePolicy() {
738        DBSDocumentState docState = getStateOrTarget();
739        return (String) docState.get(KEY_LIFECYCLE_POLICY);
740    }
741
742    @Override
743    public void setLifeCyclePolicy(String policy) throws LifeCycleException {
744        DBSDocumentState docState = getStateOrTarget();
745        docState.put(KEY_LIFECYCLE_POLICY, policy);
746        DocumentBlobManager blobManager = Framework.getService(DocumentBlobManager.class);
747        blobManager.notifyChanges(this, Collections.singleton(KEY_LIFECYCLE_POLICY));
748    }
749
750    // TODO generic
751    @Override
752    public void followTransition(String transition) throws LifeCycleException {
753        LifeCycleService service = Framework.getService(LifeCycleService.class);
754        if (service == null) {
755            throw new LifeCycleException("LifeCycleService not available");
756        }
757        service.followTransition(this, transition);
758    }
759
760    // TODO generic
761    @Override
762    public Collection<String> getAllowedStateTransitions() throws LifeCycleException {
763        LifeCycleService service = Framework.getService(LifeCycleService.class);
764        if (service == null) {
765            throw new LifeCycleException("LifeCycleService not available");
766        }
767        LifeCycle lifeCycle = service.getLifeCycleFor(this);
768        if (lifeCycle == null) {
769            return Collections.emptyList();
770        }
771        return lifeCycle.getAllowedStateTransitionsFrom(getLifeCycleState());
772    }
773
774    @Override
775    public void setSystemProp(String name, Serializable value) {
776        String propertyName;
777        if (name.startsWith(SYSPROP_FULLTEXT_SIMPLE)) {
778            propertyName = name.replace(SYSPROP_FULLTEXT_SIMPLE, KEY_FULLTEXT_SIMPLE);
779        } else if (name.startsWith(SYSPROP_FULLTEXT_BINARY)) {
780            propertyName = name.replace(SYSPROP_FULLTEXT_BINARY, KEY_FULLTEXT_BINARY);
781        } else {
782            propertyName = systemPropNameMap.get(name);
783        }
784        if (propertyName == null) {
785            throw new PropertyNotFoundException(name, "Unknown system property");
786        }
787        setPropertyValue(propertyName, value);
788    }
789
790    @SuppressWarnings("unchecked")
791    @Override
792    public <T extends Serializable> T getSystemProp(String name, Class<T> type) {
793        String propertyName = systemPropNameMap.get(name);
794        if (propertyName == null) {
795            throw new PropertyNotFoundException(name, "Unknown system property: ");
796        }
797        Serializable value = getPropertyValue(propertyName);
798        if (value == null) {
799            if (type == Boolean.class) {
800                value = Boolean.FALSE;
801            } else if (type == Long.class) {
802                value = Long.valueOf(0);
803            }
804        }
805        return (T) value;
806    }
807
808    public static final String CHANGE_TOKEN_PROXY_SEP = "/";
809
810    @Override
811    public String getChangeToken() {
812        if (session.changeTokenEnabled) {
813            Long sysChangeToken = docState.getSysChangeToken();
814            Long changeToken = docState.getChangeToken();
815            String userVisibleChangeToken = buildUserVisibleChangeToken(sysChangeToken, changeToken);
816            if (isProxy()) {
817                String targetUserVisibleChangeToken = getTargetDocument().getChangeToken();
818                return getProxyUserVisibleChangeToken(userVisibleChangeToken, targetUserVisibleChangeToken);
819            } else {
820                return userVisibleChangeToken;
821            }
822        } else {
823            DBSDocumentState docState = getStateOrTarget();
824            Calendar modified = (Calendar) docState.get(KEY_DC_MODIFIED);
825            return getLegacyChangeToken(modified);
826        }
827    }
828
829    protected static String getProxyUserVisibleChangeToken(String proxyToken, String targetToken) {
830        if (proxyToken == null && targetToken == null) {
831            return null;
832        } else {
833            if (proxyToken == null) {
834                proxyToken = "";
835            } else if (targetToken == null) {
836                targetToken = "";
837            }
838            return proxyToken + CHANGE_TOKEN_PROXY_SEP + targetToken;
839        }
840    }
841
842    @Override
843    public boolean validateUserVisibleChangeToken(String userVisibleChangeToken) {
844        if (userVisibleChangeToken == null) {
845            return true;
846        }
847        if (session.changeTokenEnabled) {
848            if (isProxy()) {
849                return validateProxyChangeToken(userVisibleChangeToken, docState, getTargetDocument().docState);
850            } else {
851                return docState.validateUserVisibleChangeToken(userVisibleChangeToken);
852            }
853        } else {
854            DBSDocumentState docState = getStateOrTarget();
855            Calendar modified = (Calendar) docState.get(KEY_DC_MODIFIED);
856            return validateLegacyChangeToken(modified, userVisibleChangeToken);
857        }
858    }
859
860    protected static boolean validateProxyChangeToken(String userVisibleChangeToken, DBSDocumentState proxyState,
861            DBSDocumentState targetState) {
862        String[] parts = userVisibleChangeToken.split(CHANGE_TOKEN_PROXY_SEP, 2);
863        if (parts.length != 2) {
864            // invalid format
865            return false;
866        }
867        String proxyToken = parts[0];
868        if (proxyToken.isEmpty()) {
869            proxyToken = null;
870        }
871        String targetToken = parts[1];
872        if (targetToken.isEmpty()) {
873            targetToken = null;
874        }
875        if (proxyToken == null && targetToken == null) {
876            return true;
877        }
878        return proxyState.validateUserVisibleChangeToken(proxyToken)
879                && targetState.validateUserVisibleChangeToken(targetToken);
880    }
881
882    @Override
883    public void markUserChange() {
884        if (isProxy()) {
885            session.markUserChange(getTargetDocumentId());
886        }
887        session.markUserChange(id);
888    }
889
890    protected DBSDocumentState getStateOrTarget(Type type) throws PropertyException {
891        return getStateOrTargetForSchema(type.getName());
892    }
893
894    protected DBSDocumentState getStateOrTarget(String xpath) {
895        return getStateOrTargetForSchema(getSchema(xpath));
896    }
897
898    /**
899     * Checks if the given schema should be resolved on the proxy or the target.
900     */
901    protected DBSDocumentState getStateOrTargetForSchema(String schema) {
902        if (isProxy() && !isSchemaForProxy(schema)) {
903            return getTargetDocument().docState;
904        } else {
905            return docState;
906        }
907    }
908
909    /**
910     * Gets the target state if this is a proxy, or the regular state otherwise.
911     */
912    protected DBSDocumentState getStateOrTarget() {
913        if (isProxy()) {
914            return getTargetDocument().docState;
915        } else {
916            return docState;
917        }
918    }
919
920    protected boolean isSchemaForProxy(String schema) {
921        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
922        return schemaManager.isProxySchema(schema, getType().getName());
923    }
924
925    protected String getSchema(String xpath) {
926        switch (xpath) {
927        case KEY_MAJOR_VERSION:
928        case KEY_MINOR_VERSION:
929        case "major_version":
930        case "minor_version":
931            return "uid";
932        case KEY_FULLTEXT_JOBID:
933        case KEY_IS_TRASHED:
934        case KEY_LIFECYCLE_POLICY:
935        case KEY_LIFECYCLE_STATE:
936            return "__ecm__";
937        }
938        if (xpath.startsWith(KEY_FULLTEXT_SIMPLE) || xpath.startsWith(KEY_FULLTEXT_BINARY)) {
939            return "__ecm__";
940        }
941        String[] segments = xpath.split("/");
942        String segment = segments[0];
943        Field field = type.getField(segment);
944        if (field == null) {
945            // check facets
946            SchemaManager schemaManager = Framework.getService(SchemaManager.class);
947            for (String facet : getFacets()) {
948                CompositeType facetType = schemaManager.getFacet(facet);
949                field = facetType.getField(segment);
950                if (field != null) {
951                    break;
952                }
953            }
954        }
955        if (field == null && getProxySchemas() != null) {
956            // check proxy schemas
957            for (Schema schema : getProxySchemas()) {
958                field = schema.getField(segment);
959                if (field != null) {
960                    break;
961                }
962            }
963        }
964        if (field == null) {
965            throw new PropertyNotFoundException(xpath);
966        }
967        return field.getDeclaringType().getName();
968    }
969
970    @Override
971    public void readDocumentPart(DocumentPart dp) throws PropertyException {
972        DBSDocumentState docState = getStateOrTarget(dp.getType());
973        readComplexProperty(docState.getState(), (ComplexProperty) dp);
974    }
975
976    @Override
977    protected String internalName(String name) {
978        switch (name) {
979        case PROP_MAJOR_VERSION:
980            return KEY_MAJOR_VERSION;
981        case PROP_MINOR_VERSION:
982            return KEY_MINOR_VERSION;
983        }
984        return name;
985    }
986
987    @Override
988    public boolean writeDocumentPart(DocumentPart dp, WriteContext writeContext) throws PropertyException {
989        DBSDocumentState docState = getStateOrTarget(dp.getType());
990        // markDirty has to be called *before* we change the state
991        docState.markDirty();
992        boolean changed = writeComplexProperty(docState.getState(), (ComplexProperty) dp, writeContext);
993        clearDirtyFlags(dp);
994        return changed;
995    }
996
997    @Override
998    public Set<String> getAllFacets() {
999        Set<String> facets = new HashSet<>(getType().getFacets());
1000        facets.addAll(Arrays.asList(getFacets()));
1001        return facets;
1002    }
1003
1004    @Override
1005    public String[] getFacets() {
1006        DBSDocumentState docState = getStateOrTarget();
1007        Object[] mixins = (Object[]) docState.get(KEY_MIXIN_TYPES);
1008        if (mixins == null) {
1009            return EMPTY_STRING_ARRAY;
1010        } else {
1011            String[] res = new String[mixins.length];
1012            System.arraycopy(mixins, 0, res, 0, mixins.length);
1013            return res;
1014        }
1015    }
1016
1017    @Override
1018    public boolean hasFacet(String facet) {
1019        return getAllFacets().contains(facet);
1020    }
1021
1022    @Override
1023    public boolean addFacet(String facet) {
1024        if (getType().getFacets().contains(facet)) {
1025            return false; // already present in type
1026        }
1027        DBSDocumentState docState = getStateOrTarget();
1028        Object[] mixins = (Object[]) docState.get(KEY_MIXIN_TYPES);
1029        if (mixins == null) {
1030            mixins = new Object[] { facet };
1031        } else {
1032            List<Object> list = Arrays.asList(mixins);
1033            if (list.contains(facet)) {
1034                return false; // already present in doc
1035            }
1036            list = new ArrayList<>(list);
1037            list.add(facet);
1038            mixins = list.toArray(new Object[list.size()]);
1039        }
1040        docState.put(KEY_MIXIN_TYPES, mixins);
1041        return true;
1042    }
1043
1044    @Override
1045    public boolean removeFacet(String facet) {
1046        DBSDocumentState docState = getStateOrTarget();
1047        Object[] mixins = (Object[]) docState.get(KEY_MIXIN_TYPES);
1048        if (mixins == null) {
1049            return false;
1050        }
1051        List<Object> list = new ArrayList<>(Arrays.asList(mixins));
1052        if (!list.remove(facet)) {
1053            return false; // not present in doc
1054        }
1055        mixins = list.toArray(new Object[list.size()]);
1056        if (mixins.length == 0) {
1057            mixins = null;
1058        }
1059        docState.put(KEY_MIXIN_TYPES, mixins);
1060        // remove the fields belonging to the facet
1061        // except for schemas still present due to the primary type or another facet
1062        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
1063        CompositeType ft = schemaManager.getFacet(facet);
1064        Set<String> otherSchemas = getSchemas(getType(), list);
1065        for (Schema schema : ft.getSchemas()) {
1066            if (otherSchemas.contains(schema.getName())) {
1067                continue;
1068            }
1069            for (Field field : schema.getFields()) {
1070                String name = field.getName().getPrefixedName();
1071                if (docState.containsKey(name)) {
1072                    docState.put(name, null);
1073                }
1074            }
1075        }
1076        return true;
1077    }
1078
1079    protected static Set<String> getSchemas(DocumentType type, List<Object> facets) {
1080        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
1081        Set<String> schemas = new HashSet<>(Arrays.asList(type.getSchemaNames()));
1082        for (Object facet : facets) {
1083            CompositeType ft = schemaManager.getFacet((String) facet);
1084            if (ft != null) {
1085                schemas.addAll(Arrays.asList(ft.getSchemaNames()));
1086            }
1087        }
1088        return schemas;
1089    }
1090
1091    @Override
1092    public DBSDocument getTargetDocument() {
1093        String targetId = getTargetDocumentId();
1094        return targetId == null ? null : session.getDocument(targetId);
1095    }
1096
1097    protected String getTargetDocumentId() {
1098        return isProxy() ? (String) docState.get(KEY_PROXY_TARGET_ID) : null;
1099    }
1100
1101    @Override
1102    public void setTargetDocument(Document target) {
1103        if (isProxy()) {
1104            if (isReadOnly()) {
1105                throw new ReadOnlyPropertyException("Cannot write proxy: " + this);
1106            }
1107            if (!target.getVersionSeriesId().equals(getVersionSeriesId())) {
1108                throw new ReadOnlyPropertyException("Cannot set proxy target to different version series");
1109            }
1110            session.setProxyTarget(this, target);
1111        } else {
1112            throw new NuxeoException("Cannot set proxy target on non-proxy");
1113        }
1114    }
1115
1116    @Override
1117    protected Lock getDocumentLock() {
1118        String owner = (String) docState.get(KEY_LOCK_OWNER);
1119        if (owner == null) {
1120            return null;
1121        }
1122        Calendar created = (Calendar) docState.get(KEY_LOCK_CREATED);
1123        return new Lock(owner, created);
1124    }
1125
1126    @Override
1127    protected Lock setDocumentLock(Lock lock) {
1128        String owner = (String) docState.get(KEY_LOCK_OWNER);
1129        if (owner != null) {
1130            // return old lock
1131            Calendar created = (Calendar) docState.get(KEY_LOCK_CREATED);
1132            return new Lock(owner, created);
1133        }
1134        docState.put(KEY_LOCK_OWNER, lock.getOwner());
1135        docState.put(KEY_LOCK_CREATED, lock.getCreated());
1136        return null;
1137    }
1138
1139    @Override
1140    protected Lock removeDocumentLock(String owner) {
1141        String oldOwner = (String) docState.get(KEY_LOCK_OWNER);
1142        if (oldOwner == null) {
1143            // no previous lock
1144            return null;
1145        }
1146        Calendar oldCreated = (Calendar) docState.get(KEY_LOCK_CREATED);
1147        if (!LockManager.canLockBeRemoved(oldOwner, owner)) {
1148            // existing mismatched lock, flag failure
1149            return new Lock(oldOwner, oldCreated, true);
1150        }
1151        // remove lock
1152        docState.put(KEY_LOCK_OWNER, null);
1153        docState.put(KEY_LOCK_CREATED, null);
1154        // return old lock
1155        return new Lock(oldOwner, oldCreated);
1156    }
1157
1158    @Override
1159    public String toString() {
1160        return getClass().getSimpleName() + '(' + getName() + ',' + getUUID() + ')';
1161    }
1162
1163    @Override
1164    public boolean equals(Object other) {
1165        if (other == this) {
1166            return true;
1167        }
1168        if (other == null) {
1169            return false;
1170        }
1171        if (other.getClass() == getClass()) {
1172            return equals((DBSDocument) other);
1173        }
1174        return false;
1175    }
1176
1177    private boolean equals(DBSDocument other) {
1178        return id.equals(other.id);
1179    }
1180
1181    @Override
1182    public int hashCode() {
1183        return id.hashCode();
1184    }
1185
1186}