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