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}