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