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.getLocalService(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.getLocalService(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     * ----- LifeCycle -----
303     */
304
305    @Override
306    public String getLifeCyclePolicy() {
307        return target.getLifeCyclePolicy();
308    }
309
310    @Override
311    public void setLifeCyclePolicy(String policy) {
312        target.setLifeCyclePolicy(policy);
313    }
314
315    @Override
316    public String getLifeCycleState() {
317        return target.getLifeCycleState();
318    }
319
320    @Override
321    public void setCurrentLifeCycleState(String state) {
322        target.setCurrentLifeCycleState(state);
323    }
324
325    @Override
326    public void followTransition(String transition) throws LifeCycleException {
327        target.followTransition(transition);
328    }
329
330    @Override
331    public Collection<String> getAllowedStateTransitions() {
332        return target.getAllowedStateTransitions();
333    }
334
335    @Override
336    public Lock getLock() {
337        return target.getLock();
338    }
339
340    @Override
341    public Lock setLock(Lock lock) {
342        return target.setLock(lock);
343    }
344
345    @Override
346    public Lock removeLock(String owner) {
347        return target.removeLock(owner);
348    }
349
350    @Override
351    public boolean isVersion() {
352        return false;
353    }
354
355    @Override
356    public Document getBaseVersion() {
357        return target.getBaseVersion();
358    }
359
360    @Override
361    public String getVersionSeriesId() {
362        return target.getVersionSeriesId();
363    }
364
365    @Override
366    public Document getSourceDocument() {
367        // this is what the rest of Nuxeo expects for a proxy
368        return target;
369    }
370
371    @Override
372    public Document checkIn(String label, String checkinComment) {
373        return target.checkIn(label, checkinComment);
374    }
375
376    @Override
377    public void checkOut() {
378        target.checkOut();
379    }
380
381    @Override
382    public boolean isCheckedOut() {
383        return target.isCheckedOut();
384    }
385
386    @Override
387    public boolean isLatestVersion() {
388        return target.isLatestVersion();
389    }
390
391    @Override
392    public boolean isMajorVersion() {
393        return target.isMajorVersion();
394    }
395
396    @Override
397    public boolean isLatestMajorVersion() {
398        return target.isLatestMajorVersion();
399    }
400
401    @Override
402    public boolean isVersionSeriesCheckedOut() {
403        return target.isVersionSeriesCheckedOut();
404    }
405
406    @Override
407    public String getVersionLabel() {
408        return target.getVersionLabel();
409    }
410
411    @Override
412    public String getCheckinComment() {
413        return target.getCheckinComment();
414    }
415
416    @Override
417    public Document getWorkingCopy() {
418        return target.getWorkingCopy();
419    }
420
421    @Override
422    public Calendar getVersionCreationDate() {
423        return target.getVersionCreationDate();
424    }
425
426    @Override
427    public void restore(Document version) {
428        target.restore(version);
429    }
430
431    @Override
432    public List<String> getVersionsIds() {
433        return target.getVersionsIds();
434    }
435
436    @Override
437    public Document getVersion(String label) {
438        return target.getVersion(label);
439    }
440
441    @Override
442    public List<Document> getVersions() {
443        return target.getVersions();
444    }
445
446    @Override
447    public Document getLastVersion() {
448        return target.getLastVersion();
449    }
450
451    @Override
452    public Document getChild(String name) {
453        return proxy.getChild(name);
454    }
455
456    @Override
457    public List<Document> getChildren() {
458        return proxy.getChildren();
459    }
460
461    @Override
462    public List<String> getChildrenIds() {
463        return proxy.getChildrenIds();
464    }
465
466    @Override
467    public boolean hasChild(String name) {
468        return proxy.hasChild(name);
469    }
470
471    @Override
472    public boolean hasChildren() {
473        return proxy.hasChildren();
474    }
475
476    @Override
477    public Document addChild(String name, String typeName) {
478        return proxy.addChild(name, typeName);
479    }
480
481    @Override
482    public void orderBefore(String src, String dest) {
483        proxy.orderBefore(src, dest);
484    }
485
486    /*
487     * ----- DocumentProxy -----
488     */
489
490    @Override
491    public Document getTargetDocument() {
492        return target;
493    }
494
495    @Override
496    public void setTargetDocument(Document target) {
497        if (((SQLDocumentLive) proxy).isReadOnly()) {
498            throw new ReadOnlyPropertyException("Cannot write proxy: " + this);
499        }
500        if (!target.getVersionSeriesId().equals(getVersionSeriesId())) {
501            throw new ReadOnlyPropertyException("Cannot set proxy target to different version series");
502        }
503        getSession().setProxyTarget(proxy, target);
504        this.target = target;
505    }
506
507    @Override
508    public Serializable getPropertyValue(String name) {
509        if (isPropertyForProxy(name)) {
510            return proxy.getPropertyValue(name);
511        } else {
512            return target.getPropertyValue(name);
513        }
514    }
515
516    @Override
517    public void setPropertyValue(String name, Serializable value) {
518        if (isPropertyForProxy(name)) {
519            proxy.setPropertyValue(name, value);
520        } else {
521            target.setPropertyValue(name, value);
522        }
523    }
524
525    @Override
526    public Object getValue(String xpath) throws PropertyException {
527        if (isPropertyForProxy(xpath)) {
528            return proxy.getValue(xpath);
529        } else {
530            return target.getValue(xpath);
531        }
532    }
533
534    @Override
535    public void setValue(String xpath, Object value) throws PropertyException {
536        if (isPropertyForProxy(xpath)) {
537            proxy.setValue(xpath, value);
538        } else {
539            target.setValue(xpath, value);
540        }
541    }
542
543    @Override
544    public void visitBlobs(Consumer<BlobAccessor> blobVisitor) throws PropertyException {
545        // visit all blobs from the proxy AND the target
546        proxy.visitBlobs(blobVisitor);
547        target.visitBlobs(blobVisitor);
548    }
549
550    /*
551     * ----- toString/equals/hashcode -----
552     */
553
554    @Override
555    public String toString() {
556        return getClass().getSimpleName() + '(' + target + ',' + proxy.getUUID() + ')';
557    }
558
559    @Override
560    public boolean equals(Object other) {
561        if (other == this) {
562            return true;
563        }
564        if (other instanceof SQLDocumentProxy) {
565            return equals((SQLDocumentProxy) other);
566        }
567        return false;
568    }
569
570    private boolean equals(SQLDocumentProxy other) {
571        return proxy.equals(other.proxy) && target.equals(other.target);
572    }
573
574    @Override
575    public int hashCode() {
576        return proxy.hashCode() + target.hashCode();
577    }
578
579}