001/*
002 * (C) Copyright 2006-2019 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.core.storage.sql.coremodel;
020
021import java.io.Serializable;
022import java.util.ArrayList;
023import java.util.Calendar;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030import java.util.function.Consumer;
031
032import org.nuxeo.ecm.core.api.CoreSession;
033import org.nuxeo.ecm.core.api.DocumentNotFoundException;
034import org.nuxeo.ecm.core.api.LifeCycleException;
035import org.nuxeo.ecm.core.api.Lock;
036import org.nuxeo.ecm.core.api.NuxeoException;
037import org.nuxeo.ecm.core.api.NuxeoPrincipal;
038import org.nuxeo.ecm.core.api.PropertyException;
039import org.nuxeo.ecm.core.api.model.DocumentPart;
040import org.nuxeo.ecm.core.api.model.Property;
041import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
042import org.nuxeo.ecm.core.api.model.impl.ComplexProperty;
043import org.nuxeo.ecm.core.blob.DocumentBlobManager;
044import org.nuxeo.ecm.core.lifecycle.LifeCycle;
045import org.nuxeo.ecm.core.lifecycle.LifeCycleService;
046import org.nuxeo.ecm.core.model.Document;
047import org.nuxeo.ecm.core.schema.DocumentType;
048import org.nuxeo.ecm.core.schema.SchemaManager;
049import org.nuxeo.ecm.core.schema.types.ComplexType;
050import org.nuxeo.ecm.core.schema.types.Field;
051import org.nuxeo.ecm.core.schema.types.ListType;
052import org.nuxeo.ecm.core.schema.types.Schema;
053import org.nuxeo.ecm.core.schema.types.Type;
054import org.nuxeo.ecm.core.storage.BaseDocument;
055import org.nuxeo.ecm.core.storage.sql.Model;
056import org.nuxeo.ecm.core.storage.sql.Node;
057import org.nuxeo.runtime.api.Framework;
058
059public class SQLDocumentLive extends BaseDocument<Node> implements SQLDocument {
060
061    protected final Node node;
062
063    protected final Type type;
064
065    protected SQLSession session;
066
067    /** Proxy-induced types. */
068    protected final List<Schema> proxySchemas;
069
070    /**
071     * Read-only flag, used to allow/disallow writes on versions.
072     */
073    protected boolean readonly;
074
075    protected SQLDocumentLive(Node node, ComplexType type, SQLSession session, boolean readonly) {
076        this.node = node;
077        this.type = type;
078        this.session = session;
079        if (node != null && node.isProxy()) {
080            SchemaManager schemaManager = Framework.getService(SchemaManager.class);
081            proxySchemas = schemaManager.getProxySchemas(type.getName());
082        } else {
083            proxySchemas = null;
084        }
085        this.readonly = readonly;
086    }
087
088    @Override
089    public void setReadOnly(boolean readonly) {
090        this.readonly = readonly;
091    }
092
093    @Override
094    public boolean isReadOnly() {
095        return readonly;
096    }
097
098    @Override
099    public Node getNode() {
100        return node;
101    }
102
103    @Override
104    public String getName() {
105        return getNode() == null ? null : getNode().getName();
106    }
107
108    @Override
109    public Long getPos() {
110        return getNode().getPos();
111    }
112
113    /*
114     * ----- org.nuxeo.ecm.core.model.Document -----
115     */
116
117    @Override
118    public DocumentType getType() {
119        return (DocumentType) type;
120    }
121
122    @Override
123    public SQLSession getSession() {
124        return session;
125    }
126
127    @Override
128    public boolean isFolder() {
129        return type == null // null document
130                || ((DocumentType) type).isFolder();
131    }
132
133    @Override
134    public String getUUID() {
135        return session.idToString(getNode().getId());
136    }
137
138    @Override
139    public Document getParent() {
140        return session.getParent(getNode());
141    }
142
143    @Override
144    public String getPath() {
145        return session.getPath(getNode());
146    }
147
148    @Override
149    public boolean isProxy() {
150        return false;
151    }
152
153    @Override
154    public String getRepositoryName() {
155        return session.getRepositoryName();
156    }
157
158    @Override
159    protected List<Schema> getProxySchemas() {
160        return proxySchemas;
161    }
162
163    @Override
164    public void remove() {
165        session.remove(getNode());
166    }
167
168    @Override
169    public void remove(NuxeoPrincipal principal) {
170        // principal is not yet used in SQL document
171        remove();
172    }
173
174    @Override
175    public void removeSingleton() {
176        throw new UnsupportedOperationException("Not implemented yet");
177    }
178
179    /**
180     * Reads into the {@link DocumentPart} the values from this {@link SQLDocument}.
181     */
182    @Override
183    public void readDocumentPart(DocumentPart dp) throws PropertyException {
184        readComplexProperty(getNode(), (ComplexProperty) dp);
185    }
186
187    @Override
188    public boolean writeDocumentPart(DocumentPart dp, WriteContext writeContext, boolean create)
189            throws PropertyException {
190        boolean changed = writeDocumentPart(getNode(), dp, writeContext, create);
191        clearDirtyFlags(dp);
192        return changed;
193    }
194
195    @Override
196    protected Node getChild(Node node, String name, Type type) throws PropertyException {
197        return session.getChildProperty(node, name, type.getName());
198    }
199
200    @Override
201    protected Node getChildForWrite(Node node, String name, Type type) throws PropertyException {
202        return session.getChildPropertyForWrite(node, name, type.getName());
203    }
204
205    @Override
206    protected List<Node> getChildAsList(Node node, String name) throws PropertyException {
207        return session.getComplexList(node, name);
208    }
209
210    @Override
211    protected void updateList(Node node, String name, Field field, String xpath, List<Object> values)
212            throws PropertyException {
213        List<Node> childNodes = getChildAsList(node, name);
214        int oldSize = childNodes.size();
215        int newSize = values.size();
216        // remove extra list elements
217        if (oldSize > newSize) {
218            for (int i = oldSize - 1; i >= newSize; i--) {
219                session.removeProperty(childNodes.remove(i));
220            }
221        }
222        // add new list elements
223        if (oldSize < newSize) {
224            String typeName = field.getType().getName();
225            for (int i = oldSize; i < newSize; i++) {
226                Node childNode = session.addChildProperty(node, name, Long.valueOf(i), typeName);
227                childNodes.add(childNode);
228            }
229        }
230        // write values
231        int i = 0;
232        for (Object v : values) {
233            Node childNode = childNodes.get(i);
234            setValueComplex(childNode, field, xpath + '/' + i, v);
235            i++;
236        }
237    }
238
239    @Override
240    protected List<Node> updateList(Node node, String name, Property property) throws PropertyException {
241        Collection<Property> properties = property.getChildren();
242        List<Node> childNodes = getChildAsList(node, name);
243        int oldSize = childNodes.size();
244        int newSize = properties.size();
245        // remove extra list elements
246        if (oldSize > newSize) {
247            for (int i = oldSize - 1; i >= newSize; i--) {
248                session.removeProperty(childNodes.remove(i));
249            }
250        }
251        // add new list elements
252        if (oldSize < newSize) {
253            String typeName = ((ListType) property.getType()).getFieldType().getName();
254            for (int i = oldSize; i < newSize; i++) {
255                Node childNode = session.addChildProperty(node, name, Long.valueOf(i), typeName);
256                childNodes.add(childNode);
257            }
258        }
259        return childNodes;
260    }
261
262    @Override
263    protected String internalName(String name) {
264        return name;
265    }
266
267    @Override
268    public Object getValue(String xpath) throws PropertyException {
269        return getValueObject(getNode(), xpath);
270    }
271
272    @Override
273    public void setValue(String xpath, Object value) throws PropertyException {
274        setValueObject(getNode(), xpath, value);
275    }
276
277    @Override
278    public void visitBlobs(Consumer<BlobAccessor> blobVisitor) throws PropertyException {
279        visitBlobs(getNode(), blobVisitor, NO_DIRTY);
280    }
281
282    @Override
283    public Serializable getPropertyValue(String name) {
284        return getNode().getSimpleProperty(name).getValue();
285    }
286
287    @Override
288    public void setPropertyValue(String name, Serializable value) {
289        getNode().setSimpleProperty(name, value);
290    }
291
292    protected static final Map<String, String> systemPropNameMap;
293
294    static {
295        systemPropNameMap = new HashMap<>();
296        systemPropNameMap.put(FULLTEXT_JOBID_SYS_PROP, Model.FULLTEXT_JOBID_PROP);
297        systemPropNameMap.put(IS_TRASHED_SYS_PROP, Model.MAIN_IS_TRASHED_PROP);
298    }
299
300    @Override
301    public void setSystemProp(String name, Serializable value) {
302        String propertyName;
303        if (name.startsWith(SIMPLE_TEXT_SYS_PROP)) {
304            propertyName = name.replace(SIMPLE_TEXT_SYS_PROP, Model.FULLTEXT_SIMPLETEXT_PROP);
305            if (session.isFulltextStoredInBlob()) {
306                // if binary fulltext is stored in blob, there is no simple fulltext available
307                return;
308            }
309        } else if (name.startsWith(BINARY_TEXT_SYS_PROP)) {
310            propertyName = name.replace(BINARY_TEXT_SYS_PROP, Model.FULLTEXT_BINARYTEXT_PROP);
311            if (session.isFulltextStoredInBlob()) {
312                if (!(value instanceof String)) {
313                    throw new PropertyException("Property " + name + " must be a string");
314                }
315                setPropertyBlobData(propertyName, (String) value);
316                return;
317            }
318        } else {
319            propertyName = systemPropNameMap.get(name);
320        }
321        if (propertyName == null) {
322            throw new PropertyNotFoundException(name, "Unknown system property");
323        }
324        setPropertyValue(propertyName, value);
325    }
326
327    @Override
328    @SuppressWarnings("unchecked")
329    public <T extends Serializable> T getSystemProp(String name, Class<T> type) {
330        String propertyName = systemPropNameMap.get(name);
331        if (propertyName == null) {
332            throw new PropertyNotFoundException(name, "Unknown system property");
333        }
334        Serializable value = getPropertyValue(propertyName);
335        if (value == null) {
336            if (type == Boolean.class) {
337                value = Boolean.FALSE;
338            } else if (type == Long.class) {
339                value = Long.valueOf(0);
340            }
341        }
342        return (T) value;
343    }
344
345    @Override
346    public String getChangeToken() {
347        if (session.isChangeTokenEnabled()) {
348            Long sysChangeToken = (Long) getPropertyValue(Model.MAIN_SYS_CHANGE_TOKEN_PROP);
349            Long changeToken = (Long) getPropertyValue(Model.MAIN_CHANGE_TOKEN_PROP);
350            return buildUserVisibleChangeToken(sysChangeToken, changeToken);
351        } else {
352            Calendar modified;
353            try {
354                modified = (Calendar) getPropertyValue(DC_MODIFIED);
355            } catch (PropertyNotFoundException e) {
356                modified = null;
357            }
358            return getLegacyChangeToken(modified);
359        }
360    }
361
362    @Override
363    public boolean validateUserVisibleChangeToken(String userVisibleChangeToken) {
364        if (userVisibleChangeToken == null) {
365            return true;
366        }
367        if (session.isChangeTokenEnabled()) {
368            Long sysChangeToken = (Long) getPropertyValue(Model.MAIN_SYS_CHANGE_TOKEN_PROP);
369            Long changeToken = (Long) getPropertyValue(Model.MAIN_CHANGE_TOKEN_PROP);
370            return validateUserVisibleChangeToken(sysChangeToken, changeToken, userVisibleChangeToken);
371        } else {
372            Calendar modified;
373            try {
374                modified = (Calendar) getPropertyValue(DC_MODIFIED);
375            } catch (PropertyNotFoundException e) {
376                modified = null;
377            }
378            return validateLegacyChangeToken(modified, userVisibleChangeToken);
379        }
380    }
381
382    @Override
383    public void markUserChange() {
384        session.markUserChange(getNode().getId());
385    }
386
387    /*
388     * ----- Retention and Hold -----
389     */
390
391    protected DocumentBlobManager getDocumentBlobManager() {
392        return Framework.getService(DocumentBlobManager.class);
393    }
394
395    @Override
396    public void makeRecord() {
397        setPropertyValue(Model.MAIN_IS_RECORD_PROP, Boolean.TRUE);
398        getDocumentBlobManager().notifyMakeRecord(this);
399    }
400
401    @Override
402    public boolean isRecord() {
403        return Boolean.TRUE.equals(getPropertyValue(Model.MAIN_IS_RECORD_PROP));
404    }
405
406    @Override
407    public void setRetainUntil(Calendar retainUntil) {
408        Calendar current = (Calendar) getPropertyValue(Model.MAIN_RETAIN_UNTIL_PROP);
409        if (!allowNewRetention(current, retainUntil)) {
410            throw new PropertyException(
411                    "Cannot reduce retention time from: " + (current == null ? "null" : current.toInstant()) + " to: "
412                            + (retainUntil == null ? "null" : retainUntil.toInstant()));
413        }
414        setPropertyValue(Model.MAIN_RETAIN_UNTIL_PROP, retainUntil);
415        getDocumentBlobManager().notifySetRetainUntil(this, retainUntil);
416    }
417
418    @Override
419    public Calendar getRetainUntil() {
420        return (Calendar) getPropertyValue(Model.MAIN_RETAIN_UNTIL_PROP);
421    }
422
423    @Override
424    public void setLegalHold(boolean hold) {
425        setPropertyValue(Model.MAIN_HAS_LEGAL_HOLD_PROP, hold ? Boolean.TRUE : null);
426        getDocumentBlobManager().notifySetLegalHold(this, hold);
427    }
428
429    @Override
430    public boolean hasLegalHold() {
431        return Boolean.TRUE.equals(getPropertyValue(Model.MAIN_HAS_LEGAL_HOLD_PROP));
432    }
433
434    @Override
435    public void setRetentionActive(boolean retentionActive) {
436        setPropertyValue(Model.MAIN_IS_RETENTION_ACTIVE_PROP, retentionActive ? Boolean.TRUE : null);
437    }
438
439    @Override
440    public boolean isRetentionActive() {
441        return Boolean.TRUE.equals(getPropertyValue(Model.MAIN_IS_RETENTION_ACTIVE_PROP));
442    }
443
444    /*
445     * ----- LifeCycle -----
446     */
447
448    @Override
449    public String getLifeCyclePolicy() {
450        return (String) getPropertyValue(Model.MISC_LIFECYCLE_POLICY_PROP);
451    }
452
453    @Override
454    public void setLifeCyclePolicy(String policy) {
455        setPropertyValue(Model.MISC_LIFECYCLE_POLICY_PROP, policy);
456        getDocumentBlobManager().notifyChanges(this, Collections.singleton(Model.MISC_LIFECYCLE_POLICY_PROP));
457    }
458
459    @Override
460    public String getLifeCycleState() {
461        return (String) getPropertyValue(Model.MISC_LIFECYCLE_STATE_PROP);
462    }
463
464    @Override
465    public void setCurrentLifeCycleState(String state) {
466        setPropertyValue(Model.MISC_LIFECYCLE_STATE_PROP, state);
467        getDocumentBlobManager().notifyChanges(this, Collections.singleton(Model.MISC_LIFECYCLE_STATE_PROP));
468    }
469
470    @Override
471    public void followTransition(String transition) throws LifeCycleException {
472        LifeCycleService service = Framework.getService(LifeCycleService.class);
473        if (service == null) {
474            throw new NuxeoException("LifeCycleService not available");
475        }
476        service.followTransition(this, transition);
477    }
478
479    @Override
480    public Collection<String> getAllowedStateTransitions() {
481        LifeCycleService service = Framework.getService(LifeCycleService.class);
482        if (service == null) {
483            throw new NuxeoException("LifeCycleService not available");
484        }
485        LifeCycle lifeCycle = service.getLifeCycleFor(this);
486        if (lifeCycle == null) {
487            return Collections.emptyList();
488        }
489        return lifeCycle.getAllowedStateTransitionsFrom(getLifeCycleState());
490    }
491
492    /*
493     * ----- org.nuxeo.ecm.core.versioning.VersionableDocument -----
494     */
495
496    @Override
497    public boolean isVersion() {
498        return false;
499    }
500
501    @Override
502    public Document getBaseVersion() {
503        if (isCheckedOut()) {
504            return null;
505        }
506        Serializable id = getPropertyValue(Model.MAIN_BASE_VERSION_PROP);
507        if (id == null) {
508            // shouldn't happen
509            return null;
510        }
511        return session.getDocumentById(id);
512    }
513
514    @Override
515    public String getVersionSeriesId() {
516        return getUUID();
517    }
518
519    @Override
520    public Document getSourceDocument() {
521        return this;
522    }
523
524    @Override
525    public Document checkIn(String label, String checkinComment) {
526        if (isRecord()) {
527            throw new PropertyException("Record cannot be checked in: " + getUUID());
528        }
529        Document version = session.checkIn(getNode(), label, checkinComment);
530        getDocumentBlobManager().freezeVersion(version);
531        return version;
532    }
533
534    @Override
535    public void checkOut() {
536        session.checkOut(getNode());
537    }
538
539    @Override
540    public boolean isCheckedOut() {
541        return !Boolean.TRUE.equals(getPropertyValue(Model.MAIN_CHECKED_IN_PROP));
542    }
543
544    @Override
545    public boolean isMajorVersion() {
546        return false;
547    }
548
549    @Override
550    public boolean isLatestVersion() {
551        return false;
552    }
553
554    @Override
555    public boolean isLatestMajorVersion() {
556        return false;
557    }
558
559    @Override
560    public boolean isVersionSeriesCheckedOut() {
561        return isCheckedOut();
562    }
563
564    @Override
565    public String getVersionLabel() {
566        return (String) getPropertyValue(Model.VERSION_LABEL_PROP);
567    }
568
569    @Override
570    public String getCheckinComment() {
571        return (String) getPropertyValue(Model.VERSION_DESCRIPTION_PROP);
572    }
573
574    @Override
575    public Document getWorkingCopy() {
576        return this;
577    }
578
579    @Override
580    public Calendar getVersionCreationDate() {
581        return (Calendar) getPropertyValue(Model.VERSION_CREATED_PROP);
582    }
583
584    @Override
585    public void restore(Document version) {
586        if (!version.isVersion()) {
587            throw new NuxeoException("Cannot restore a non-version: " + version);
588        }
589        session.restore(getNode(), ((SQLDocument) version).getNode());
590    }
591
592    @Override
593    public List<String> getVersionsIds() {
594        String versionSeriesId = getVersionSeriesId();
595        Collection<Document> versions = session.getVersions(versionSeriesId);
596        List<String> ids = new ArrayList<>(versions.size());
597        for (Document version : versions) {
598            ids.add(version.getUUID());
599        }
600        return ids;
601    }
602
603    @Override
604    public Document getVersion(String label) {
605        String versionSeriesId = getVersionSeriesId();
606        return session.getVersionByLabel(versionSeriesId, label);
607    }
608
609    @Override
610    public List<Document> getVersions() {
611        String versionSeriesId = getVersionSeriesId();
612        return session.getVersions(versionSeriesId);
613    }
614
615    @Override
616    public Document getLastVersion() {
617        String versionSeriesId = getVersionSeriesId();
618        return session.getLastVersion(versionSeriesId);
619    }
620
621    @Override
622    public Document getChild(String name) {
623        return session.getChild(getNode(), name);
624    }
625
626    @Override
627    public List<Document> getChildren() {
628        return session.getChildren(getNode()); // newly allocated
629    }
630
631    @Override
632    public List<String> getChildrenIds() {
633        // not optimized as this method doesn't seem to be used
634        List<Document> children = session.getChildren(getNode());
635        List<String> ids = new ArrayList<>(children.size());
636        for (Document child : children) {
637            ids.add(child.getUUID());
638        }
639        return ids;
640    }
641
642    @Override
643    public boolean hasChild(String name) {
644        return session.hasChild(getNode(), name);
645    }
646
647    @Override
648    public boolean hasChildren() {
649        return session.hasChildren(getNode());
650    }
651
652    @Override
653    public Document addChild(String name, String typeName) {
654        return session.addChild(getNode(), name, null, typeName);
655    }
656
657    @Override
658    public void orderBefore(String src, String dest) {
659        SQLDocument srcDoc = (SQLDocument) getChild(src);
660        if (srcDoc == null) {
661            throw new DocumentNotFoundException("Document " + this + " has no child: " + src);
662        }
663        SQLDocument destDoc;
664        if (dest == null) {
665            destDoc = null;
666        } else {
667            destDoc = (SQLDocument) getChild(dest);
668            if (destDoc == null) {
669                throw new DocumentNotFoundException("Document " + this + " has no child: " + dest);
670            }
671        }
672        session.orderBefore(getNode(), srcDoc.getNode(), destDoc == null ? null : destDoc.getNode());
673    }
674
675    @Override
676    public Set<String> getAllFacets() {
677        return getNode().getAllMixinTypes();
678    }
679
680    @Override
681    public String[] getFacets() {
682        return getNode().getMixinTypes();
683    }
684
685    @Override
686    public boolean hasFacet(String facet) {
687        return getNode().hasMixinType(facet);
688    }
689
690    @Override
691    public boolean addFacet(String facet) {
692        return session.addMixinType(getNode(), facet);
693    }
694
695    @Override
696    public boolean removeFacet(String facet) {
697        return session.removeMixinType(getNode(), facet);
698    }
699
700    /*
701     * ----- PropertyContainer inherited from SQLComplexProperty -----
702     */
703
704    /*
705     * ----- toString/equals/hashcode -----
706     */
707
708    @Override
709    public String toString() {
710        return getClass().getSimpleName() + '(' + getName() + ',' + getUUID() + ')';
711    }
712
713    @Override
714    public boolean equals(Object other) {
715        if (other == this) {
716            return true;
717        }
718        if (other == null) {
719            return false;
720        }
721        if (other.getClass() == this.getClass()) {
722            return equals((SQLDocumentLive) other);
723        }
724        return false;
725    }
726
727    private boolean equals(SQLDocumentLive other) {
728        return getNode().equals(other.getNode());
729    }
730
731    @Override
732    public int hashCode() {
733        return getNode().hashCode();
734    }
735
736    @Override
737    public Document getTargetDocument() {
738        return null;
739    }
740
741    @Override
742    public void setTargetDocument(Document target) {
743        throw new NuxeoException("Not a proxy");
744    }
745
746    @Override
747    protected Lock getDocumentLock() {
748        // lock manager can get the lock even on a recently created and unsaved document
749        throw new UnsupportedOperationException();
750    }
751
752    @Override
753    protected Lock setDocumentLock(Lock lock) {
754        // lock manager can set the lock even on a recently created and unsaved document
755        throw new UnsupportedOperationException();
756    }
757
758    @Override
759    protected Lock removeDocumentLock(String owner) {
760        // lock manager can remove the lock even on a recently created and unsaved document
761        throw new UnsupportedOperationException();
762    }
763
764}