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.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.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_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    public static final String KEY_LOCK_OWNER = "ecm:lockOwner";
169
170    public static final String KEY_LOCK_CREATED = "ecm:lockCreated";
171
172    public static final String KEY_SYS_CHANGE_TOKEN = "ecm:systemChangeToken";
173
174    public static final String KEY_CHANGE_TOKEN = "ecm:changeToken";
175
176    // used instead of ecm:changeToken when change tokens are disabled
177    public static final String KEY_DC_MODIFIED = "dc:modified";
178
179    public static final String KEY_BLOB_NAME = "name";
180
181    public static final String KEY_BLOB_MIME_TYPE = "mime-type";
182
183    public static final String KEY_BLOB_ENCODING = "encoding";
184
185    public static final String KEY_BLOB_DIGEST = "digest";
186
187    public static final String KEY_BLOB_LENGTH = "length";
188
189    public static final String KEY_BLOB_DATA = "data";
190
191    public static final String KEY_FULLTEXT_SIMPLE = "ecm:fulltextSimple";
192
193    public static final String KEY_FULLTEXT_BINARY = "ecm:fulltextBinary";
194
195    public static final String KEY_FULLTEXT_JOBID = "ecm:fulltextJobId";
196
197    public static final String KEY_FULLTEXT_SCORE = "ecm:fulltextScore";
198
199    public static final String APPLICATION_OCTET_STREAM = "application/octet-stream";
200
201    public static final String PROP_UID_MAJOR_VERSION = "uid:major_version";
202
203    public static final String PROP_UID_MINOR_VERSION = "uid:minor_version";
204
205    public static final String PROP_MAJOR_VERSION = "major_version";
206
207    public static final String PROP_MINOR_VERSION = "minor_version";
208    /**
209     * @since 9.3
210     */
211    public static final String FACETED_TAG = "nxtag:tags";
212
213    /**
214     * @since 9.3
215     */
216    public static final String FACETED_TAG_LABEL = "label";
217
218    public static final Long INITIAL_SYS_CHANGE_TOKEN = Long.valueOf(0);
219
220    public static final Long INITIAL_CHANGE_TOKEN = Long.valueOf(0);
221
222    protected final String id;
223
224    protected final DBSDocumentState docState;
225
226    protected final DocumentType type;
227
228    protected final List<Schema> proxySchemas;
229
230    protected final DBSSession session;
231
232    protected boolean readonly;
233
234    protected static final Map<String, String> systemPropNameMap;
235
236    static {
237        systemPropNameMap = new HashMap<String, String>();
238        systemPropNameMap.put(SYSPROP_FULLTEXT_JOBID, KEY_FULLTEXT_JOBID);
239    }
240
241    public DBSDocument(DBSDocumentState docState, DocumentType type, DBSSession session, boolean readonly) {
242        // no state for NullDocument (parent of placeless children)
243        this.id = docState == null ? null : (String) docState.get(KEY_ID);
244        this.docState = docState;
245        this.type = type;
246        this.session = session;
247        if (docState != null && isProxy()) {
248            SchemaManager schemaManager = Framework.getService(SchemaManager.class);
249            proxySchemas = schemaManager.getProxySchemas(type.getName());
250        } else {
251            proxySchemas = null;
252        }
253        this.readonly = readonly;
254    }
255
256    @Override
257    public DocumentType getType() {
258        return type;
259    }
260
261    @Override
262    public Session getSession() {
263        return session;
264    }
265
266    @Override
267    public String getRepositoryName() {
268        return session.getRepositoryName();
269    }
270
271    @Override
272    protected List<Schema> getProxySchemas() {
273        return proxySchemas;
274    }
275
276    @Override
277    public String getUUID() {
278        return id;
279    }
280
281    @Override
282    public String getName() {
283        return docState.getName();
284    }
285
286    @Override
287    public Long getPos() {
288        return (Long) docState.get(KEY_POS);
289    }
290
291    @Override
292    public Document getParent() {
293        if (isVersion()) {
294            Document workingCopy = session.getDocument(getVersionSeriesId());
295            return workingCopy == null ? null : workingCopy.getParent();
296        }
297        String parentId = docState.getParentId();
298        return parentId == null ? null : session.getDocument(parentId);
299    }
300
301    @Override
302    public boolean isProxy() {
303        return TRUE.equals(docState.get(KEY_IS_PROXY));
304    }
305
306    @Override
307    public boolean isVersion() {
308        return TRUE.equals(docState.get(KEY_IS_VERSION));
309    }
310
311    @Override
312    public String getPath() {
313        if (isVersion()) {
314            Document workingCopy = session.getDocument(getVersionSeriesId());
315            return workingCopy == null ? null : workingCopy.getPath();
316        }
317        String name = getName();
318        Document doc = getParent();
319        if (doc == null) {
320            if ("".equals(name)) {
321                return "/"; // root
322            } else {
323                return name; // placeless, no slash
324            }
325        }
326        LinkedList<String> list = new LinkedList<String>();
327        list.addFirst(name);
328        while (doc != null) {
329            list.addFirst(doc.getName());
330            doc = doc.getParent();
331        }
332        return StringUtils.join(list, '/');
333    }
334
335    @Override
336    public Document getChild(String name) {
337        return session.getChild(id, name);
338    }
339
340    @Override
341    public List<Document> getChildren() {
342        if (!isFolder()) {
343            return Collections.emptyList();
344        }
345        return session.getChildren(id);
346    }
347
348    @Override
349    public List<String> getChildrenIds() {
350        if (!isFolder()) {
351            return Collections.emptyList();
352        }
353        return session.getChildrenIds(id);
354    }
355
356    @Override
357    public boolean hasChild(String name) {
358        if (!isFolder()) {
359            return false;
360        }
361        return session.hasChild(id, name);
362    }
363
364    @Override
365    public boolean hasChildren() {
366        if (!isFolder()) {
367            return false;
368        }
369        return session.hasChildren(id);
370    }
371
372    @Override
373    public Document addChild(String name, String typeName) {
374        if (!isFolder()) {
375            throw new IllegalArgumentException("Not a folder");
376        }
377        return session.createChild(null, id, name, null, typeName);
378    }
379
380    @Override
381    public void orderBefore(String src, String dest) {
382        Document srcDoc = getChild(src);
383        if (srcDoc == null) {
384            throw new DocumentNotFoundException("Document " + this + " has no child: " + src);
385        }
386        Document destDoc;
387        if (dest == null) {
388            destDoc = null;
389        } else {
390            destDoc = getChild(dest);
391            if (destDoc == null) {
392                throw new DocumentNotFoundException("Document " + this + " has no child: " + dest);
393            }
394        }
395        session.orderBefore(id, srcDoc.getUUID(), destDoc == null ? null : destDoc.getUUID());
396    }
397
398    // simple property only
399    @Override
400    public Serializable getPropertyValue(String name) {
401        DBSDocumentState docState = getStateOrTarget(name);
402        return docState.get(name);
403    }
404
405    // simple property only
406    @Override
407    public void setPropertyValue(String name, Serializable value) {
408        DBSDocumentState docState = getStateOrTarget(name);
409        docState.put(name, value);
410    }
411
412    // helpers for getValue / setValue
413
414    @Override
415    protected State getChild(State state, String name, Type type) {
416        return (State) state.get(name);
417    }
418
419    @Override
420    protected State getChildForWrite(State state, String name, Type type) throws PropertyException {
421        State child = getChild(state, name, type);
422        if (child == null) {
423            state.put(name, child = new State());
424        }
425        return child;
426    }
427
428    @Override
429    protected List<State> getChildAsList(State state, String name) {
430        @SuppressWarnings("unchecked")
431        List<State> list = (List<State>) state.get(name);
432        if (list == null) {
433            list = new ArrayList<>();
434        }
435        return list;
436    }
437
438    @Override
439    protected void updateList(State state, String name, Field field, String xpath, List<Object> values) {
440        List<State> childStates = new ArrayList<>(values.size());
441        int i = 0;
442        for (Object v : values) {
443            State childState = new State();
444            setValueComplex(childState, field, xpath + '/' + i, v);
445            childStates.add(childState);
446            i++;
447        }
448        state.put(name, (Serializable) childStates);
449    }
450
451    @Override
452    protected List<State> updateList(State state, String name, Property property) throws PropertyException {
453        Collection<Property> properties = property.getChildren();
454        int newSize = properties.size();
455        @SuppressWarnings("unchecked")
456        List<State> childStates = (List<State>) state.get(name);
457        if (childStates == null) {
458            childStates = new ArrayList<>(newSize);
459            state.put(name, (Serializable) childStates);
460        }
461        int oldSize = childStates.size();
462        // remove extra list elements
463        if (oldSize > newSize) {
464            for (int i = oldSize - 1; i >= newSize; i--) {
465                childStates.remove(i);
466            }
467        }
468        // add new list elements
469        if (oldSize < newSize) {
470            for (int i = oldSize; i < newSize; i++) {
471                childStates.add(new State());
472            }
473        }
474        return childStates;
475    }
476
477    @Override
478    public Object getValue(String xpath) throws PropertyException {
479        DBSDocumentState docState = getStateOrTarget(xpath);
480        return getValueObject(docState.getState(), xpath);
481    }
482
483    @Override
484    public void setValue(String xpath, Object value) throws PropertyException {
485        DBSDocumentState docState = getStateOrTarget(xpath);
486        // markDirty has to be called *before* we change the state
487        docState.markDirty();
488        setValueObject(docState.getState(), xpath, value);
489    }
490
491    @Override
492    public void visitBlobs(Consumer<BlobAccessor> blobVisitor) throws PropertyException {
493        if (isProxy()) {
494            getTargetDocument().visitBlobs(blobVisitor);
495            // fall through for proxy schemas
496        }
497        Runnable markDirty = () -> docState.markDirty();
498        visitBlobs(docState.getState(), blobVisitor, markDirty);
499    }
500
501    @Override
502    public boolean isRetentionActive() {
503        DBSDocumentState docState = getStateOrTarget();
504        return TRUE.equals(docState.get(KEY_IS_RETENTION_ACTIVE));
505    }
506
507    @Override
508    public void setRetentionActive(boolean retentionActive) {
509        DBSDocumentState docState = getStateOrTarget();
510        docState.put(KEY_IS_RETENTION_ACTIVE, retentionActive ? TRUE : null);
511    }
512
513    @Override
514    public Document checkIn(String label, String checkinComment) {
515        if (isProxy()) {
516            return getTargetDocument().checkIn(label, checkinComment);
517        } else if (isVersion()) {
518            throw new VersionNotModifiableException();
519        } else {
520            Document version = session.checkIn(id, label, checkinComment);
521            DocumentBlobManager blobManager = Framework.getService(DocumentBlobManager.class);
522            blobManager.freezeVersion(version);
523            return version;
524        }
525    }
526
527    @Override
528    public void checkOut() {
529        if (isProxy()) {
530            getTargetDocument().checkOut();
531        } else if (isVersion()) {
532            throw new VersionNotModifiableException();
533        } else {
534            session.checkOut(id);
535        }
536    }
537
538    @Override
539    public List<String> getVersionsIds() {
540        return session.getVersionsIds(getVersionSeriesId());
541    }
542
543    @Override
544    public List<Document> getVersions() {
545        List<String> ids = session.getVersionsIds(getVersionSeriesId());
546        List<Document> versions = new ArrayList<Document>();
547        for (String id : ids) {
548            versions.add(session.getDocument(id));
549        }
550        return versions;
551    }
552
553    @Override
554    public Document getLastVersion() {
555        return session.getLastVersion(getVersionSeriesId());
556    }
557
558    @Override
559    public Document getSourceDocument() {
560        if (isProxy()) {
561            return getTargetDocument();
562        } else if (isVersion()) {
563            return getWorkingCopy();
564        } else {
565            return this;
566        }
567    }
568
569    @Override
570    public void restore(Document version) {
571        if (!version.isVersion()) {
572            throw new NuxeoException("Cannot restore a non-version: " + version);
573        }
574        session.restoreVersion(this, version);
575    }
576
577    @Override
578    public Document getVersion(String label) {
579        DBSDocumentState state = session.getVersionByLabel(getVersionSeriesId(), label);
580        return session.getDocument(state);
581    }
582
583    @Override
584    public Document getBaseVersion() {
585        if (isProxy()) {
586            return getTargetDocument().getBaseVersion();
587        } else if (isVersion()) {
588            return null;
589        } else {
590            if (isCheckedOut()) {
591                return null;
592            } else {
593                String id = (String) docState.get(KEY_BASE_VERSION_ID);
594                if (id == null) {
595                    // shouldn't happen
596                    return null;
597                }
598                return session.getDocument(id);
599            }
600        }
601    }
602
603    @Override
604    public boolean isCheckedOut() {
605        if (isProxy()) {
606            return getTargetDocument().isCheckedOut();
607        } else if (isVersion()) {
608            return false;
609        } else {
610            return !TRUE.equals(docState.get(KEY_IS_CHECKED_IN));
611        }
612    }
613
614    @Override
615    public String getVersionSeriesId() {
616        if (isProxy()) {
617            return (String) docState.get(KEY_PROXY_VERSION_SERIES_ID);
618        } else if (isVersion()) {
619            return (String) docState.get(KEY_VERSION_SERIES_ID);
620        } else {
621            return getUUID();
622        }
623    }
624
625    @Override
626    public Calendar getVersionCreationDate() {
627        DBSDocumentState docState = getStateOrTarget();
628        return (Calendar) docState.get(KEY_VERSION_CREATED);
629    }
630
631    @Override
632    public String getVersionLabel() {
633        DBSDocumentState docState = getStateOrTarget();
634        return (String) docState.get(KEY_VERSION_LABEL);
635    }
636
637    @Override
638    public String getCheckinComment() {
639        DBSDocumentState docState = getStateOrTarget();
640        return (String) docState.get(KEY_VERSION_DESCRIPTION);
641    }
642
643    @Override
644    public boolean isLatestVersion() {
645        return isEqualOnVersion(TRUE, KEY_IS_LATEST_VERSION);
646    }
647
648    @Override
649    public boolean isMajorVersion() {
650        return isEqualOnVersion(ZERO, KEY_MINOR_VERSION);
651    }
652
653    @Override
654    public boolean isLatestMajorVersion() {
655        return isEqualOnVersion(TRUE, KEY_IS_LATEST_MAJOR_VERSION);
656    }
657
658    protected boolean isEqualOnVersion(Object ob, String key) {
659        if (isProxy()) {
660            // TODO avoid getting the target just to check if it's a version
661            // use another specific property instead
662            if (getTargetDocument().isVersion()) {
663                return ob.equals(docState.get(key));
664            } else {
665                // if live version, return false
666                return false;
667            }
668        } else if (isVersion()) {
669            return ob.equals(docState.get(key));
670        } else {
671            return false;
672        }
673    }
674
675    @Override
676    public boolean isVersionSeriesCheckedOut() {
677        if (isProxy() || isVersion()) {
678            Document workingCopy = getWorkingCopy();
679            return workingCopy == null ? false : workingCopy.isCheckedOut();
680        } else {
681            return isCheckedOut();
682        }
683    }
684
685    @Override
686    public Document getWorkingCopy() {
687        if (isProxy() || isVersion()) {
688            String versionSeriesId = getVersionSeriesId();
689            return versionSeriesId == null ? null : session.getDocument(versionSeriesId);
690        } else {
691            return this;
692        }
693    }
694
695    @Override
696    public boolean isFolder() {
697        return type == null // null document
698                || type.isFolder();
699    }
700
701    @Override
702    public void setReadOnly(boolean readonly) {
703        this.readonly = readonly;
704    }
705
706    @Override
707    public boolean isReadOnly() {
708        return readonly;
709    }
710
711    @Override
712    public void remove() {
713        session.remove(id);
714    }
715
716    @Override
717    public String getLifeCycleState() {
718        DBSDocumentState docState = getStateOrTarget();
719        return (String) docState.get(KEY_LIFECYCLE_STATE);
720    }
721
722    @Override
723    public void setCurrentLifeCycleState(String lifeCycleState) throws LifeCycleException {
724        DBSDocumentState docState = getStateOrTarget();
725        docState.put(KEY_LIFECYCLE_STATE, lifeCycleState);
726        DocumentBlobManager blobManager = Framework.getService(DocumentBlobManager.class);
727        blobManager.notifyChanges(this, Collections.singleton(KEY_LIFECYCLE_STATE));
728    }
729
730    @Override
731    public String getLifeCyclePolicy() {
732        DBSDocumentState docState = getStateOrTarget();
733        return (String) docState.get(KEY_LIFECYCLE_POLICY);
734    }
735
736    @Override
737    public void setLifeCyclePolicy(String policy) throws LifeCycleException {
738        DBSDocumentState docState = getStateOrTarget();
739        docState.put(KEY_LIFECYCLE_POLICY, policy);
740        DocumentBlobManager blobManager = Framework.getService(DocumentBlobManager.class);
741        blobManager.notifyChanges(this, Collections.singleton(KEY_LIFECYCLE_POLICY));
742    }
743
744    // TODO generic
745    @Override
746    public void followTransition(String transition) throws LifeCycleException {
747        LifeCycleService service = NXCore.getLifeCycleService();
748        if (service == null) {
749            throw new LifeCycleException("LifeCycleService not available");
750        }
751        service.followTransition(this, transition);
752    }
753
754    // TODO generic
755    @Override
756    public Collection<String> getAllowedStateTransitions() throws LifeCycleException {
757        LifeCycleService service = NXCore.getLifeCycleService();
758        if (service == null) {
759            throw new LifeCycleException("LifeCycleService not available");
760        }
761        LifeCycle lifeCycle = service.getLifeCycleFor(this);
762        if (lifeCycle == null) {
763            return Collections.emptyList();
764        }
765        return lifeCycle.getAllowedStateTransitionsFrom(getLifeCycleState());
766    }
767
768    @Override
769    public void setSystemProp(String name, Serializable value) {
770        String propertyName;
771        if (name.startsWith(SYSPROP_FULLTEXT_SIMPLE)) {
772            propertyName = name.replace(SYSPROP_FULLTEXT_SIMPLE, KEY_FULLTEXT_SIMPLE);
773        } else if (name.startsWith(SYSPROP_FULLTEXT_BINARY)) {
774            propertyName = name.replace(SYSPROP_FULLTEXT_BINARY, KEY_FULLTEXT_BINARY);
775        } else {
776            propertyName = systemPropNameMap.get(name);
777        }
778        if (propertyName == null) {
779            throw new PropertyNotFoundException(name, "Unknown system property");
780        }
781        setPropertyValue(propertyName, value);
782    }
783
784    @SuppressWarnings("unchecked")
785    @Override
786    public <T extends Serializable> T getSystemProp(String name, Class<T> type) {
787        String propertyName = systemPropNameMap.get(name);
788        if (propertyName == null) {
789            throw new PropertyNotFoundException(name, "Unknown system property: ");
790        }
791        Serializable value = getPropertyValue(propertyName);
792        if (value == null) {
793            if (type == Boolean.class) {
794                value = Boolean.FALSE;
795            } else if (type == Long.class) {
796                value = Long.valueOf(0);
797            }
798        }
799        return (T) value;
800    }
801
802    public static final String CHANGE_TOKEN_PROXY_SEP = "/";
803
804    @Override
805    public String getChangeToken() {
806        if (session.changeTokenEnabled) {
807            Long sysChangeToken = docState.getSysChangeToken();
808            Long changeToken = docState.getChangeToken();
809            String userVisibleChangeToken = buildUserVisibleChangeToken(sysChangeToken, changeToken);
810            if (isProxy()) {
811                String targetUserVisibleChangeToken = getTargetDocument().getChangeToken();
812                return getProxyUserVisibleChangeToken(userVisibleChangeToken, targetUserVisibleChangeToken);
813            } else {
814                return userVisibleChangeToken;
815            }
816        } else {
817            DBSDocumentState docState = getStateOrTarget();
818            Calendar modified = (Calendar) docState.get(KEY_DC_MODIFIED);
819            return getLegacyChangeToken(modified);
820        }
821    }
822
823    protected static String getProxyUserVisibleChangeToken(String proxyToken, String targetToken) {
824        if (proxyToken == null && targetToken == null) {
825            return null;
826        } else {
827            if (proxyToken == null) {
828                proxyToken = "";
829            } else if (targetToken == null) {
830                targetToken = "";
831            }
832            return proxyToken + CHANGE_TOKEN_PROXY_SEP + targetToken;
833        }
834    }
835
836    @Override
837    public boolean validateUserVisibleChangeToken(String userVisibleChangeToken) {
838        if (userVisibleChangeToken == null) {
839            return true;
840        }
841        if (session.changeTokenEnabled) {
842            if (isProxy()) {
843                return validateProxyChangeToken(userVisibleChangeToken, docState, getTargetDocument().docState);
844            } else {
845                return docState.validateUserVisibleChangeToken(userVisibleChangeToken);
846            }
847        } else {
848            DBSDocumentState docState = getStateOrTarget();
849            Calendar modified = (Calendar) docState.get(KEY_DC_MODIFIED);
850            return validateLegacyChangeToken(modified, userVisibleChangeToken);
851        }
852    }
853
854    protected static boolean validateProxyChangeToken(String userVisibleChangeToken, DBSDocumentState proxyState,
855            DBSDocumentState targetState) {
856        String[] parts = userVisibleChangeToken.split(CHANGE_TOKEN_PROXY_SEP, 2);
857        if (parts.length != 2) {
858            // invalid format
859            return false;
860        }
861        String proxyToken = parts[0];
862        if (proxyToken.isEmpty()) {
863            proxyToken = null;
864        }
865        String targetToken = parts[1];
866        if (targetToken.isEmpty()) {
867            targetToken = null;
868        }
869        if (proxyToken == null && targetToken == null) {
870            return true;
871        }
872        return proxyState.validateUserVisibleChangeToken(proxyToken)
873                && targetState.validateUserVisibleChangeToken(targetToken);
874    }
875
876    @Override
877    public void markUserChange() {
878        if (isProxy()) {
879            session.markUserChange(getTargetDocumentId());
880        }
881        session.markUserChange(id);
882    }
883
884    protected DBSDocumentState getStateOrTarget(Type type) throws PropertyException {
885        return getStateOrTargetForSchema(type.getName());
886    }
887
888    protected DBSDocumentState getStateOrTarget(String xpath) {
889        return getStateOrTargetForSchema(getSchema(xpath));
890    }
891
892    /**
893     * Checks if the given schema should be resolved on the proxy or the target.
894     */
895    protected DBSDocumentState getStateOrTargetForSchema(String schema) {
896        if (isProxy() && !isSchemaForProxy(schema)) {
897            return getTargetDocument().docState;
898        } else {
899            return docState;
900        }
901    }
902
903    /**
904     * Gets the target state if this is a proxy, or the regular state otherwise.
905     */
906    protected DBSDocumentState getStateOrTarget() {
907        if (isProxy()) {
908            return getTargetDocument().docState;
909        } else {
910            return docState;
911        }
912    }
913
914    protected boolean isSchemaForProxy(String schema) {
915        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
916        return schemaManager.isProxySchema(schema, getType().getName());
917    }
918
919    protected String getSchema(String xpath) {
920        switch (xpath) {
921        case KEY_MAJOR_VERSION:
922        case KEY_MINOR_VERSION:
923        case "major_version":
924        case "minor_version":
925            return "uid";
926        case KEY_FULLTEXT_JOBID:
927        case KEY_LIFECYCLE_POLICY:
928        case KEY_LIFECYCLE_STATE:
929            return "__ecm__";
930        }
931        if (xpath.startsWith(KEY_FULLTEXT_SIMPLE) || xpath.startsWith(KEY_FULLTEXT_BINARY)) {
932            return "__ecm__";
933        }
934        String[] segments = xpath.split("/");
935        String segment = segments[0];
936        Field field = type.getField(segment);
937        if (field == null) {
938            // check facets
939            SchemaManager schemaManager = Framework.getService(SchemaManager.class);
940            for (String facet : getFacets()) {
941                CompositeType facetType = schemaManager.getFacet(facet);
942                field = facetType.getField(segment);
943                if (field != null) {
944                    break;
945                }
946            }
947        }
948        if (field == null && getProxySchemas() != null) {
949            // check proxy schemas
950            for (Schema schema : getProxySchemas()) {
951                field = schema.getField(segment);
952                if (field != null) {
953                    break;
954                }
955            }
956        }
957        if (field == null) {
958            throw new PropertyNotFoundException(xpath);
959        }
960        return field.getDeclaringType().getName();
961    }
962
963    @Override
964    public void readDocumentPart(DocumentPart dp) throws PropertyException {
965        DBSDocumentState docState = getStateOrTarget(dp.getType());
966        readComplexProperty(docState.getState(), (ComplexProperty) dp);
967    }
968
969    @Override
970    protected String internalName(String name) {
971        switch (name) {
972        case PROP_MAJOR_VERSION:
973            return KEY_MAJOR_VERSION;
974        case PROP_MINOR_VERSION:
975            return KEY_MINOR_VERSION;
976        }
977        return name;
978    }
979
980    @Override
981    public Map<String, Serializable> readPrefetch(ComplexType complexType, Set<String> xpaths)
982            throws PropertyException {
983        DBSDocumentState docState = getStateOrTarget(complexType);
984        return readPrefetch(docState.getState(), complexType, xpaths);
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<String>(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<Object>(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<Object>(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}