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