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.Calendar;
023import java.util.Collection;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027import java.util.function.Consumer;
028
029import org.nuxeo.ecm.core.api.LifeCycleException;
030import org.nuxeo.ecm.core.api.Lock;
031import org.nuxeo.ecm.core.api.PropertyException;
032import org.nuxeo.ecm.core.api.model.DocumentPart;
033import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
034import org.nuxeo.ecm.core.api.model.ReadOnlyPropertyException;
035import org.nuxeo.ecm.core.model.Document;
036import org.nuxeo.ecm.core.model.Session;
037import org.nuxeo.ecm.core.schema.DocumentType;
038import org.nuxeo.ecm.core.schema.SchemaManager;
039import org.nuxeo.ecm.core.schema.types.ComplexType;
040import org.nuxeo.ecm.core.schema.types.Schema;
041import org.nuxeo.ecm.core.storage.sql.Model;
042import org.nuxeo.ecm.core.storage.sql.Node;
043import org.nuxeo.runtime.api.Framework;
044
045/**
046 * A proxy is a shortcut to a target document (a version or normal document).
047 */
048public class SQLDocumentProxy implements SQLDocument {
049
050    /** The proxy seen as a normal doc ({@link SQLDocument}). */
051    private final Document proxy;
052
053    /** The target. */
054    private Document target;
055
056    // private SQLDocumentVersion version;
057
058    protected SQLDocumentProxy(Document proxy, Document target) {
059        this.proxy = proxy;
060        this.target = target;
061    }
062
063    protected String getSchema(String xpath) {
064        int p = xpath.indexOf(':');
065        if (p == -1) {
066            throw new PropertyNotFoundException(xpath, "Schema not specified");
067        }
068        String prefix = xpath.substring(0, p);
069        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
070        Schema schema = schemaManager.getSchemaFromPrefix(prefix);
071        if (schema == null) {
072            schema = schemaManager.getSchema(prefix);
073            if (schema == null) {
074                throw new PropertyNotFoundException(xpath, "No schema for prefix");
075            }
076        }
077        return schema.getName();
078    }
079
080    /**
081     * Checks if the given schema should be resolved on the proxy or the target.
082     */
083    protected boolean isSchemaForProxy(String schema) {
084        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
085        return schemaManager.isProxySchema(schema, getType().getName());
086    }
087
088    /**
089     * Checks if the given property should be resolved on the proxy or the target.
090     */
091    protected boolean isPropertyForProxy(String xpath) {
092        if (Model.MAIN_MINOR_VERSION_PROP.equals(xpath) || Model.MAIN_MAJOR_VERSION_PROP.equals(xpath)) {
093            return false;
094        }
095        return isSchemaForProxy(getSchema(xpath));
096    }
097
098    /*
099     * ----- SQLDocument -----
100     */
101
102    @Override
103    public Node getNode() {
104        return ((SQLDocument) proxy).getNode();
105    }
106
107    /*
108     * ----- Document -----
109     */
110
111    @Override
112    public boolean isProxy() {
113        return true;
114    }
115
116    @Override
117    public String getUUID() {
118        return proxy.getUUID();
119    }
120
121    @Override
122    public String getName() {
123        return proxy.getName();
124    }
125
126    @Override
127    public Long getPos() {
128        return proxy.getPos();
129    }
130
131    @Override
132    public Document getParent() {
133        return proxy.getParent();
134    }
135
136    @Override
137    public String getPath() {
138        return proxy.getPath();
139    }
140
141    @Override
142    public void remove() {
143        proxy.remove();
144    }
145
146    @Override
147    public DocumentType getType() {
148        return target.getType();
149    }
150
151    @Override
152    public String getRepositoryName() {
153        return target.getRepositoryName();
154    }
155
156    @Override
157    public Session getSession() {
158        return target.getSession();
159    }
160
161    @Override
162    public boolean isFolder() {
163        return target.isFolder();
164    }
165
166    @Override
167    public void setReadOnly(boolean readonly) {
168        target.setReadOnly(readonly);
169    }
170
171    @Override
172    public boolean isReadOnly() {
173        return target.isReadOnly();
174    }
175
176    @Override
177    public void readDocumentPart(DocumentPart dp) throws PropertyException {
178        if (isSchemaForProxy(dp.getName())) {
179            proxy.readDocumentPart(dp);
180        } else {
181            target.readDocumentPart(dp);
182        }
183    }
184
185    @Override
186    public Map<String, Serializable> readPrefetch(ComplexType complexType, Set<String> xpaths)
187            throws PropertyException {
188        if (isSchemaForProxy(complexType.getName())) {
189            return proxy.readPrefetch(complexType, xpaths);
190        } else {
191            return target.readPrefetch(complexType, xpaths);
192        }
193    }
194
195    @Override
196    public WriteContext getWriteContext() {
197        // proxy or target doesn't matter, this is about typing
198        return proxy.getWriteContext();
199    }
200
201    @Override
202    public boolean writeDocumentPart(DocumentPart dp, WriteContext writeContext) throws PropertyException {
203        if (isSchemaForProxy(dp.getName())) {
204            return proxy.writeDocumentPart(dp, writeContext);
205        } else {
206            return target.writeDocumentPart(dp, writeContext);
207        }
208    }
209
210    @Override
211    public void setSystemProp(String name, Serializable value) {
212        target.setSystemProp(name, value);
213    }
214
215    @Override
216    public <T extends Serializable> T getSystemProp(String name, Class<T> type) {
217        return target.getSystemProp(name, type);
218    }
219
220    public static final String CHANGE_TOKEN_PROXY_SEP = "/";
221
222    /*
223     * The change token for a proxy must reflect the fact that either the proxy (name, parent, acls, etc.) or its target
224     * may be changed.
225     */
226    @Override
227    public String getChangeToken() {
228        String proxyToken = proxy.getChangeToken();
229        String targetToken = target.getChangeToken();
230        return getProxyChangeToken(proxyToken, targetToken);
231    }
232
233    protected static String getProxyChangeToken(String proxyToken, String targetToken) {
234        if (proxyToken == null && targetToken == null) {
235            return null;
236        } else {
237            if (proxyToken == null) {
238                proxyToken = "";
239            } else if (targetToken == null) {
240                targetToken = "";
241            }
242            return proxyToken + CHANGE_TOKEN_PROXY_SEP + targetToken;
243        }
244    }
245
246    @Override
247    public boolean validateUserVisibleChangeToken(String userVisibleChangeToken) {
248        if (userVisibleChangeToken == null) {
249            return true;
250        }
251        String[] parts = userVisibleChangeToken.split(CHANGE_TOKEN_PROXY_SEP, 2);
252        if (parts.length != 2) {
253            // invalid format
254            return false;
255        }
256        String proxyToken = parts[0];
257        if (proxyToken.isEmpty()) {
258            proxyToken = null;
259        }
260        String targetToken = parts[1];
261        if (targetToken.isEmpty()) {
262            targetToken = null;
263        }
264        if (proxyToken == null && targetToken == null) {
265            return true;
266        }
267        return proxy.validateUserVisibleChangeToken(proxyToken) && target.validateUserVisibleChangeToken(targetToken);
268    }
269
270    @Override
271    public void markUserChange() {
272        proxy.markUserChange();
273        target.markUserChange();
274    }
275
276    @Override
277    public Set<String> getAllFacets() {
278        return target.getAllFacets(); // TODO proxy facets
279    }
280
281    @Override
282    public String[] getFacets() {
283        return target.getFacets(); // TODO proxy facets
284    }
285
286    @Override
287    public boolean hasFacet(String facet) {
288        return target.hasFacet(facet); // TODO proxy facets
289    }
290
291    @Override
292    public boolean addFacet(String facet) {
293        return target.addFacet(facet); // TODO proxy facets
294    }
295
296    @Override
297    public boolean removeFacet(String facet) {
298        return target.removeFacet(facet); // TODO proxy facets
299    }
300
301    /*
302     * ----- Retention -----
303     */
304
305    @Override
306    public void setRetentionActive(boolean retentionActive) {
307        target.setRetentionActive(retentionActive);
308    }
309
310    @Override
311    public boolean isRetentionActive() {
312        return target.isRetentionActive();
313    }
314
315    /*
316     * ----- LifeCycle -----
317     */
318
319    @Override
320    public String getLifeCyclePolicy() {
321        return target.getLifeCyclePolicy();
322    }
323
324    @Override
325    public void setLifeCyclePolicy(String policy) {
326        target.setLifeCyclePolicy(policy);
327    }
328
329    @Override
330    public String getLifeCycleState() {
331        return target.getLifeCycleState();
332    }
333
334    @Override
335    public void setCurrentLifeCycleState(String state) {
336        target.setCurrentLifeCycleState(state);
337    }
338
339    @Override
340    public void followTransition(String transition) throws LifeCycleException {
341        target.followTransition(transition);
342    }
343
344    @Override
345    public Collection<String> getAllowedStateTransitions() {
346        return target.getAllowedStateTransitions();
347    }
348
349    @Override
350    public Lock getLock() {
351        return target.getLock();
352    }
353
354    @Override
355    public Lock setLock(Lock lock) {
356        return target.setLock(lock);
357    }
358
359    @Override
360    public Lock removeLock(String owner) {
361        return target.removeLock(owner);
362    }
363
364    @Override
365    public boolean isVersion() {
366        return false;
367    }
368
369    @Override
370    public Document getBaseVersion() {
371        return target.getBaseVersion();
372    }
373
374    @Override
375    public String getVersionSeriesId() {
376        return target.getVersionSeriesId();
377    }
378
379    @Override
380    public Document getSourceDocument() {
381        // this is what the rest of Nuxeo expects for a proxy
382        return target;
383    }
384
385    @Override
386    public Document checkIn(String label, String checkinComment) {
387        return target.checkIn(label, checkinComment);
388    }
389
390    @Override
391    public void checkOut() {
392        target.checkOut();
393    }
394
395    @Override
396    public boolean isCheckedOut() {
397        return target.isCheckedOut();
398    }
399
400    @Override
401    public boolean isLatestVersion() {
402        return target.isLatestVersion();
403    }
404
405    @Override
406    public boolean isMajorVersion() {
407        return target.isMajorVersion();
408    }
409
410    @Override
411    public boolean isLatestMajorVersion() {
412        return target.isLatestMajorVersion();
413    }
414
415    @Override
416    public boolean isVersionSeriesCheckedOut() {
417        return target.isVersionSeriesCheckedOut();
418    }
419
420    @Override
421    public String getVersionLabel() {
422        return target.getVersionLabel();
423    }
424
425    @Override
426    public String getCheckinComment() {
427        return target.getCheckinComment();
428    }
429
430    @Override
431    public Document getWorkingCopy() {
432        return target.getWorkingCopy();
433    }
434
435    @Override
436    public Calendar getVersionCreationDate() {
437        return target.getVersionCreationDate();
438    }
439
440    @Override
441    public void restore(Document version) {
442        target.restore(version);
443    }
444
445    @Override
446    public List<String> getVersionsIds() {
447        return target.getVersionsIds();
448    }
449
450    @Override
451    public Document getVersion(String label) {
452        return target.getVersion(label);
453    }
454
455    @Override
456    public List<Document> getVersions() {
457        return target.getVersions();
458    }
459
460    @Override
461    public Document getLastVersion() {
462        return target.getLastVersion();
463    }
464
465    @Override
466    public Document getChild(String name) {
467        return proxy.getChild(name);
468    }
469
470    @Override
471    public List<Document> getChildren() {
472        return proxy.getChildren();
473    }
474
475    @Override
476    public List<String> getChildrenIds() {
477        return proxy.getChildrenIds();
478    }
479
480    @Override
481    public boolean hasChild(String name) {
482        return proxy.hasChild(name);
483    }
484
485    @Override
486    public boolean hasChildren() {
487        return proxy.hasChildren();
488    }
489
490    @Override
491    public Document addChild(String name, String typeName) {
492        return proxy.addChild(name, typeName);
493    }
494
495    @Override
496    public void orderBefore(String src, String dest) {
497        proxy.orderBefore(src, dest);
498    }
499
500    /*
501     * ----- DocumentProxy -----
502     */
503
504    @Override
505    public Document getTargetDocument() {
506        return target;
507    }
508
509    @Override
510    public void setTargetDocument(Document target) {
511        if (((SQLDocumentLive) proxy).isReadOnly()) {
512            throw new ReadOnlyPropertyException("Cannot write proxy: " + this);
513        }
514        if (!target.getVersionSeriesId().equals(getVersionSeriesId())) {
515            throw new ReadOnlyPropertyException("Cannot set proxy target to different version series");
516        }
517        getSession().setProxyTarget(proxy, target);
518        this.target = target;
519    }
520
521    @Override
522    public Serializable getPropertyValue(String name) {
523        if (isPropertyForProxy(name)) {
524            return proxy.getPropertyValue(name);
525        } else {
526            return target.getPropertyValue(name);
527        }
528    }
529
530    @Override
531    public void setPropertyValue(String name, Serializable value) {
532        if (isPropertyForProxy(name)) {
533            proxy.setPropertyValue(name, value);
534        } else {
535            target.setPropertyValue(name, value);
536        }
537    }
538
539    @Override
540    public Object getValue(String xpath) throws PropertyException {
541        if (isPropertyForProxy(xpath)) {
542            return proxy.getValue(xpath);
543        } else {
544            return target.getValue(xpath);
545        }
546    }
547
548    @Override
549    public void setValue(String xpath, Object value) throws PropertyException {
550        if (isPropertyForProxy(xpath)) {
551            proxy.setValue(xpath, value);
552        } else {
553            target.setValue(xpath, value);
554        }
555    }
556
557    @Override
558    public void visitBlobs(Consumer<BlobAccessor> blobVisitor) throws PropertyException {
559        // visit all blobs from the proxy AND the target
560        proxy.visitBlobs(blobVisitor);
561        target.visitBlobs(blobVisitor);
562    }
563
564    /*
565     * ----- toString/equals/hashcode -----
566     */
567
568    @Override
569    public String toString() {
570        return getClass().getSimpleName() + '(' + target + ',' + proxy.getUUID() + ')';
571    }
572
573    @Override
574    public boolean equals(Object other) {
575        if (other == this) {
576            return true;
577        }
578        if (other instanceof SQLDocumentProxy) {
579            return equals((SQLDocumentProxy) other);
580        }
581        return false;
582    }
583
584    private boolean equals(SQLDocumentProxy other) {
585        return proxy.equals(other.proxy) && target.equals(other.target);
586    }
587
588    @Override
589    public int hashCode() {
590        return proxy.hashCode() + target.hashCode();
591    }
592
593}