001/*
002 * (C) Copyright 2012 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 *     Thierry Delprat
018 */
019package org.nuxeo.snapshot;
020
021import java.io.Serializable;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collections;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Map;
028
029import org.nuxeo.common.utils.IdUtils;
030import org.nuxeo.common.utils.Path;
031import org.nuxeo.ecm.core.api.CoreSession;
032import org.nuxeo.ecm.core.api.DocumentModel;
033import org.nuxeo.ecm.core.api.DocumentModelList;
034import org.nuxeo.ecm.core.api.DocumentRef;
035import org.nuxeo.ecm.core.api.IdRef;
036import org.nuxeo.ecm.core.api.IterableQueryResult;
037import org.nuxeo.ecm.core.api.NuxeoException;
038import org.nuxeo.ecm.core.api.PropertyException;
039import org.nuxeo.ecm.core.api.VersioningOption;
040import org.nuxeo.ecm.core.api.impl.DocumentModelImpl;
041import org.nuxeo.ecm.core.event.EventService;
042import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
043import org.nuxeo.ecm.core.query.sql.NXQL;
044import org.nuxeo.ecm.core.schema.FacetNames;
045import org.nuxeo.runtime.api.Framework;
046
047public class SnapshotableAdapter implements Snapshot, Serializable {
048
049    private static final long serialVersionUID = 1L;
050
051    protected DocumentModel doc;
052
053    public static final String SCHEMA = "snapshot";
054
055    public static final String CHILDREN_PROP = "snap:children";
056
057    public static final String NAME_PROP = "snap:originalName";
058
059    public SnapshotableAdapter(DocumentModel doc) {
060        this.doc = doc;
061    }
062
063    @Override
064    public DocumentModel getDocument() {
065        return doc;
066    }
067
068    @Override
069    public DocumentRef getRef() {
070        return doc.getRef();
071    }
072
073    protected DocumentRef createLeafVersion(DocumentModel targetDoc, VersioningOption option) {
074        if (targetDoc.isFolder() && !targetDoc.hasSchema(SCHEMA)) {
075            throw new NuxeoException("Can not version a folder that has not snapshot schema");
076        }
077        if (targetDoc.isVersion()) {
078            return targetDoc.getRef();
079        }
080        if (!targetDoc.isProxy() && !targetDoc.isCheckedOut()) {
081            return targetDoc.getCoreSession().getBaseVersion(targetDoc.getRef());
082        }
083        if (targetDoc.isProxy()) {
084            DocumentModel proxyTarget = targetDoc.getCoreSession().getDocument(new IdRef(targetDoc.getSourceId()));
085            if (proxyTarget.isVersion()) {
086                // standard proxy : nothing to snapshot
087                return targetDoc.getRef();
088            } else {
089                // live proxy
090                // create a new leaf with target doc ?
091                return createLeafVersion(proxyTarget, option);
092
093                // create a new proxy ??
094                // XXX
095            }
096
097        }
098
099        // Fire event to change document
100        DocumentEventContext ctx = new DocumentEventContext(targetDoc.getCoreSession(),
101                targetDoc.getCoreSession().getPrincipal(), targetDoc);
102        ctx.setProperty(ROOT_DOCUMENT_PROPERTY, doc);
103
104        Framework.getService(EventService.class).fireEvent(ctx.newEvent(ABOUT_TO_CREATE_LEAF_VERSION_EVENT));
105        // Save only if needed
106        if (targetDoc.isDirty()) {
107            targetDoc.getCoreSession().saveDocument(targetDoc);
108        }
109
110        return targetDoc.getCoreSession().checkIn(targetDoc.getRef(), option, null);
111    }
112
113    protected DocumentModel createLeafVersionAndFetch(VersioningOption option) {
114        DocumentRef versionRef = createLeafVersion(doc, option);
115        DocumentModel version = doc.getCoreSession().getDocument(versionRef);
116        return version;
117    }
118
119    @Override
120    public Snapshot createSnapshot(VersioningOption option) {
121
122        if (!doc.isFolder()) {
123            if (doc.isCheckedOut()) {
124                return new SnapshotableAdapter(createLeafVersionAndFetch(option));
125            } else {
126                return new SnapshotableAdapter(doc);
127            }
128        }
129
130        if (!doc.hasFacet(Snapshot.FACET)) {
131            doc.addFacet(Snapshot.FACET);
132        }
133
134        if (!doc.hasFacet(FacetNames.VERSIONABLE)) {
135            doc.addFacet(FacetNames.VERSIONABLE);
136        }
137
138        List<DocumentModel> folders = new ArrayList<>();
139        List<DocumentModel> leafs = new ArrayList<>();
140        String[] vuuids = null;
141
142        doc.getCoreSession().save();
143        String query = "SELECT ecm:uuid FROM Document WHERE ecm:parentId = '" + doc.getId()
144                + "' AND ecm:isTrashed = 0 ORDER BY ecm:pos";
145        try (IterableQueryResult res = doc.getCoreSession().queryAndFetch(query, "NXQL")) {
146
147            vuuids = new String[(int) res.size()];
148            for (Map<String, Serializable> item : res) {
149                DocumentModel child = doc.getCoreSession().getDocument(new IdRef((String) item.get(NXQL.ECM_UUID)));
150                if (child.isFolder()) {
151                    folders.add(child);
152                } else {
153                    leafs.add(child);
154                }
155            }
156
157        }
158
159        int i = 0;
160
161        for (DocumentModel child : leafs) {
162            DocumentRef docRef = createLeafVersion(child, option);
163            String versionUuid = docRef.toString();
164            vuuids[i++] = versionUuid;
165        }
166
167        for (DocumentModel child : folders) {
168            SnapshotableAdapter adapter = new SnapshotableAdapter(child);
169            Snapshot snap = adapter.createSnapshot(option);
170            String versionUuid = snap.getRef().toString();
171            vuuids[i++] = versionUuid;
172        }
173
174        // check if a snapshot is needed
175        boolean mustSnapshot = false;
176        if (doc.isCheckedOut()) {
177            mustSnapshot = true;
178        } else {
179            String[] existingUUIds = (String[]) doc.getPropertyValue(CHILDREN_PROP);
180            if (existingUUIds == null) {
181                existingUUIds = new String[0];
182            }
183            if (doc.hasFacet(FacetNames.ORDERABLE)) {
184                // ordered, exact comparison
185                if (!Arrays.equals(vuuids, existingUUIds)) {
186                    mustSnapshot = true;
187                }
188            } else {
189                // not ordered, use unordered comparison
190                if (!new HashSet<>(Arrays.asList(vuuids)).equals(new HashSet<>(Arrays.asList(existingUUIds)))) {
191                    mustSnapshot = true;
192                }
193            }
194        }
195
196        if (mustSnapshot) {
197            doc.setPropertyValue(CHILDREN_PROP, vuuids);
198            doc.setPropertyValue(NAME_PROP, doc.getName());
199            doc = doc.getCoreSession().saveDocument(doc);
200            return new SnapshotableAdapter(createLeafVersionAndFetch(option));
201        } else {
202            DocumentModel lastversion = doc.getCoreSession().getLastDocumentVersion(doc.getRef());
203            return new SnapshotableAdapter(lastversion);
204        }
205    }
206
207    protected List<DocumentModel> getChildren(DocumentModel target) {
208        if (!target.isVersion()) {
209            throw new NuxeoException("Not a version:");
210        }
211
212        if (!target.isFolder()) {
213            return Collections.emptyList();
214        }
215
216        if (target.isFolder() && !target.hasSchema(SCHEMA)) {
217            throw new NuxeoException("Folderish children should have the snapshot schema");
218        }
219
220        try {
221
222            String[] uuids = (String[]) target.getPropertyValue(CHILDREN_PROP);
223
224            if (uuids != null && uuids.length > 0) {
225                DocumentRef[] refs = new DocumentRef[uuids.length];
226                for (int i = 0; i < uuids.length; i++) {
227                    refs[i] = new IdRef(uuids[i]);
228                }
229                return target.getCoreSession().getDocuments(refs);
230            }
231        } catch (PropertyException e) {
232            e.printStackTrace();
233        }
234
235        return Collections.emptyList();
236    }
237
238    @Override
239    public List<DocumentModel> getChildren() {
240        return getChildren(doc);
241    }
242
243    @Override
244    public List<Snapshot> getChildrenSnapshots() {
245
246        List<Snapshot> snaps = new ArrayList<Snapshot>();
247
248        for (DocumentModel child : getChildren()) {
249            snaps.add(new SnapshotableAdapter(child));
250        }
251
252        return snaps;
253    }
254
255    protected void fillFlatTree(List<Snapshot> list) {
256        for (Snapshot snap : getChildrenSnapshots()) {
257            list.add(snap);
258            if (snap.getDocument().isFolder()) {
259                ((SnapshotableAdapter) snap).fillFlatTree(list);
260            }
261        }
262    }
263
264    @Override
265    public List<Snapshot> getFlatTree() {
266        List<Snapshot> list = new ArrayList<Snapshot>();
267
268        fillFlatTree(list);
269
270        return list;
271    }
272
273    protected void dump(int level, StringBuffer sb) {
274        for (Snapshot snap : getChildrenSnapshots()) {
275            sb.append(new String(new char[level]).replace('\0', ' '));
276            sb.append(snap.getDocument().getName() + " -- " + snap.getDocument().getVersionLabel());
277            sb.append("\n");
278            if (snap.getDocument().isFolder()) {
279                ((SnapshotableAdapter) snap).dump(level + 1, sb);
280            }
281        }
282    }
283
284    @Override
285    public String toString() {
286        StringBuffer sb = new StringBuffer();
287        sb.append(doc.getName() + " -- " + doc.getVersionLabel());
288        sb.append("\n");
289
290        dump(1, sb);
291
292        return sb.toString();
293    }
294
295    protected DocumentModel getVersionForLabel(DocumentModel target, String versionLabel) {
296        List<DocumentModel> versions = target.getCoreSession().getVersions(target.getRef());
297        for (DocumentModel version : versions) {
298            if (version.getVersionLabel().equals(versionLabel)) {
299                return version;
300            }
301        }
302        return null;
303    }
304
305    protected DocumentModel getCheckoutDocument(DocumentModel target) {
306        if (target.isVersion()) {
307            target = target.getCoreSession().getDocument(new IdRef(doc.getSourceId()));
308        }
309        return target;
310    }
311
312    protected DocumentModel restore(DocumentModel leafVersion, DocumentModel target, boolean first,
313            DocumentModelList olddocs) {
314
315        CoreSession session = doc.getCoreSession();
316
317        if (leafVersion == null) {
318            return null;
319        }
320
321        if (target.isFolder() && first) {
322            // save all subtree
323            olddocs = session.query(
324                    "select * from Document where ecm:path STARTSWITH " + NXQL.escapeString(target.getPathAsString()));
325            if (olddocs.size() > 0) {
326                DocumentModel container = session.createDocumentModel(target.getPath().removeLastSegments(1).toString(),
327                        target.getName() + "_tmp", "Folder");
328                container = session.createDocument(container);
329                for (DocumentModel oldChild : olddocs) {
330                    session.move(oldChild.getRef(), container.getRef(), oldChild.getName());
331                }
332                olddocs.add(container);
333            }
334        }
335
336        // restore leaf
337        target = session.restoreToVersion(target.getRef(), leafVersion.getRef());
338
339        // restore children
340        for (DocumentModel child : getChildren(leafVersion)) {
341
342            String liveUUID = child.getVersionSeriesId();
343            DocumentModel placeholder = null;
344            for (DocumentModel doc : olddocs) {
345                if (doc.getId().equals(liveUUID)) {
346                    placeholder = doc;
347                    break;
348                }
349            }
350            if (placeholder == null) {
351                if (session.exists(new IdRef(liveUUID))) {
352                    placeholder = session.getDocument(new IdRef(liveUUID));
353                }
354            }
355            if (placeholder != null) {
356                olddocs.remove(placeholder);
357                session.move(placeholder.getRef(), target.getRef(), placeholder.getName());
358            } else {
359                String name = child.getName();
360                // name will be null if there is no checkecout version
361                // need to rebuild name
362                if (name == null && child.hasSchema(SCHEMA)) {
363                    name = (String) child.getPropertyValue(NAME_PROP);
364                }
365                if (name == null && child.getTitle() != null) {
366                    name = IdUtils.generateId(child.getTitle(), "-", true, 24);
367                }
368                if (name == null) {
369                    name = child.getType() + System.currentTimeMillis();
370                }
371                placeholder = new DocumentModelImpl((String) null, child.getType(), liveUUID, new Path(name), null,
372                        null, target.getRef(), null, null, null, null);
373                placeholder.putContextData(CoreSession.IMPORT_CHECKED_IN, Boolean.TRUE);
374                placeholder.addFacet(Snapshot.FACET);
375                placeholder.addFacet(FacetNames.VERSIONABLE);
376                session.importDocuments(Collections.singletonList(placeholder));
377                placeholder = session.getDocument(new IdRef(liveUUID));
378            }
379
380            new SnapshotableAdapter(child).restore(child, placeholder, false, olddocs);
381        }
382
383        if (first) {
384            for (DocumentModel old : olddocs) {
385                session.removeDocument(old.getRef());
386            }
387        }
388        return target;
389    }
390
391    @Override
392    public DocumentModel restore(String versionLabel) {
393        DocumentModel target = getCheckoutDocument(doc);
394        DocumentModel leafVersion = getVersionForLabel(target, versionLabel);
395        DocumentModel restoredDoc = restore(leafVersion, target, true, null);
396        return restoredDoc;
397    }
398
399}