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.security.JCRSessionOp;
37 import info.magnolia.cms.security.PermissionUtil;
38 import info.magnolia.cms.util.Rule;
39 import info.magnolia.context.MgnlContext;
40 import info.magnolia.context.SystemContext;
41 import info.magnolia.jcr.predicate.RuleBasedNodePredicate;
42 import info.magnolia.jcr.util.NodeTypes;
43 import info.magnolia.jcr.util.NodeUtil;
44 import info.magnolia.jcr.util.VersionUtil;
45 import info.magnolia.repository.RepositoryConstants;
46 import info.magnolia.repository.RepositoryManager;
47 import info.magnolia.repository.definition.RepositoryDefinition;
48
49 import java.io.ByteArrayInputStream;
50 import java.io.ByteArrayOutputStream;
51 import java.io.IOException;
52 import java.io.InvalidClassException;
53 import java.io.ObjectInput;
54 import java.io.ObjectInputStream;
55 import java.io.ObjectOutput;
56 import java.io.ObjectOutputStream;
57 import java.util.ArrayList;
58 import java.util.Calendar;
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 NAME = "name";
104
105
106
107
108 public static final String VERSION_USER = "versionUser";
109
110
111
112
113 public static final String TMP_REFERENCED_NODES = "mgnl:tmpReferencedNodes";
114
115
116
117
118 public static final String SYSTEM_NODE = "mgnl:versionMetaData";
119
120
121
122
123 public static final String PROPERTY_RULE = "Rule";
124
125
126
127
128 protected static final String ROOT_VERSION = "jcr:rootVersion";
129
130
131
132
133 public static final String SOURCE_WORKSPACE = "sourceWorkspace";
134
135 private Collection<String> versionWorkspaces = new ArrayList<>();
136
137 private final SystemContext systemContext;
138 private final RepositoryManager repositoryManager;
139 private final CopyUtil copyUtil;
140
141 @Inject
142 public BaseVersionManager(SystemContext systemContext, RepositoryManager repositoryManager, CopyUtil copyUtil) {
143 this.systemContext = systemContext;
144 this.repositoryManager = repositoryManager;
145 this.copyUtil = copyUtil;
146 for (RepositoryDefinition repositoryDefinition : repositoryManager.getRepositoryDefinitions()) {
147 String repositoryId = repositoryDefinition.getName();
148 String workspaceName = repositoryId + "-" + RepositoryConstants.VERSION_STORE;
149 if (repositoryManager.getWorkspaceMapping(workspaceName) != null) {
150 versionWorkspaces.add(workspaceName);
151 } else {
152 throw new RuntimeException(String.format("Something went wrong, version workspace for repository %s does not exist.", repositoryId));
153 }
154 }
155 }
156
157
158
159
160
161
162 protected void createInitialStructure() throws RepositoryException {
163 for (String workspaceName : versionWorkspaces) {
164 Session session = systemContext.getJCRSession(workspaceName);
165 try {
166 Node tmp = session.getNode("/" + TMP_REFERENCED_NODES);
167
168 NodeIterator children = tmp.getNodes();
169 while (children.hasNext()) {
170 Node child = children.nextNode();
171 if (child.getReferences().getSize() < 1) {
172 child.remove();
173 }
174 }
175 } catch (PathNotFoundException e) {
176 session.getRootNode().addNode(TMP_REFERENCED_NODES, NodeTypes.System.NAME);
177 }
178 session.save();
179 }
180 }
181
182
183
184
185 public synchronized Version addVersion(Node node) throws RepositoryException {
186 Rulel/Rule.html#Rule">Rule rule = new Rule(VersionUtil.getNodeTypeName(node) + "," + NodeTypes.System.NAME, ",");
187 rule.reverse();
188 return this.addVersion(node, rule);
189 }
190
191
192
193
194
195 public synchronized Version addVersion(final Node node, final Rule rule) throws RepositoryException {
196 final String userName = getSafelyUserNameFromMgnlContext();
197 return addVersion(node, rule, userName);
198 }
199
200 public synchronized Version addVersion(final Node node, final Rule rule, final String userName) throws RepositoryException {
201 return addVersion(node, rule, userName, null);
202 }
203
204
205
206
207 public synchronized Version addVersion(final Node node, final Rule rule, final String userName, final String comment) throws RepositoryException {
208 final String workspaceName = node.getSession().getWorkspace().getName();
209 Version version = MgnlContext.doInSystemContext(new JCRSessionOp<Version>(workspaceName) {
210
211 @Override
212 public Version exec(Session session) throws RepositoryException {
213 try {
214 return createVersion(session.getNodeByIdentifier(node.getIdentifier()), rule, userName, comment);
215 } catch (RepositoryException re) {
216
217
218 log.error("failed to copy versionable node to version store, reverting all changes made in this session", re);
219 String versionWorkspaceName = VersionUtil.getVersionWorkspaceForNode(repositoryManager, node);
220
221 MgnlContext.getJCRSession(versionWorkspaceName).refresh(false);
222 throw re;
223 }
224
225 }
226 });
227 return version;
228 }
229
230 private String getSafelyUserNameFromMgnlContext() {
231 String userName = "";
232 if (MgnlContext.getUser() != null) {
233 userName = MgnlContext.getUser().getName();
234 }
235 return userName;
236 }
237
238
239
240
241
242
243
244
245
246
247
248 protected Version createVersion(Node node, Rule rule, String userName) throws RepositoryException {
249 return createVersion(node, rule, userName, "");
250 }
251
252 private Version createVersion(Node node, Rule rule, String userName, String comment) throws 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 if (!node.isNodeType(NodeTypes.HasVersion.NAME)) {
262 node.addMixin(NodeTypes.HasVersion.NAME);
263 node.getSession().save();
264 }
265 copyUtil.copyToVersion(node, new RuleBasedNodePredicate(rule));
266 Node versionedNode = this.getVersionedNode(node);
267 versionedNode.setProperty(NodeTypes.Versionable.COMMENT, comment);
268
269 checkAndAddMixin(versionedNode);
270 Node systemInfo = this.getSystemNode(versionedNode);
271
272 ByteArrayOutputStream out = new ByteArrayOutputStream();
273 try {
274 ObjectOutput objectOut = new ObjectOutputStream(out);
275 objectOut.writeObject(rule);
276 objectOut.flush();
277 objectOut.close();
278
279 systemInfo.setProperty(PROPERTY_RULE, new String(Base64.encodeBase64(out.toByteArray())));
280 } catch (IOException e) {
281 throw new RepositoryException("Unable to add serialized Rule to the versioned content");
282 }
283
284 systemInfo.setProperty(VERSION_USER, userName);
285 systemInfo.setProperty(NAME, node.getName());
286 systemInfo.setProperty(SOURCE_WORKSPACE, node.getSession().getWorkspace().getName());
287
288 versionedNode.getSession().save();
289
290
291 VersionManager versionManager = versionedNode.getSession().getWorkspace().getVersionManager();
292 Version newVersion = versionManager.checkin(versionedNode.getPath());
293 versionManager.checkout(versionedNode.getPath());
294
295 try {
296 this.setMaxVersionHistory(versionedNode);
297 } catch (RepositoryException re) {
298 log.error("Failed to limit version history to the maximum configured", re);
299 log.error("New version has already been created");
300 }
301
302
303 return newVersion;
304 }
305
306
307
308
309 public abstract boolean isInvalidMaxVersions();
310
311
312
313
314 public synchronized Node getVersionedNode(Node node) throws RepositoryException {
315 try {
316 final String versionWorkspace = VersionUtil.getVersionWorkspaceForNode(repositoryManager, node);
317 final Session versionSession = new MgnlVersionSessionDecorator(node.getSession().getWorkspace().getName()).wrapSession(systemContext.getJCRSession(versionWorkspace));
318 return versionSession.getNodeByIdentifier(node.getIdentifier());
319 } catch (ItemNotFoundException e) {
320
321 return null;
322 }
323 }
324
325
326
327
328 protected Node getVersionedNode(Session session, String uuid) throws RepositoryException {
329 try {
330 return session.getNodeByIdentifier(uuid);
331 } catch (ItemNotFoundException e) {
332
333 return null;
334 }
335 }
336
337
338
339
340
341
342 public abstract void setMaxVersionHistory(Node node) throws RepositoryException;
343
344
345
346
347
348
349
350
351 public synchronized VersionHistory getVersionHistory(Node node) throws RepositoryException {
352 try {
353 Node versionedNode = this.getVersionedNode(node);
354 if (versionedNode == null) {
355
356 log.debug("No VersionHistory found for {} node.", node);
357 return null;
358 }
359 VersionHistory versionHistory = versionedNode.getVersionHistory();
360 VersionIterator versionIterator = versionHistory.getAllVersions();
361 long filteredSize = versionIterator.getSize();
362 long unfilteredSize = ((MgnlVersionSessionDecorator.MgnlVersionSessionVersionIteratorWrapper) versionIterator).getUnfilteredSize();
363
364
365 if (filteredSize == 1 && unfilteredSize > filteredSize) {
366 return null;
367 }
368 return versionHistory;
369 } catch (UnsupportedRepositoryOperationException e) {
370 log.debug("Node {} is not versionable.", node);
371
372 return null;
373 }
374 }
375
376
377
378
379
380
381
382 public synchronized Version getVersion(Node node, String name) throws RepositoryException {
383 VersionHistory history = this.getVersionHistory(node);
384 if (history != null) {
385 return new VersionedNode(history.getVersion(name), node);
386 }
387 log.error("Node {} was never versioned", node.getPath());
388 return null;
389 }
390
391
392
393
394 public Version getBaseVersion(Node node) throws UnsupportedOperationException, RepositoryException {
395 VersionIterator versionIterator = getAllVersions(node);
396 Version baseVersion = null;
397 if (versionIterator != null) {
398 while (versionIterator.hasNext()) {
399 Version version = versionIterator.nextVersion();
400 if (baseVersion == null) {
401 baseVersion = version;
402 } else if (baseVersion.getCreated().compareTo(version.getCreated()) < 0) {
403 baseVersion = version;
404 }
405 }
406 }
407 if (baseVersion == null) {
408 throw new RepositoryException("Node " + node.getPath() + " was never versioned");
409 }
410 return baseVersion;
411 }
412
413
414
415
416
417
418
419
420 public synchronized VersionIterator getAllVersions(Node node) throws RepositoryException {
421 Node versionedNode = this.getVersionedNode(node);
422 if (versionedNode == null) {
423
424 return null;
425 }
426 VersionHistory versionHistory = getVersionHistory(node);
427 if (versionHistory == null) {
428
429 return null;
430 }
431 return versionHistory.getAllVersions();
432 }
433
434
435
436
437
438
439
440
441
442
443 public synchronized void restore(final Node node, Version version, boolean removeExisting) throws RepositoryException {
444
445 PermissionUtil.verifyIsGrantedOrThrowException(node.getSession(), node.getPath(), Session.ACTION_SET_PROPERTY + "," + Session.ACTION_REMOVE + "," + Session.ACTION_ADD_NODE);
446
447
448 final Node versionedNode = this.getVersionedNode(node);
449
450 final Version unwrappedVersion;
451 if (version instanceof VersionedNode) {
452 unwrappedVersion = VersionUtil.unwrap(((VersionedNode) version).unwrap());
453 } else {
454 unwrappedVersion = VersionUtil.unwrap(version);
455 }
456
457 final String lastActivatedVersion = NodeTypes.Activatable.getLastActivatedVersion(node);
458 final Calendar lastActivatedVersionCreated = NodeTypes.Activatable.getLastActivatedVersionCreated(node);
459
460 VersionManager versionManager = versionedNode.getSession().getWorkspace().getVersionManager();
461 versionManager.restore(unwrappedVersion, removeExisting);
462 versionManager.checkout(versionedNode.getPath());
463 MgnlContext.doInSystemContext(new JCRSessionOp<Void>(versionedNode.getSession().getWorkspace().getName()) {
464
465 @Override
466 public Void exec(Session session) throws RepositoryException {
467
468 List<String> mixins = new ArrayList<>();
469 for (Value v : unwrappedVersion.getNode(JcrConstants.JCR_FROZENNODE).getProperty("jcr:frozenMixinTypes").getValues()) {
470 mixins.add(v.getString());
471 }
472
473 final Node systemVersionedNode = session.getNodeByIdentifier(versionedNode.getUUID());
474 for (NodeType nt : versionedNode.getMixinNodeTypes()) {
475 if (!mixins.remove(nt.getName())) {
476 systemVersionedNode.removeMixin(nt.getName());
477 }
478 }
479 for (String mix : mixins) {
480 systemVersionedNode.addMixin(mix);
481 }
482 systemVersionedNode.save();
483
484 try {
485
486 Session sysDestinationSession = MgnlContext.getJCRSession(node.getSession().getWorkspace().getName());
487 log.debug("restoring info:{}:{}", sysDestinationSession.getWorkspace().getName(), node.getPath());
488 Node destinationNode = sysDestinationSession.getNode(node.getPath());
489
490 Rule rule = getUsedFilter(versionedNode);
491 try {
492 copyUtil.copyFromVersion(versionedNode, destinationNode, new RuleBasedNodePredicate(rule));
493 if (NodeUtil.hasMixin(destinationNode, NodeTypes.Deleted.NAME)) {
494 destinationNode.removeMixin(NodeTypes.Deleted.NAME);
495 }
496 if (lastActivatedVersion != null) {
497
498 destinationNode.setProperty(NodeTypes.Activatable.LAST_ACTIVATED_VERSION, lastActivatedVersion);
499 }
500 if (lastActivatedVersionCreated != null) {
501
502 destinationNode.setProperty(NodeTypes.Activatable.LAST_ACTIVATED_VERSION_CREATED, lastActivatedVersionCreated);
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 Rulel/Rule.html#Rule">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 }