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