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