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