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
179
180
181
182
183 public synchronized Version addVersion(Node node) throws UnsupportedRepositoryOperationException,
184 RepositoryException {
185 Rule rule = new Rule(VersionUtil.getNodeTypeName(node) + "," + NodeTypes.System.NAME, ",");
186 rule.reverse();
187 return this.addVersion(node, rule);
188 }
189
190
191
192
193
194
195
196
197
198 public synchronized Version addVersion(final Node node, final Rule rule) throws UnsupportedRepositoryOperationException,
199 RepositoryException {
200 final String userName = getSafelyUserNameFromMgnlContext();
201 final String workspaceName = node.getSession().getWorkspace().getName();
202 Version version = MgnlContext.doInSystemContext(new JCRSessionOp<Version>(workspaceName) {
203
204 @Override
205 public Version exec(Session session) throws RepositoryException {
206 try {
207 return createVersion(session.getNodeByIdentifier(node.getIdentifier()), rule, userName);
208 } catch (RepositoryException re) {
209
210
211 log.error("failed to copy versionable node to version store, reverting all changes made in this session", re);
212 String versionWorkspaceName = VersionUtil.getVersionWorkspaceForNode(repositoryManager, node);
213
214 MgnlContext.getJCRSession(versionWorkspaceName).refresh(false);
215 throw re;
216 }
217
218 }
219 });
220 return version;
221 }
222
223
224
225
226
227
228
229
230
231
232 @Deprecated
233 protected Version createVersion(Content node, Rule rule) throws UnsupportedRepositoryOperationException, RepositoryException {
234 return createVersion(node.getJCRNode(), rule, getSafelyUserNameFromMgnlContext());
235 }
236
237 private String getSafelyUserNameFromMgnlContext() {
238 String userName = "";
239 if (MgnlContext.getUser() != null) {
240 userName = MgnlContext.getUser().getName();
241 }
242 return userName;
243 }
244
245
246
247
248
249
250
251
252
253
254
255 protected Version createVersion(Node node, Rule rule, String userName) throws UnsupportedRepositoryOperationException,
256 RepositoryException {
257 if (isInvalidMaxVersions()) {
258 log.debug("Ignore create version, MaxVersionIndex < 1");
259 return null;
260 }
261 if (node.isNodeType(NodeTypes.Deleted.NAME)) {
262 log.debug("Don't create version for content marked as deleted");
263 return null;
264 }
265
266 copyUtil.copyToVersion(node, new RuleBasedNodePredicate(rule));
267 Node versionedNode = this.getVersionedNode(node);
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(ContentVersion.VERSION_USER, userName);
285 systemInfo.setProperty(ContentVersion.NAME, node.getName());
286
287 versionedNode.save();
288
289
290 VersionManager versionManager = versionedNode.getSession().getWorkspace().getVersionManager();
291 Version newVersion = versionManager.checkin(versionedNode.getPath());
292 versionManager.checkout(versionedNode.getPath());
293
294 try {
295 this.setMaxVersionHistory(versionedNode);
296 } catch (RepositoryException re) {
297 log.error("Failed to limit version history to the maximum configured", re);
298 log.error("New version has already been created");
299 }
300
301
302 return newVersion;
303 }
304
305
306
307
308 public abstract boolean isInvalidMaxVersions();
309
310
311
312
313 public synchronized Node getVersionedNode(Node node) throws RepositoryException {
314 try {
315 final String versionWorkspace = VersionUtil.getVersionWorkspaceForNode(repositoryManager, node);
316 final Session versionSession = systemContext.getJCRSession(versionWorkspace);
317 return versionSession.getNodeByIdentifier(node.getUUID());
318 } catch (ItemNotFoundException e) {
319
320 return null;
321 }
322 }
323
324
325
326
327 protected Node getVersionedNode(Session session, String uuid) throws RepositoryException {
328 try {
329 return session.getNodeByIdentifier(uuid);
330 } catch (ItemNotFoundException e) {
331
332 return null;
333 }
334 }
335
336
337
338
339
340
341 public abstract void setMaxVersionHistory(Node node) throws RepositoryException;
342
343
344
345
346
347
348
349
350 public synchronized VersionHistory getVersionHistory(Node node) throws UnsupportedRepositoryOperationException,
351 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 return versionedNode.getVersionHistory();
360 } catch (UnsupportedRepositoryOperationException e) {
361 log.debug("Node {} is not versionable.", node);
362
363 return null;
364 }
365 }
366
367
368
369
370
371
372
373 public synchronized Version getVersion(Node node, String name) throws UnsupportedRepositoryOperationException,
374 RepositoryException {
375 VersionHistory history = this.getVersionHistory(node);
376 if (history != null) {
377 return new VersionedNode(history.getVersion(name), node);
378 }
379 log.error("Node {} was never versioned", node.getPath());
380 return null;
381 }
382
383
384
385
386 public Version getBaseVersion(Node node) throws UnsupportedOperationException, RepositoryException {
387 Node versionedNode = this.getVersionedNode(node);
388 if (versionedNode != null) {
389 VersionManager versionManager = versionedNode.getSession().getWorkspace().getVersionManager();
390 return versionManager.getBaseVersion(versionedNode.getPath());
391 }
392
393 throw new RepositoryException("Node " + node.getPath() + " was never versioned");
394 }
395
396
397
398
399
400
401
402
403 public synchronized VersionIterator getAllVersions(Node node) throws UnsupportedRepositoryOperationException, RepositoryException {
404 Node versionedNode = this.getVersionedNode(node);
405 if (versionedNode == null) {
406
407 return null;
408 }
409 VersionManager versionManager = versionedNode.getSession().getWorkspace().getVersionManager();
410 return versionManager.getVersionHistory(versionedNode.getPath()).getAllVersions();
411 }
412
413
414
415
416
417
418
419
420
421
422
423 @Deprecated
424 public synchronized void restore(Content node, Version version, boolean removeExisting) throws VersionException, UnsupportedRepositoryOperationException, RepositoryException {
425 restore(node.getJCRNode(), version, removeExisting);
426 }
427
428
429
430
431
432
433
434
435
436
437 public synchronized void restore(final Node node, Version version, boolean removeExisting) throws VersionException, UnsupportedRepositoryOperationException, RepositoryException {
438
439 PermissionUtil.verifyIsGrantedOrThrowException(node.getSession(), node.getPath(), Session.ACTION_SET_PROPERTY + "," + Session.ACTION_REMOVE + "," + Session.ACTION_ADD_NODE);
440
441
442 final Node versionedNode = this.getVersionedNode(node);
443
444 final Version unwrappedVersion;
445 if (version instanceof VersionedNode) {
446 unwrappedVersion = VersionUtil.unwrap(((VersionedNode) version).unwrap());
447 } else {
448 unwrappedVersion = VersionUtil.unwrap(version);
449 }
450
451 VersionManager versionManager = versionedNode.getSession().getWorkspace().getVersionManager();
452 versionManager.restore(unwrappedVersion, removeExisting);
453 versionManager.checkout(versionedNode.getPath());
454 MgnlContext.doInSystemContext(new JCRSessionOp<Void>(versionedNode.getSession().getWorkspace().getName()) {
455
456 @Override
457 public Void exec(Session session) throws RepositoryException {
458
459 List<String> mixins = new ArrayList<String>();
460 for (Value v : unwrappedVersion.getNode(JcrConstants.JCR_FROZENNODE).getProperty("jcr:frozenMixinTypes").getValues()) {
461 mixins.add(v.getString());
462 }
463
464 final Node systemVersionedNode = session.getNodeByIdentifier(versionedNode.getUUID());
465 for (NodeType nt : versionedNode.getMixinNodeTypes()) {
466 if (!mixins.remove(nt.getName())) {
467 systemVersionedNode.removeMixin(nt.getName());
468 }
469 }
470 for (String mix : mixins) {
471 systemVersionedNode.addMixin(mix);
472 }
473 systemVersionedNode.save();
474
475 try {
476
477 Session sysDestinationSession = MgnlContext.getJCRSession(node.getSession().getWorkspace().getName());
478 log.debug("restoring info:{}:{}", sysDestinationSession.getWorkspace().getName(), node.getPath());
479 Node destinationNode = sysDestinationSession.getNode(node.getPath());
480
481 Rule rule = getUsedFilter(versionedNode);
482 try {
483 copyUtil.copyFromVersion(versionedNode, destinationNode, new RuleBasedNodePredicate(rule));
484 if (NodeUtil.hasMixin(destinationNode, NodeTypes.Deleted.NAME)) {
485 destinationNode.removeMixin(NodeTypes.Deleted.NAME);
486 }
487
488 NodeTypes.LastModified.update(destinationNode);
489 destinationNode.save();
490
491 node.refresh(false);
492 } catch (RepositoryException re) {
493 log.debug("error during restore: {}", re.getMessage(), re);
494 log.error("failed to restore versioned node, reverting all changes make to this node");
495 destinationNode.refresh(false);
496 throw re;
497 }
498 } catch (IOException e) {
499 throw new RepositoryException(e);
500 } catch (ClassNotFoundException e) {
501 throw new RepositoryException(e);
502 }
503 return null;
504 }
505 });
506 }
507
508
509
510
511
512
513 public synchronized void removeVersionHistory(final Node node) throws RepositoryException {
514 PermissionUtil.verifyIsGrantedOrThrowException(node.getSession(), Path.getAbsolutePath(node.getPath()), Session.ACTION_ADD_NODE + "," + Session.ACTION_REMOVE + "," + Session.ACTION_SET_PROPERTY);
515 final String uuid = node.getIdentifier();
516 String workspaceName = repositoryManager.getRepositoryNameForWorkspace(node.getSession().getWorkspace().getName()) + "-" + RepositoryConstants.VERSION_STORE;
517 MgnlContext.doInSystemContext(new JCRSessionOp<Void>(workspaceName) {
518
519 @Override
520 public Void exec(Session session) throws RepositoryException {
521 Node node = getVersionedNode(session, uuid);
522 if (node != null) {
523 if (node.getReferences().getSize() < 1) {
524
525 node.remove();
526 } else {
527 VersionManager versionManager = node.getSession().getWorkspace().getVersionManager();
528 VersionHistory history = versionManager.getVersionHistory(node.getPath());
529 VersionIterator versions = history.getAllVersions();
530 if (versions != null) {
531
532 versions.nextVersion();
533 while (versions.hasNext()) {
534 history.removeVersion(versions.nextVersion().getName());
535 }
536 }
537 }
538 }
539 session.save();
540 return null;
541 }
542 });
543 }
544
545
546
547
548 protected void checkAndAddMixin(Node node) throws RepositoryException {
549 if (!node.isNodeType("mix:versionable")) {
550 log.debug("Add mix:versionable");
551 node.addMixin("mix:versionable");
552 }
553 }
554
555
556
557
558 protected Rule getUsedFilter(Node versionedNode) throws IOException, ClassNotFoundException, RepositoryException {
559
560 ByteArrayInputStream inStream = null;
561 try {
562 String ruleString = this.getSystemNode(versionedNode).getProperty(PROPERTY_RULE).getString();
563 inStream = new ByteArrayInputStream(Base64.decodeBase64(ruleString.getBytes()));
564 ObjectInput objectInput = new ObjectInputStream(inStream);
565 return (Rule) objectInput.readObject();
566 } catch (InvalidClassException e) {
567 log.debug(e.getMessage());
568 log.debug("Will return default rule -> all child nodes while ignoring versionedNode nodeType.");
569 Rule rule = new Rule(VersionUtil.getNodeTypeName(versionedNode) + "," + NodeTypes.System.NAME, ",");
570 rule.reverse();
571 return rule;
572 } catch (IOException e) {
573 throw e;
574 } catch (ClassNotFoundException e) {
575 throw e;
576 } finally {
577 IOUtils.closeQuietly(inStream);
578 }
579 }
580
581
582
583
584
585
586 protected synchronized Node getSystemNode(Node node) throws RepositoryException {
587 if (node.hasNode(SYSTEM_NODE)) {
588 return node.getNode(SYSTEM_NODE);
589 }
590 return node.addNode(SYSTEM_NODE, NodeTypes.System.NAME);
591 }
592 }