View Javadoc
1   /*
2    * Copyright 2012-2024 Bloomreach
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.onehippo.forge.utilities.commons.jcrmockup;
18  
19  import java.util.ArrayList;
20  import java.util.Calendar;
21  import java.util.Collection;
22  import java.util.Collections;
23  import java.util.HashMap;
24  import java.util.Iterator;
25  import java.util.List;
26  import java.util.Map;
27  
28  import javax.jcr.Node;
29  import javax.jcr.NodeIterator;
30  import javax.jcr.RepositoryException;
31  import javax.jcr.Session;
32  import javax.jcr.Value;
33  import javax.jcr.Workspace;
34  import javax.jcr.nodetype.NodeType;
35  import javax.jcr.version.VersionManager;
36  import jakarta.xml.bind.annotation.XmlAttribute;
37  import jakarta.xml.bind.annotation.XmlElement;
38  import jakarta.xml.bind.annotation.XmlRootElement;
39  import jakarta.xml.bind.annotation.XmlTransient;
40  
41  import org.mockito.Matchers;
42  import org.mockito.Mockito;
43  import org.mockito.invocation.InvocationOnMock;
44  import org.mockito.stubbing.Answer;
45  
46  /**
47   * JAXB annotated backing mock node for javax.jcr.Node
48   */
49  @XmlRootElement(name = "node", namespace = MockNode.HTTP_WWW_JCP_ORG_JCR_SV_1_0)
50  public class MockNode {
51  
52      public static final String HTTP_WWW_JCP_ORG_JCR_SV_1_0 = "http://www.jcp.org/jcr/sv/1.0";
53  
54      // initialized with the first node mockup
55      private static MockNode rootMockNode;
56      private static Session session = Mockito.mock(Session.class);
57  
58      @XmlTransient
59      private boolean removed = false;
60  
61      @XmlTransient
62      private MockNode parent;
63  
64      @XmlElement(name = "node", namespace = MockNode.HTTP_WWW_JCP_ORG_JCR_SV_1_0)
65      private List<MockNode> childNodes;
66  
67      @XmlElement(name = "property", namespace = MockNode.HTTP_WWW_JCP_ORG_JCR_SV_1_0)
68      private List<MockProperty> properties;
69  
70      @XmlTransient
71      private boolean propertiesMapInitialized = false;
72  
73      @XmlTransient
74      private Map<String, MockProperty> propertiesMap = new HashMap<String, MockProperty>();
75  
76      @XmlAttribute(name = "name", namespace = MockNode.HTTP_WWW_JCP_ORG_JCR_SV_1_0)
77      private String name;
78  
79      public List<MockNode> getMockChildNodes() {
80          if (childNodes == null) {
81              return Collections.emptyList();
82          }
83          List<MockNode> notRemovedChildNodes = new ArrayList<MockNode>();
84          Iterator<MockNode> childIterator = childNodes.iterator();
85          while (childIterator.hasNext()) {
86              MockNode child = childIterator.next();
87              if (child.isRemoved()) {
88                  childIterator.remove();
89              } else {
90                  notRemovedChildNodes.add(child);
91              }
92          }
93          return notRemovedChildNodes;
94      }
95  
96      public void addMockChildNode(MockNode childNode) {
97          if (this.childNodes == null) {
98              this.childNodes = new ArrayList<MockNode>();
99          }
100         childNode.setParent(this);
101         this.childNodes.add(childNode);
102     }
103 
104     /**
105      * The loaded mock node is initialized with a list by JAXB, however a node can only have one property of a certain
106      * name. To ensure this and improve performance the initial list ist backed in a map.
107      */
108     private void initializePropertiesMap() {
109         if (!propertiesMapInitialized && this.properties != null) {
110             final Iterator<MockProperty> propertyIterator = this.properties.iterator();
111             while (propertyIterator.hasNext()) {
112                 final MockProperty mockProperty = propertyIterator.next();
113                 if (mockProperty.isRemoved()) {
114                     propertyIterator.remove();
115                 } else {
116                     this.propertiesMap.put(mockProperty.getMockPropertyName(), mockProperty);
117                 }
118             }
119             propertiesMapInitialized = true;
120         }
121     }
122 
123     public MockProperty getMockProperty(String name) {
124         initializePropertiesMap();
125         if (!this.propertiesMap.containsKey(name)) {
126             return null;
127         }
128         final MockProperty mockProperty = this.propertiesMap.get(name);
129         if (mockProperty.isRemoved()) {
130             this.propertiesMap.remove(name);
131             return null;
132         }
133         return mockProperty;
134     }
135 
136     public Collection<MockProperty> getMockProperties() {
137         initializePropertiesMap();
138         final List<MockProperty> result = new ArrayList<MockProperty>();
139         final Iterator<MockProperty> mockPropertyIterator = this.propertiesMap.values().iterator();
140         while (mockPropertyIterator.hasNext()) {
141             final MockProperty mockProperty = mockPropertyIterator.next();
142             if (mockProperty.isRemoved()) {
143                 mockPropertyIterator.remove();
144             } else {
145                 result.add(mockProperty);
146             }
147         }
148         return result;
149     }
150 
151     public MockNode getMockChildNode(String name) {
152         if (childNodes == null) {
153             return null;
154         }
155         Iterator<MockNode> childIterator = childNodes.iterator();
156         while (childIterator.hasNext()) {
157             MockNode child = childIterator.next();
158             if (child.isRemoved()) {
159                 childIterator.remove();
160             } else if (name.equals(child.getMockNodeName())) {
161                 return child;
162             }
163         }
164         return null;
165     }
166 
167     public List<MockNode> getMockChildNodesByName(String name) {
168         if (childNodes == null) {
169             return Collections.emptyList();
170         }
171         List<MockNode> foundNodes = new ArrayList<MockNode>();
172         Iterator<MockNode> childIterator = childNodes.iterator();
173         while (childIterator.hasNext()) {
174             MockNode child = childIterator.next();
175             if (child.isRemoved()) {
176                 childIterator.remove();
177             } else if (name.equals(child.getMockNodeName())) {
178                 foundNodes.add(child);
179             }
180         }
181         return foundNodes;
182     }
183 
184     public void setMockProperty(MockProperty property) {
185         initializePropertiesMap();
186         property.setParent(this);
187         this.propertiesMap.put(property.getMockPropertyName(), property);
188     }
189 
190     public String getMockNodeName() {
191         return name;
192     }
193 
194     public void setMockNodeName(String name) {
195         this.name = name;
196     }
197 
198     public boolean isRemoved() {
199         return removed;
200     }
201 
202     public void setRemoved(final boolean removed) {
203         this.removed = removed;
204     }
205 
206     public MockNode getParent() {
207         return parent;
208     }
209 
210     public void setParent(MockNode parent) {
211         this.parent = parent;
212     }
213 
214     public String getPath() {
215         if (MockNode.getRooMockNode() == this) {
216             return "/";
217         }
218         int index = getIndex();
219         if (this.parent == rootMockNode) {
220             if (index > 1) {
221                 return "/"+this.name+"["+index+"]";
222             }
223             return "/" + this.name;
224         } else {
225             if (index > 1) {
226                 return this.parent.getPath() + "/" + this.name+"["+index+"]";
227             }
228             return this.parent.getPath() + "/" + this.name;
229         }
230     }
231 
232     public void buildTree(final MockNode parent) {
233         this.parent = parent;
234         if (this.properties != null) {
235             for (MockProperty property : this.properties) {
236                 property.setParent(this);
237             }
238         }
239         if (childNodes != null) {
240             for (MockNode childNode : childNodes) {
241                 childNode.buildTree(this);
242             }
243         }
244     }
245 
246     public int getIndex() {
247         if (this.parent == null) {
248             return 0;
249         }
250         int index = 1;
251         for (MockNode sibling : this.parent.childNodes) {
252             if (this.name.equals(sibling.name)) {
253                 if (this.equals(sibling)) {
254                     return index;
255                 }
256                 index++;
257             }
258         }
259         return 0;
260     }
261 
262     public Node getJcrMock() throws RepositoryException {
263         return mockJcrNode(this);
264     }
265 
266     /**
267      * Mocks a javax.jcr.Node by backing a MockNode.
268      *
269      * @param mockNode the node to mock as javax.jcr.Node
270      * @return the mocked jcr node
271      * @throws RepositoryException if mocking the node fails
272      */
273     public static Node mockJcrNode(final MockNode mockNode) throws RepositoryException {
274         Node jcrNode = Mockito.mock(Node.class);
275         Mockito.when(jcrNode.getName()).thenReturn(mockNode.getMockNodeName());
276 
277         mockUuid(mockNode, jcrNode);
278 
279         mockMixins(mockNode, jcrNode);
280 
281         final Answer<NodeIterator> nodeIteratorAnswer = new NodeIteratorAnswer(mockNode);
282         Mockito.when(jcrNode.getNodes(Matchers.anyString())).thenAnswer(nodeIteratorAnswer);
283         Mockito.when(jcrNode.getNodes()).thenAnswer(nodeIteratorAnswer);
284 
285         Mockito.when(jcrNode.getProperties()).thenAnswer(new PropertyIteratorAnswer(mockNode));
286 
287         final ItemAnswer itemAnswer = new ItemAnswer(mockNode);
288         Mockito.when(jcrNode.getNode(Matchers.anyString())).thenAnswer(itemAnswer);
289         Mockito.when(jcrNode.getProperty(Matchers.anyString())).thenAnswer(itemAnswer);
290         Mockito.when(jcrNode.setProperty(Matchers.anyString(), Matchers.anyLong())).thenAnswer(itemAnswer);
291         Mockito.when(jcrNode.setProperty(Matchers.anyString(), Matchers.anyDouble())).thenAnswer(itemAnswer);
292         Mockito.when(jcrNode.setProperty(Matchers.anyString(), Matchers.anyString())).thenAnswer(itemAnswer);
293         Mockito.when(jcrNode.setProperty(Matchers.anyString(), Matchers.anyBoolean())).thenAnswer(itemAnswer);
294         Mockito.when(jcrNode.setProperty(Matchers.anyString(), Matchers.any(String[].class))).thenAnswer(itemAnswer);
295         Mockito.when(jcrNode.setProperty(Matchers.anyString(), Matchers.any(Calendar.class))).thenAnswer(itemAnswer);
296         Mockito.when(jcrNode.setProperty(Matchers.anyString(), Matchers.any(Value.class))).thenAnswer(itemAnswer);
297         Mockito.when(jcrNode.setProperty(Matchers.anyString(), Matchers.any(Value[].class))).thenAnswer(itemAnswer);
298         Mockito.when(jcrNode.addNode(Matchers.anyString())).thenAnswer(itemAnswer);
299         Mockito.when(jcrNode.addNode(Matchers.anyString(), Matchers.anyString())).thenAnswer(itemAnswer);
300         Mockito.when(jcrNode.getParent()).thenAnswer(itemAnswer);
301         Mockito.doAnswer(new Answer() {
302             public Object answer(final InvocationOnMock invocationOnMock) {
303                 mockNode.setRemoved(true);
304                 return null;
305             }
306         }).when(jcrNode).remove();
307 
308         Mockito.when(jcrNode.getPath()).thenAnswer(new Answer<String>() {
309             public String answer(final InvocationOnMock invocationOnMock) {
310                 return mockNode.getPath();
311             }
312         });
313 
314         final UnsupportedOperationException unsupportedOperation = new UnsupportedOperationException("The method getProperties(pattern) is not supported yet.");
315         Mockito.doThrow(unsupportedOperation).when(jcrNode).getProperties(Matchers.anyString());
316 
317         final MockProperty primaryTypeProperty = mockNode.getMockProperty("jcr:primaryType");
318         Mockito.when(jcrNode.isNodeType(Matchers.anyString())).thenAnswer(new Answer<Boolean>() {
319             public Boolean answer(InvocationOnMock invocationOnMock) {
320                 final Object args[] = invocationOnMock.getArguments();
321                 return primaryTypeProperty != null && args[0] instanceof String && (args[0]).equals(primaryTypeProperty.getMockValues().get(0));
322             }
323         });
324 
325         final NodeType primaryNodeType = Mockito.mock(NodeType.class);
326         Mockito.when(primaryNodeType.getName()).thenReturn(primaryTypeProperty.getMockValues().get(0));
327         Mockito.when(jcrNode.getPrimaryNodeType()).thenReturn(primaryNodeType);
328 
329         Mockito.when(jcrNode.hasProperty(Matchers.anyString())).thenAnswer(new Answer<Boolean>() {
330             public Boolean answer(InvocationOnMock invocationOnMock) {
331                 final Object args[] = invocationOnMock.getArguments();
332                 return mockNode.getMockProperty((String) args[0]) != null;
333             }
334         });
335         Mockito.when(jcrNode.hasNode(Matchers.anyString())).thenAnswer(new Answer<Boolean>() {
336             public Boolean answer(InvocationOnMock invocationOnMock) {
337                 final Object args[] = invocationOnMock.getArguments();
338                 return mockNode.getMockChildNode((String) args[0]) != null;
339             }
340         });
341         Mockito.when(jcrNode.getIndex()).thenAnswer(new Answer<Integer>() {
342             public Integer answer(final InvocationOnMock invocationOnMock) {
343                 return mockNode.getIndex();
344             }
345         });
346 
347         if (rootMockNode == null) {
348             // initialize with the first node mock
349             rootMockNode = mockNode;
350             session = Mockito.mock(Session.class);
351             Mockito.when(session.getItem(Matchers.anyString())).thenAnswer(itemAnswer);
352             Mockito.when(session.getRootNode()).thenAnswer(itemAnswer);
353             final Workspace mockWorkspace = Mockito.mock(Workspace.class);
354             final VersionManager versionManager = Mockito.mock(VersionManager.class);
355             Mockito.when(session.getWorkspace()).thenReturn(mockWorkspace);
356             Mockito.when(mockWorkspace.getVersionManager()).thenReturn(versionManager);
357             Mockito.when(session.itemExists(Matchers.anyString())).thenAnswer(new Answer<Boolean>() {
358                 public Boolean answer(final InvocationOnMock invocationOnMock) throws RepositoryException {
359                     return itemAnswer.getItem(invocationOnMock.getArguments()) != null;
360                 }
361             });
362         }
363 
364         Mockito.when(jcrNode.getSession()).thenReturn(session);
365 
366         return jcrNode;
367     }
368 
369     private static void mockMixins(MockNode mockNode, Node jcrNode) throws RepositoryException {
370         final MockProperty mixinProperty = mockNode.getMockProperty("jcr:mixinTypes");
371         if (mixinProperty != null) {
372             final List<String> values = mixinProperty.getMockValues();
373             final List<NodeType> nodeTypes = new ArrayList<NodeType>();
374             for (String value : values) {
375                 final NodeType nodeType = Mockito.mock(NodeType.class);
376                 Mockito.when(nodeType.getName()).thenReturn(value);
377                 nodeTypes.add(nodeType);
378             }
379             Mockito.when(jcrNode.getMixinNodeTypes()).thenReturn(nodeTypes.toArray(new NodeType[nodeTypes.size()]));
380         }
381     }
382 
383     private static void mockUuid(MockNode mockNode, Node jcrNode) throws RepositoryException {
384         final MockProperty uuidProperty = mockNode.getMockProperty("jcr:uuid");
385         if (uuidProperty != null) {
386             final List<String> values = uuidProperty.getMockValues();
387             if (values.size() == 1) {
388                 Mockito.when(jcrNode.getIdentifier()).thenReturn(values.get(0));
389             }
390         }
391     }
392 
393     /**
394      * Invalidates the session for all nodes.
395      */
396     public static void invalidateSession() {
397         rootMockNode = null;
398         session = Mockito.mock(Session.class);
399     }
400 
401     public static MockNode getRooMockNode() {
402         return rootMockNode;
403     }
404 }