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}