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