1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.cms.core.version;
35
36 import info.magnolia.cms.core.Content;
37 import info.magnolia.cms.core.Path;
38 import info.magnolia.cms.security.JCRSessionOp;
39 import info.magnolia.cms.security.PermissionUtil;
40 import info.magnolia.cms.util.Rule;
41 import info.magnolia.context.MgnlContext;
42 import info.magnolia.context.SystemContext;
43 import info.magnolia.jcr.predicate.RuleBasedNodePredicate;
44 import info.magnolia.jcr.util.NodeTypes;
45 import info.magnolia.jcr.util.NodeUtil;
46 import info.magnolia.jcr.util.VersionUtil;
47 import info.magnolia.repository.RepositoryConstants;
48 import info.magnolia.repository.RepositoryManager;
49 import info.magnolia.repository.definition.RepositoryDefinition;
50
51 import java.io.ByteArrayInputStream;
52 import java.io.ByteArrayOutputStream;
53 import java.io.IOException;
54 import java.io.InvalidClassException;
55 import java.io.ObjectInput;
56 import java.io.ObjectInputStream;
57 import java.io.ObjectOutput;
58 import java.io.ObjectOutputStream;
59 import java.util.ArrayList;
60 import java.util.Collection;
61 import java.util.List;
62
63 import javax.inject.Inject;
64 import javax.jcr.ItemNotFoundException;
65 import javax.jcr.Node;
66 import javax.jcr.NodeIterator;
67 import javax.jcr.PathNotFoundException;
68 import javax.jcr.RepositoryException;
69 import javax.jcr.Session;
70 import javax.jcr.UnsupportedRepositoryOperationException;
71 import javax.jcr.Value;
72 import javax.jcr.nodetype.NodeType;
73 import javax.jcr.version.Version;
74 import javax.jcr.version.VersionException;
75 import javax.jcr.version.VersionHistory;
76 import javax.jcr.version.VersionIterator;
77 import javax.jcr.version.VersionManager;
78
79 import org.apache.commons.codec.binary.Base64;
80 import org.apache.commons.io.IOUtils;
81 import org.apache.jackrabbit.JcrConstants;
82 import org.slf4j.Logger;
83 import org.slf4j.LoggerFactory;
84
85
86
87
88
89
90
91
92 public abstract class BaseVersionManager {
93
94 private static final Logger log = LoggerFactory.getLogger(BaseVersionManager.class);
95
96
97
98
99 public static final String VERSION_WORKSPACE = "mgnlVersion";
100
101
102
103
104 public static final String TMP_REFERENCED_NODES = "mgnl:tmpReferencedNodes";
105
106
107
108
109 protected static final String SYSTEM_NODE = "mgnl:versionMetaData";
110
111
112
113
114 public static final String PROPERTY_RULE = "Rule";
115
116
117
118
119 protected static final String ROOT_VERSION = "jcr:rootVersion";
120
121 private Collection<String> versionWorkspaces = new ArrayList<>();
122
123 private final SystemContext systemContext;
124 private final RepositoryManager repositoryManager;
125 private final CopyUtil copyUtil;
126
127 @Inject
128 public BaseVersionManager(SystemContext systemContext, RepositoryManager repositoryManager, CopyUtil copyUtil) {
129 this.systemContext = systemContext;
130 this.repositoryManager = repositoryManager;
131 this.copyUtil = copyUtil;
132 for (RepositoryDefinition repositoryDefinition : repositoryManager.getRepositoryDefinitions()) {
133 String repositoryId = repositoryDefinition.getName();
134 String workspaceName = repositoryId + "-" + RepositoryConstants.VERSION_STORE;
135 if (repositoryManager.getWorkspaceMapping(workspaceName) != null) {
136 versionWorkspaces.add(workspaceName);
137 } else {
138 throw new RuntimeException(String.format("Something went wrong, version workspace for repository %s does not exist.", repositoryId));
139 }
140 }
141 }
142
143
144
145
146
147
148 protected void createInitialStructure() throws RepositoryException {
149 for (String workspaceName : versionWorkspaces) {
150 MgnlContext.doInSystemContext(new JCRSessionOp<Void>(workspaceName) {
151
152 @Override
153 public Void exec(Session session) throws RepositoryException {
154 try {
155 Node tmp = session.getNode("/" + TMP_REFERENCED_NODES);
156
157 NodeIterator children = tmp.getNodes();
158 while (children.hasNext()) {
159 Node child = children.nextNode();
160 if (child.getReferences().getSize() < 1) {
161 child.remove();
162 }
163 }
164 } catch (PathNotFoundException e) {
165 session.getRootNode().addNode(TMP_REFERENCED_NODES, NodeTypes.System.NAME);
166 }
167 session.save();
168
169 return null;
170 }
171 });
172 }
173 }
174
175
176
177
178 public synchronized Version addVersion(Node node) throws RepositoryException {
179 Rule rule = new Rule(VersionUtil.getNodeTypeName(node) + "," + NodeTypes.System.NAME, ",");
180 rule.reverse();
181 return this.addVersion(node, rule);
182 }
183
184
185
186
187
188 public synchronized Version addVersion(final Node node, final Rule rule) throws RepositoryException {
189 final String userName = getSafelyUserNameFromMgnlContext();
190 return addVersion(node, rule, userName);
191 }
192
193
194
195
196 public synchronized Version addVersion(final Node node, final Rule rule, final String userName) throws RepositoryException {
197 final String workspaceName = node.getSession().getWorkspace().getName();
198 Version version = MgnlContext.doInSystemContext(new JCRSessionOp<Version>(workspaceName) {
199
200 @Override
201 public Version exec(Session session) throws RepositoryException {
202 try {
203 return createVersion(session.getNodeByIdentifier(node.getIdentifier()), rule, userName);
204 } catch (RepositoryException re) {
205
206
207 log.error("failed to copy versionable node to version store, reverting all changes made in this session", re);
208 String versionWorkspaceName = VersionUtil.getVersionWorkspaceForNode(repositoryManager, node);
209
210 MgnlContext.getJCRSession(versionWorkspaceName).refresh(false);
211 throw re;
212 }
213
214 }
215 });
216 return version;
217 }
218
219
220
221
222
223
224
225
226
227
228 @Deprecated
229 protected Version createVersion(Content node, Rule rule) throws UnsupportedRepositoryOperationException, RepositoryException {
230 return createVersion(node.getJCRNode(), rule, getSafelyUserNameFromMgnlContext());
231 }
232
233 private String getSafelyUserNameFromMgnlContext() {
234 String userName = "";
235 if (MgnlContext.getUser() != null) {
236 userName = MgnlContext.getUser().getName();
237 }
238 return userName;
239 }
240
241
242
243
244
245
246
247
248
249
250
251 protected Version createVersion(Node node, Rule rule, String userName) throws UnsupportedRepositoryOperationException,
252 RepositoryException {
253 if (isInvalidMaxVersions()) {
254 log.debug("Ignore create version, MaxVersionIndex < 1");
255 return null;
256 }
257 if (node.isNodeType(NodeTypes.Deleted.NAME)) {
258 log.debug("Don't create version for content marked as deleted");
259 return null;
260 }
261
262 copyUtil.copyToVersion(node, new RuleBasedNodePredicate(rule));
263 Node versionedNode = this.getVersionedNode(node);
264
265 checkAndAddMixin(versionedNode);
266 Node systemInfo = this.getSystemNode(versionedNode);
267
268 ByteArrayOutputStream out = new ByteArrayOutputStream();
269 try {
270 ObjectOutput objectOut = new ObjectOutputStream(out);
271 objectOut.writeObject(rule);
272 objectOut.flush();
273 objectOut.close();
274
275 systemInfo.setProperty(PROPERTY_RULE, new String(Base64.encodeBase64(out.toByteArray())));
276 } catch (IOException e) {
277 throw new RepositoryException("Unable to add serialized Rule to the versioned content");
278 }
279
280 systemInfo.setProperty(ContentVersion.VERSION_USER, userName);
281 systemInfo.setProperty(ContentVersion.NAME, node.getName());
282
283 versionedNode.save();
284
285
286 VersionManager versionManager = versionedNode.getSession().getWorkspace().getVersionManager();
287 Version newVersion = versionManager.checkin(versionedNode.getPath());
288 versionManager.checkout(versionedNode.getPath());
289
290 try {
291 this.setMaxVersionHistory(versionedNode);
292 } catch (RepositoryException re) {
293 log.error("Failed to limit version history to the maximum configured", re);
294 log.error("New version has already been created");
295 }
296
297
298 return newVersion;
299 }
300
301
302
303
304 public abstract boolean isInvalidMaxVersions();
305
306
307
308
309 public synchronized Node getVersionedNode(Node node) throws RepositoryException {
310 try {
311 final String versionWorkspace = VersionUtil.getVersionWorkspaceForNode(repositoryManager, node);
312 final Session versionSession = systemContext.getJCRSession(versionWorkspace);
313 return versionSession.getNodeByIdentifier(node.getUUID());
314 } catch (ItemNotFoundException e) {
315
316 return null;
317 }
318 }
319
320
321
322
323 protected Node getVersionedNode(Session session, String uuid) throws RepositoryException {
324 try {
325 return session.getNodeByIdentifier(uuid);
326 } catch (ItemNotFoundException e) {
327
328 return null;
329 }
330 }
331
332
333
334
335
336
337 public abstract void setMaxVersionHistory(Node node) throws RepositoryException;
338
339
340
341
342
343
344
345
346 public synchronized VersionHistory getVersionHistory(Node node) throws UnsupportedRepositoryOperationException,
347 RepositoryException {
348 try {
349 Node versionedNode = this.getVersionedNode(node);
350 if (versionedNode == null) {
351
352 log.debug("No VersionHistory found for {} node.", node);
353 return null;
354 }
355 return versionedNode.getVersionHistory();
356 } catch (UnsupportedRepositoryOperationException e) {
357 log.debug("Node {} is not versionable.", node);
358
359 return null;
360 }
361 }
362
363
364
365
366
367
368
369 public synchronized Version getVersion(Node node, String name) throws UnsupportedRepositoryOperationException,
370 RepositoryException {
371 VersionHistory history = this.getVersionHistory(node);
372 if (history != null) {
373 return new VersionedNode(history.getVersion(name), node);
374 }
375 log.error("Node {} was never versioned", node.getPath());
376 return null;
377 }
378
379
380
381
382 public Version getBaseVersion(Node node) throws UnsupportedOperationException, RepositoryException {
383 Node versionedNode = this.getVersionedNode(node);
384 if (versionedNode != null) {
385 VersionManager versionManager = versionedNode.getSession().getWorkspace().getVersionManager();
386 return versionManager.getBaseVersion(versionedNode.getPath());
387 }
388
389 throw new RepositoryException("Node " + node.getPath() + " was never versioned");
390 }
391
392
393
394
395
396
397
398
399 public synchronized VersionIterator getAllVersions(Node node) throws UnsupportedRepositoryOperationException, RepositoryException {
400 Node versionedNode = this.getVersionedNode(node);
401 if (versionedNode == null) {
402
403 return null;
404 }
405 VersionManager versionManager = versionedNode.getSession().getWorkspace().getVersionManager();
406 return versionManager.getVersionHistory(versionedNode.getPath()).getAllVersions();
407 }
408
409
410
411
412
413
414
415
416
417
418
419 @Deprecated
420 public synchronized void restore(Content node, Version version, boolean removeExisting) throws VersionException, UnsupportedRepositoryOperationException, RepositoryException {
421 restore(node.getJCRNode(), version, removeExisting);
422 }
423
424
425
426
427
428
429
430
431
432
433 public synchronized void restore(final Node node, Version version, boolean removeExisting) throws VersionException, UnsupportedRepositoryOperationException, RepositoryException {
434
435 PermissionUtil.verifyIsGrantedOrThrowException(node.getSession(), node.getPath(), Session.ACTION_SET_PROPERTY + "," + Session.ACTION_REMOVE + "," + Session.ACTION_ADD_NODE);
436
437
438 final Node versionedNode = this.getVersionedNode(node);
439
440 final Version unwrappedVersion;
441 if (version instanceof VersionedNode) {
442 unwrappedVersion = VersionUtil.unwrap(((VersionedNode) version).unwrap());
443 } else {
444 unwrappedVersion = VersionUtil.unwrap(version);
445 }
446
447 VersionManager versionManager = versionedNode.getSession().getWorkspace().getVersionManager();
448 versionManager.restore(unwrappedVersion, removeExisting);
449 versionManager.checkout(versionedNode.getPath());
450 MgnlContext.doInSystemContext(new JCRSessionOp<Void>(versionedNode.getSession().getWorkspace().getName()) {
451
452 @Override
453 public Void exec(Session session) throws RepositoryException {
454
455 List<String> mixins = new ArrayList<String>();
456 for (Value v : unwrappedVersion.getNode(JcrConstants.JCR_FROZENNODE).getProperty("jcr:frozenMixinTypes").getValues()) {
457 mixins.add(v.getString());
458 }
459
460 final Node systemVersionedNode = session.getNodeByIdentifier(versionedNode.getUUID());
461 for (NodeType nt : versionedNode.getMixinNodeTypes()) {
462 if (!mixins.remove(nt.getName())) {
463 systemVersionedNode.removeMixin(nt.getName());
464 }
465 }
466 for (String mix : mixins) {
467 systemVersionedNode.addMixin(mix);
468 }
469 systemVersionedNode.save();
470
471 try {
472
473 Session sysDestinationSession = MgnlContext.getJCRSession(node.getSession().getWorkspace().getName());
474 log.debug("restoring info:{}:{}", sysDestinationSession.getWorkspace().getName(), node.getPath());
475 Node destinationNode = sysDestinationSession.getNode(node.getPath());
476
477 Rule rule = getUsedFilter(versionedNode);
478 try {
479 copyUtil.copyFromVersion(versionedNode, destinationNode, new RuleBasedNodePredicate(rule));
480 if (NodeUtil.hasMixin(destinationNode, NodeTypes.Deleted.NAME)) {
481 destinationNode.removeMixin(NodeTypes.Deleted.NAME);
482 }
483
484 NodeTypes.LastModified.update(destinationNode);
485 destinationNode.save();
486
487 node.refresh(false);
488 } catch (RepositoryException re) {
489 log.debug("error during restore: {}", re.getMessage(), re);
490 log.error("failed to restore versioned node, reverting all changes make to this node");
491 destinationNode.refresh(false);
492 throw re;
493 }
494 } catch (IOException e) {
495 throw new RepositoryException(e);
496 } catch (ClassNotFoundException e) {
497 throw new RepositoryException(e);
498 }
499 return null;
500 }
501 });
502 }
503
504
505
506
507
508
509 public synchronized void removeVersionHistory(final Node node) throws RepositoryException {
510 PermissionUtil.verifyIsGrantedOrThrowException(node.getSession(), Path.getAbsolutePath(node.getPath()), Session.ACTION_ADD_NODE + "," + Session.ACTION_REMOVE + "," + Session.ACTION_SET_PROPERTY);
511 final String uuid = node.getIdentifier();
512 String workspaceName = repositoryManager.getRepositoryNameForWorkspace(node.getSession().getWorkspace().getName()) + "-" + RepositoryConstants.VERSION_STORE;
513 MgnlContext.doInSystemContext(new JCRSessionOp<Void>(workspaceName) {
514
515 @Override
516 public Void exec(Session session) throws RepositoryException {
517 Node node = getVersionedNode(session, uuid);
518 if (node != null) {
519 if (node.getReferences().getSize() < 1) {
520
521 node.remove();
522 } else {
523 VersionManager versionManager = node.getSession().getWorkspace().getVersionManager();
524 VersionHistory history = versionManager.getVersionHistory(node.getPath());
525 VersionIterator versions = history.getAllVersions();
526 if (versions != null) {
527
528 versions.nextVersion();
529 while (versions.hasNext()) {
530 history.removeVersion(versions.nextVersion().getName());
531 }
532 }
533 }
534 }
535 session.save();
536 return null;
537 }
538 });
539 }
540
541
542
543
544 protected void checkAndAddMixin(Node node) throws RepositoryException {
545 if (!node.isNodeType("mix:versionable")) {
546 log.debug("Add mix:versionable");
547 node.addMixin("mix:versionable");
548 }
549 }
550
551
552
553
554 protected Rule getUsedFilter(Node versionedNode) throws IOException, ClassNotFoundException, RepositoryException {
555
556 ByteArrayInputStream inStream = null;
557 try {
558 String ruleString = this.getSystemNode(versionedNode).getProperty(PROPERTY_RULE).getString();
559 inStream = new ByteArrayInputStream(Base64.decodeBase64(ruleString.getBytes()));
560 ObjectInput objectInput = new ObjectInputStream(inStream);
561 return (Rule) objectInput.readObject();
562 } catch (InvalidClassException e) {
563 log.debug(e.getMessage());
564 log.debug("Will return default rule -> all child nodes while ignoring versionedNode nodeType.");
565 Rule rule = new Rule(VersionUtil.getNodeTypeName(versionedNode) + "," + NodeTypes.System.NAME, ",");
566 rule.reverse();
567 return rule;
568 } catch (IOException e) {
569 throw e;
570 } catch (ClassNotFoundException e) {
571 throw e;
572 } finally {
573 IOUtils.closeQuietly(inStream);
574 }
575 }
576
577
578
579
580
581
582 protected synchronized Node getSystemNode(Node node) throws RepositoryException {
583 if (node.hasNode(SYSTEM_NODE)) {
584 return node.getNode(SYSTEM_NODE);
585 }
586 return node.addNode(SYSTEM_NODE, NodeTypes.System.NAME);
587 }
588 }