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