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.getLocalService(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:currentLifeCycleState != 'deleted' 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}