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