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 (doc.hasFacet(FacetNames.ORDERABLE)) {
156                // ordered, exact comparison
157                if (!Arrays.equals(vuuids, existingUUIds)) {
158                    mustSnapshot = true;
159                }
160            } else {
161                // not ordered, use unordered comparison
162                if (!new HashSet<>(Arrays.asList(vuuids)).equals(new HashSet<>(Arrays.asList(existingUUIds)))) {
163                    mustSnapshot = true;
164                }
165            }
166        }
167
168        if (mustSnapshot) {
169            doc.setPropertyValue(CHILDREN_PROP, vuuids);
170            doc.setPropertyValue(NAME_PROP, doc.getName());
171            doc = doc.getCoreSession().saveDocument(doc);
172            return new SnapshotableAdapter(createLeafVersionAndFetch(option));
173        } else {
174            DocumentModel lastversion = doc.getCoreSession().getLastDocumentVersion(doc.getRef());
175            return new SnapshotableAdapter(lastversion);
176        }
177    }
178
179    protected List<DocumentModel> getChildren(DocumentModel target) {
180        if (!target.isVersion()) {
181            throw new NuxeoException("Not a version:");
182        }
183
184        if (!target.isFolder()) {
185            return Collections.emptyList();
186        }
187
188        if (target.isFolder() && !target.hasSchema(SCHEMA)) {
189            throw new NuxeoException("Folderish children should have the snapshot schema");
190        }
191
192        try {
193
194            String[] uuids = (String[]) target.getPropertyValue(CHILDREN_PROP);
195
196            if (uuids != null && uuids.length > 0) {
197                DocumentRef[] refs = new DocumentRef[uuids.length];
198                for (int i = 0; i < uuids.length; i++) {
199                    refs[i] = new IdRef(uuids[i]);
200                }
201                return target.getCoreSession().getDocuments(refs);
202            }
203        } catch (PropertyException e) {
204            e.printStackTrace();
205        }
206
207        return Collections.emptyList();
208    }
209
210    @Override
211    public List<DocumentModel> getChildren() {
212        return getChildren(doc);
213    }
214
215    @Override
216    public List<Snapshot> getChildrenSnapshots() {
217
218        List<Snapshot> snaps = new ArrayList<Snapshot>();
219
220        for (DocumentModel child : getChildren()) {
221            snaps.add(new SnapshotableAdapter(child));
222        }
223
224        return snaps;
225    }
226
227    protected void fillFlatTree(List<Snapshot> list) {
228        for (Snapshot snap : getChildrenSnapshots()) {
229            list.add(snap);
230            if (snap.getDocument().isFolder()) {
231                ((SnapshotableAdapter) snap).fillFlatTree(list);
232            }
233        }
234    }
235
236    public List<Snapshot> getFlatTree() {
237        List<Snapshot> list = new ArrayList<Snapshot>();
238
239        fillFlatTree(list);
240
241        return list;
242    }
243
244    protected void dump(int level, StringBuffer sb) {
245        for (Snapshot snap : getChildrenSnapshots()) {
246            sb.append(new String(new char[level]).replace('\0', ' '));
247            sb.append(snap.getDocument().getName() + " -- " + snap.getDocument().getVersionLabel());
248            sb.append("\n");
249            if (snap.getDocument().isFolder()) {
250                ((SnapshotableAdapter) snap).dump(level + 1, sb);
251            }
252        }
253    }
254
255    @Override
256    public String toString() {
257        StringBuffer sb = new StringBuffer();
258        sb.append(doc.getName() + " -- " + doc.getVersionLabel());
259        sb.append("\n");
260
261        dump(1, sb);
262
263        return sb.toString();
264    }
265
266    protected DocumentModel getVersionForLabel(DocumentModel target, String versionLabel) {
267        List<DocumentModel> versions = target.getCoreSession().getVersions(target.getRef());
268        for (DocumentModel version : versions) {
269            if (version.getVersionLabel().equals(versionLabel)) {
270                return version;
271            }
272        }
273        return null;
274    }
275
276    protected DocumentModel getCheckoutDocument(DocumentModel target) {
277        if (target.isVersion()) {
278            target = target.getCoreSession().getDocument(new IdRef(doc.getSourceId()));
279        }
280        return target;
281    }
282
283    protected DocumentModel restore(DocumentModel leafVersion, DocumentModel target, boolean first,
284            DocumentModelList olddocs) {
285
286        CoreSession session = doc.getCoreSession();
287
288        if (leafVersion == null) {
289            return null;
290        }
291
292        if (target.isFolder() && first) {
293            // save all subtree
294            olddocs = session.query("select * from Document where ecm:path STARTSWITH '" + target.getPathAsString()
295                    + "'");
296            if (olddocs.size() > 0) {
297                DocumentModel container = session.createDocumentModel(
298                        target.getPath().removeLastSegments(1).toString(), target.getName() + "_tmp", "Folder");
299                container = session.createDocument(container);
300                for (DocumentModel oldChild : olddocs) {
301                    session.move(oldChild.getRef(), container.getRef(), oldChild.getName());
302                }
303                olddocs.add(container);
304            }
305        }
306
307        // restore leaf
308        target = session.restoreToVersion(target.getRef(), leafVersion.getRef());
309
310        // restore children
311        for (DocumentModel child : getChildren(leafVersion)) {
312
313            String liveUUID = child.getVersionSeriesId();
314            DocumentModel placeholder = null;
315            for (DocumentModel doc : olddocs) {
316                if (doc.getId().equals(liveUUID)) {
317                    placeholder = doc;
318                    break;
319                }
320            }
321            if (placeholder == null) {
322                if (session.exists(new IdRef(liveUUID))) {
323                    placeholder = session.getDocument(new IdRef(liveUUID));
324                }
325            }
326            if (placeholder != null) {
327                olddocs.remove(placeholder);
328                session.move(placeholder.getRef(), target.getRef(), placeholder.getName());
329            } else {
330                String name = child.getName();
331                // name will be null if there is no checkecout version
332                // need to rebuild name
333                if (name == null && child.hasSchema(SCHEMA)) {
334                    name = (String) child.getPropertyValue(NAME_PROP);
335                }
336                if (name == null && child.getTitle() != null) {
337                    name = IdUtils.generateId(child.getTitle(), "-", true, 24);;
338                }
339                if (name == null) {
340                    name = child.getType() + System.currentTimeMillis();
341                }
342                placeholder = new DocumentModelImpl((String) null, child.getType(), liveUUID, new Path(name), null,
343                        null, target.getRef(), null, null, null, null);
344                placeholder.putContextData(CoreSession.IMPORT_CHECKED_IN, Boolean.TRUE);
345                placeholder.addFacet(Snapshot.FACET);
346                placeholder.addFacet(FacetNames.VERSIONABLE);
347                session.importDocuments(Collections.singletonList(placeholder));
348                placeholder = session.getDocument(new IdRef(liveUUID));
349            }
350
351            new SnapshotableAdapter(child).restore(child, placeholder, false, olddocs);
352        }
353
354        if (first) {
355            for (DocumentModel old : olddocs) {
356                session.removeDocument(old.getRef());
357            }
358        }
359        return target;
360    }
361
362    @Override
363    public DocumentModel restore(String versionLabel) {
364        DocumentModel target = getCheckoutDocument(doc);
365        DocumentModel leafVersion = getVersionForLabel(target, versionLabel);
366        DocumentModel restoredDoc = restore(leafVersion, target, true, null);
367        return restoredDoc;
368    }
369
370}