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