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