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