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