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