diff --git a/src/main/groovy/com/twcable/grabbit/client/batch/steps/jcrnodes/JcrNodesWriter.groovy b/src/main/groovy/com/twcable/grabbit/client/batch/steps/jcrnodes/JcrNodesWriter.groovy index 85fe218..30e1a5a 100644 --- a/src/main/groovy/com/twcable/grabbit/client/batch/steps/jcrnodes/JcrNodesWriter.groovy +++ b/src/main/groovy/com/twcable/grabbit/client/batch/steps/jcrnodes/JcrNodesWriter.groovy @@ -17,7 +17,6 @@ package com.twcable.grabbit.client.batch.steps.jcrnodes import com.twcable.grabbit.client.batch.ClientBatchJobContext -import com.twcable.grabbit.jcr.JcrNodeDecorator import com.twcable.grabbit.jcr.ProtoNodeDecorator import com.twcable.grabbit.proto.NodeProtos.Node as ProtoNode import groovy.transform.CompileStatic @@ -71,7 +70,8 @@ class JcrNodesWriter implements ItemWriter, ItemWriteListener { void write(List nodeProtos) throws Exception { Session session = theSession() for (ProtoNode nodeProto : nodeProtos) { - writeToJcr(nodeProto, session) + ProtoNodeDecorator protoNodeDecorator = new ProtoNodeDecorator(nodeProto) + protoNodeDecorator.writeToJcr(session) } } @@ -87,16 +87,7 @@ class JcrNodesWriter implements ItemWriter, ItemWriteListener { return retVal } - private static void writeToJcr(ProtoNode nodeProto, Session session) { - JcrNodeDecorator jcrNode = new ProtoNodeDecorator(nodeProto).writeToJcr(session) - jcrNode.setLastModified() - // This will processed all mandatory child nodes only - if(nodeProto.mandatoryChildNodeList && nodeProto.mandatoryChildNodeList.size() > 0) { - for(ProtoNode childNode: nodeProto.mandatoryChildNodeList) { - writeToJcr(childNode, session) - } - } - } + private Session theSession() { ClientBatchJobContext.session diff --git a/src/main/groovy/com/twcable/grabbit/jcr/JCRNodeDecorator.groovy b/src/main/groovy/com/twcable/grabbit/jcr/JcrNodeDecorator.groovy similarity index 64% rename from src/main/groovy/com/twcable/grabbit/jcr/JCRNodeDecorator.groovy rename to src/main/groovy/com/twcable/grabbit/jcr/JcrNodeDecorator.groovy index d3be775..31c8501 100644 --- a/src/main/groovy/com/twcable/grabbit/jcr/JCRNodeDecorator.groovy +++ b/src/main/groovy/com/twcable/grabbit/jcr/JcrNodeDecorator.groovy @@ -19,18 +19,22 @@ import com.twcable.grabbit.proto.NodeProtos.Node as ProtoNode import com.twcable.grabbit.proto.NodeProtos.Node.Builder as ProtoNodeBuilder import com.twcable.grabbit.proto.NodeProtos.Property as ProtoProperty import groovy.transform.CompileStatic - import groovy.util.logging.Slf4j +import org.apache.jackrabbit.commons.JcrUtils import org.apache.jackrabbit.value.DateValue import javax.annotation.Nonnull import javax.annotation.Nullable +import javax.jcr.ItemNotFoundException import javax.jcr.Node as JCRNode import javax.jcr.Property import javax.jcr.Property as JcrProperty import javax.jcr.RepositoryException +import javax.jcr.Session import javax.jcr.nodetype.ItemDefinition +import javax.jcr.version.VersionException +import static javax.jcr.nodetype.NodeType.* import static org.apache.jackrabbit.JcrConstants.* @CompileStatic @@ -45,11 +49,41 @@ class JcrNodeDecorator { private Collection immediateChildNodes - JcrNodeDecorator(@Nonnull JCRNode node) { + JcrNodeDecorator(@Nonnull final JCRNode node) { if(!node) throw new IllegalArgumentException("node must not be null!") this.innerNode = node } + static JcrNodeDecorator createFromProtoNode(@Nonnull final ProtoNodeDecorator protoNode, @Nonnull final Session session) { + final JcrNodeDecorator theDecorator = new JcrNodeDecorator(getOrCreateNode(protoNode, session)) + final mandatoryNodes = protoNode.getMandatoryChildNodes() + mandatoryNodes?.each { + it.writeToJcr(session) + } + //If a version exception is thrown, + try { + protoNode.writableProperties.each { it.writeToNode(innerNode) } + } + catch(VersionException ex) { + JcrNodeDecorator checkedOutNode = theDecorator.checkoutNode() + if (checkedOutNode) { + protoNode.writableProperties.each { it.writeToNode(innerNode) } + checkedOutNode.checkinNode() + } + } + theDecorator.setLastModified() + return theDecorator + } + + /** + * This method is rather succinct, but helps isolate this JcrUtils static method call + * so that we can get better test coverage. + * @param session to create or get the node path for + * @return the newly created, or found node + */ + private static JCRNode getOrCreateNode(ProtoNodeDecorator protoNode, Session session) { + JcrUtils.getOrCreateByPath(protoNode.name, protoNode.primaryType, session) + } /** * @return this node's immediate children, empty if none @@ -135,6 +169,48 @@ class JcrNodeDecorator { } } + private void checkinNode() { + try { + this.session.workspace.versionManager.checkin(this.path) + } + catch (RepositoryException e) { + log.warn("Error checking in node ${this.path}") + } + } + + /** + * Finds out nearest versionable ancestor for a node and performs a checkout + */ + private JcrNodeDecorator checkoutNode() { + try { + JcrNodeDecorator decoratedVersionableAncestor = findVersionableAncestor() + if (decoratedVersionableAncestor && !decoratedVersionableAncestor.isCheckedOut()) { + decoratedVersionableAncestor.session.workspace.versionManager.checkout(decoratedVersionableAncestor.path) + return decoratedVersionableAncestor + } + } + catch (RepositoryException exception) { + log.warn "Could not checkout node ${this.path}, ${exception.message}" + } + return null + } + + private JcrNodeDecorator findVersionableAncestor() { + if (isVersionable()) { + return this + } + try { + JcrNodeDecorator parentDecoratedNode = new JcrNodeDecorator(this.parent) + return parentDecoratedNode.findVersionableAncestor() + } catch (ItemNotFoundException e) { + return null + } + } + + private boolean isVersionable() { + return mixinNodeTypes.any{it in [MIX_SIMPLE_VERSIONABLE, MIX_VERSIONABLE]} + } + /** * Returns the "jcr:lastModified", "cq:lastModified" or "jcr:created" date property * for current Jcr Node diff --git a/src/main/groovy/com/twcable/grabbit/jcr/ProtoNodeDecorator.groovy b/src/main/groovy/com/twcable/grabbit/jcr/ProtoNodeDecorator.groovy index 750eab3..dfc4609 100644 --- a/src/main/groovy/com/twcable/grabbit/jcr/ProtoNodeDecorator.groovy +++ b/src/main/groovy/com/twcable/grabbit/jcr/ProtoNodeDecorator.groovy @@ -56,50 +56,15 @@ class ProtoNodeDecorator { Collection getWritableProperties() { - protoProperties.findAll { !(it.name in [JCR_PRIMARYTYPE, JCR_MIXINTYPES]) } + protoProperties.findAll { !(it.name in [JCR_PRIMARYTYPE]) } } - - JcrNodeDecorator writeToJcr(@Nonnull Session session) { - final jcrNode = getOrCreateNode(session) - //Write mixin types first to avoid InvalidConstraintExceptions - final mixinProperty = getMixinProperty() - if(mixinProperty) { - addMixins(mixinProperty, jcrNode) - } - //Then add other properties - writableProperties.each { it.writeToNode(jcrNode) } - - return new JcrNodeDecorator(jcrNode) + List getMandatoryChildNodes() { + return mandatoryChildNodeList.collect { new ProtoNodeDecorator(it) } } - - /** - * This method is rather succinct, but helps isolate this JcrUtils static method call - * so that we can get better test coverage. - * @param session to create or get the node path for - * @return the newly created, or found node - */ - JCRNode getOrCreateNode(Session session) { - JcrUtils.getOrCreateByPath(innerProtoNode.name, primaryType, session) - } - - - /** - * If a property can be added as a mixin, adds it to the given node - * @param property - * @param node - */ - private static void addMixins(ProtoPropertyDecorator property, JCRNode node) { - property.valuesList.each { ProtoValue value -> - if (node.canAddMixin(value.stringValue)) { - node.addMixin(value.stringValue) - log.debug "Added mixin ${value.stringValue} for : ${node.name}." - } - else { - log.warn "Encountered invalid mixin type while unmarshalling for Proto value : ${value}" - } - } + JcrNodeDecorator writeToJcr(@Nonnull Session session) { + return JcrNodeDecorator.createFromProtoNode(this, session) } } diff --git a/src/main/groovy/com/twcable/grabbit/jcr/ProtoPropertyDecorator.groovy b/src/main/groovy/com/twcable/grabbit/jcr/ProtoPropertyDecorator.groovy index eeb7a4e..43ca4dc 100644 --- a/src/main/groovy/com/twcable/grabbit/jcr/ProtoPropertyDecorator.groovy +++ b/src/main/groovy/com/twcable/grabbit/jcr/ProtoPropertyDecorator.groovy @@ -27,6 +27,7 @@ import javax.jcr.Node as JCRNode import javax.jcr.PropertyType import javax.jcr.Value import javax.jcr.ValueFormatException +import javax.jcr.version.VersionException import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE @@ -43,8 +44,11 @@ class ProtoPropertyDecorator { } - void writeToNode(@Nonnull JCRNode node) { - if(primaryType || mixinType) throw new IllegalStateException("Refuse to write jcr:primaryType or jcr:mixinType as normal properties. These are not allowed") + void writeToNode(@Nonnull JCRNode node) throws VersionException { + if(primaryType) throw new IllegalStateException("Refuse to write jcr:primaryType. This can not be written after node creation") + if(mixinType) { + writeMixinTypeToNode(node) + } try { if (multiple) { node.setProperty(this.name, getPropertyValues(), this.type) @@ -72,6 +76,18 @@ class ProtoPropertyDecorator { } } + void writeMixinTypeToNode(@Nonnull JCRNode node) { + valuesList.each { ProtoValue value -> + if(node.canAddMixin(value.stringValue)){ + node.addMixin(value.stringValue) + log.debug "Added mixin ${value.stringValue} for : ${node.name}." + } + else { + log.warn "Encountered invalid mixin type while unmarshalling for Proto value : ${value}" + } + } + } + boolean isPrimaryType() { innerProtoProperty.name == JCR_PRIMARYTYPE diff --git a/src/test/groovy/com/twcable/grabbit/jcr/JCRNodeDecoratorSpec.groovy b/src/test/groovy/com/twcable/grabbit/jcr/JcrNodeDecoratorSpec.groovy similarity index 86% rename from src/test/groovy/com/twcable/grabbit/jcr/JCRNodeDecoratorSpec.groovy rename to src/test/groovy/com/twcable/grabbit/jcr/JcrNodeDecoratorSpec.groovy index 6b5904e..3c61788 100644 --- a/src/test/groovy/com/twcable/grabbit/jcr/JCRNodeDecoratorSpec.groovy +++ b/src/test/groovy/com/twcable/grabbit/jcr/JcrNodeDecoratorSpec.groovy @@ -16,6 +16,8 @@ package com.twcable.grabbit.jcr import com.day.cq.commons.jcr.JcrConstants +import com.twcable.jackalope.NodeBuilder +import spock.lang.Ignore import spock.lang.Shared import spock.lang.Specification @@ -27,12 +29,12 @@ import javax.jcr.nodetype.ItemDefinition import javax.jcr.nodetype.NodeDefinition import javax.jcr.nodetype.NodeType -import static org.apache.jackrabbit.JcrConstants.JCR_CREATED -import static org.apache.jackrabbit.JcrConstants.JCR_LASTMODIFIED -import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE +import static com.twcable.jackalope.JCRBuilder.node +import static com.twcable.jackalope.JCRBuilder.property +import static org.apache.jackrabbit.JcrConstants.* @SuppressWarnings("GroovyAssignabilityCheck") -class JCRNodeDecoratorSpec extends Specification { +class JcrNodeDecoratorSpec extends Specification { @Shared static Calendar jcrModifiedDate = Calendar.getInstance() static Calendar cqModifiedDate = Calendar.getInstance() @@ -234,4 +236,32 @@ class JCRNodeDecoratorSpec extends Specification { false | false | true | jcrCreatedDate.time false | false | false | null } + + /* + * Needs mixin implementation in Jackalope + * https://github.com/TWCable/jackalope/pull/7 + */ + @Ignore + def "Checkout nearest versionable node"() { + given: + NodeBuilder fakeNodeBuilder = + node("a", + node("b", property("jcr:mixinTypes", ["mix:versionable"].toArray()), + property("jcr:primaryType", "cq:Page"), + node("c", + node("d")), + + ), + ) + + Node parentNode = fakeNodeBuilder.build(); + + final nodeDecorator = new JcrNodeDecorator(parentNode.getNode("b/c/d")) + + when: + nodeDecorator.checkoutNode() + + then: + parentNode.getNode("b").getProperty("jcr:isCheckedOut") == true + } }