View Javadoc
1   /*
2    * Copyright 2015-2024 Brian Thomas Matthews
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 com.buralotech.oss.jcrunit;
18  
19  import org.apache.jackrabbit.api.JackrabbitSession;
20  import org.apache.jackrabbit.oak.Oak;
21  import org.apache.jackrabbit.oak.jcr.Jcr;
22  
23  import javax.jcr.Credentials;
24  import javax.jcr.Node;
25  import javax.jcr.Repository;
26  import javax.jcr.RepositoryException;
27  import javax.jcr.Session;
28  import javax.jcr.SimpleCredentials;
29  import javax.jcr.ValueFactory;
30  import java.io.ByteArrayInputStream;
31  import java.io.IOException;
32  import java.io.InputStream;
33  import java.nio.charset.StandardCharsets;
34  
35  import static javax.jcr.ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW;
36  import static javax.jcr.Node.JCR_CONTENT;
37  import static javax.jcr.Property.JCR_DATA;
38  import static javax.jcr.Property.JCR_ENCODING;
39  import static javax.jcr.Property.JCR_MIMETYPE;
40  import static javax.jcr.nodetype.NodeType.NT_FILE;
41  import static javax.jcr.nodetype.NodeType.NT_FOLDER;
42  import static javax.jcr.nodetype.NodeType.NT_RESOURCE;
43  import static org.junit.Assert.assertTrue;
44  
45  /**
46   * A JUnit Rule to help test applications that use the Java Content Repository API. This rule creates an in-memory
47   * content repository using <a href="https://jackrabbit.apache.org/oak/">Jackrabbit Oak</a>.
48   *
49   * @author <a href="mailto:brian.matthews@buralo.com">Brian Matthews</a>
50   * @since 3.0
51   */
52  public final class JCRRepositoryTester {
53  
54      /**
55       * The default username and password.
56       */
57      private static final String ADMIN = "admin";
58  
59      /**
60       * The default credentials.
61       */
62      private static final Credentials ADMIN_CREDENTIALS = new SimpleCredentials(ADMIN, ADMIN.toCharArray());
63  
64      /**
65       * The JCR repository.
66       */
67      private final Repository repository;
68  
69      /**
70       * The credentials used to authenticate when connecting to the repository.
71       */
72      private final Credentials credentials;
73  
74      /**
75       * Initialise the helper state with the repository and credentials.
76       *
77       * @param credentials The credentials.
78       */
79      public JCRRepositoryTester(final Repository repository,
80                                 final Credentials credentials) {
81          this.repository = repository;
82          this.credentials = credentials;
83      }
84  
85      /**
86       * Create the {@link JCRRepositoryTester} using the username, password and XML files.
87       *
88       * @param username   The username.
89       * @param password   The user's password.
90       * @param importXMLs Paths of XML files used to populate the repository.
91       * @return A {@link JCRRepositoryTester}.
92       * @throws IOException         If there was a problem reading from an XML file.
93       * @throws RepositoryException If there was a problem creating repository entries.
94       */
95      public static JCRRepositoryTester createHelper(final String username,
96                                                     final String password,
97                                                     final String[] importXMLs)
98              throws IOException, RepositoryException {
99          final Repository repository = new Jcr(new Oak()).createRepository();
100         if (!ADMIN.equals(username)) {
101             final Session session = repository.login(ADMIN_CREDENTIALS);
102             try {
103                 ((JackrabbitSession)session).getUserManager().createUser(username, password);
104                 session.save();
105             } finally {
106                 session.logout();
107             }
108         }
109         final JCRRepositoryTester helper = new JCRRepositoryTester(repository, new SimpleCredentials(username, password.toCharArray()));
110         for (final String path : importXMLs) {
111             helper.importFromXML(ADMIN_CREDENTIALS, path);
112         }
113         return helper;
114     }
115 
116     /**
117      * Create the {@link JCRRepositoryTester} using the details from the {@link JCRRepositoryConfiguration} annotation.
118      *
119      * @param annotation Annotation specifying the username, password and XML files.
120      * @return A {@link JCRRepositoryTester}.
121      * @throws IOException         If there was a problem reading from an XML file.
122      * @throws RepositoryException If there was a problem creating repository entries.
123      */
124     public static JCRRepositoryTester createHelper(final JCRRepositoryConfiguration annotation)
125             throws IOException, RepositoryException {
126         return createHelper(annotation.username(), annotation.password(), annotation.importXMLs());
127     }
128 
129     /**
130      * Return the JCR repository.
131      *
132      * @return The JCR repository.
133      */
134     public Repository getRepository() {
135         return repository;
136     }
137 
138     /**
139      * Create a top-level folder in the repository.
140      *
141      * @param name The name of the new top-level folder to be created.
142      * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
143      */
144     public JCRRepositoryTester createRootFolder(final String name)
145             throws RepositoryException {
146         final Session session = repository.login(credentials);
147         try {
148             final Node parent = session.getRootNode();
149             parent.addNode(name, NT_FOLDER);
150             session.save();
151         } finally {
152             session.logout();
153         }
154         return this;
155     }
156 
157     /**
158      * Create a sub-folder in the repository.
159      *
160      * @param path The fully qualified path of the parent folder.
161      * @param name The name of the new sub-folder to be created.
162      * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
163      */
164     public JCRRepositoryTester createFolder(final String path,
165                                             final String name)
166             throws RepositoryException {
167         final Session session = repository.login(credentials);
168         try {
169             final Node parent = session.getNode(path);
170             parent.addNode(name, NT_FOLDER);
171             session.save();
172         } finally {
173             session.logout();
174         }
175         return this;
176     }
177 
178     /**
179      * Create a file in the repository.
180      *
181      * @param path     The fully qualified path of the parent folder.
182      * @param name     The name of the file to be created.
183      * @param type     The content type of the file.
184      * @param encoding The content encoding of the file.
185      * @param data     The binary content of the file.
186      * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
187      */
188     public JCRRepositoryTester createFile(final String path,
189                                           final String name,
190                                           final String type,
191                                           final String encoding,
192                                           final byte[] data)
193             throws IOException, RepositoryException {
194         try (final InputStream inputStream = new ByteArrayInputStream(data)) {
195             return createFile(path, name, type, encoding, inputStream);
196         }
197     }
198 
199     /**
200      * Create a file in the repository.
201      *
202      * @param path     The fully qualified path of the parent folder.
203      * @param name     The name of the file to be created.
204      * @param type     The content type of the file.
205      * @param encoding The content encoding of the file.
206      * @param data     The string content of the file.
207      * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
208      */
209     public JCRRepositoryTester createFile(final String path,
210                                           final String name,
211                                           final String type,
212                                           final String encoding,
213                                           final String data)
214             throws IOException, RepositoryException {
215         try (final InputStream inputStream = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8))) {
216             return createFile(path, name, type, encoding, inputStream);
217         }
218     }
219 
220     /**
221      * Create a file in the repository.
222      *
223      * @param path        The fully qualified path of the parent folder.
224      * @param name        The name of the file to be created.
225      * @param type        The content type of the file.
226      * @param encoding    The content encoding of the file.
227      * @param inputStream The input stream that provides the binary content of the file.
228      * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
229      */
230     public JCRRepositoryTester createFile(final String path,
231                                           final String name,
232                                           final String type,
233                                           final String encoding,
234                                           final InputStream inputStream)
235             throws RepositoryException {
236         final Session session = repository.login(credentials);
237         try {
238             final ValueFactory valueFactory = session.getValueFactory();
239             final Node parent = session.getNode(path);
240             final Node file = parent.addNode(name, NT_FILE);
241             final Node resource = file.addNode(JCR_CONTENT, NT_RESOURCE);
242             resource.setProperty(JCR_MIMETYPE, type);
243             resource.setProperty(JCR_ENCODING, encoding);
244             resource.setProperty(JCR_DATA, valueFactory.createBinary(inputStream));
245             session.save();
246         } finally {
247             session.logout();
248         }
249         return this;
250     }
251 
252     /**
253      * Assert that a folder exists in the repository.
254      *
255      * @param path The fully qualified path of the folder.
256      * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
257      */
258     public JCRRepositoryTester assertFolderExists(final String path) {
259         try {
260             final Session session = repository.login(credentials);
261             try {
262                 final Node node = session.getNode(path);
263                 assertTrue(path + " is not a folder", node.isNodeType(NT_FOLDER));
264             } finally {
265                 session.logout();
266             }
267         } catch (final RepositoryException e) {
268             throw new AssertionError(path + " does not exist", e);
269         }
270         return this;
271     }
272 
273     /**
274      * Assert that a file exists in the repository.
275      *
276      * @param path The fully qualified path of the file.
277      * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
278      */
279     public JCRRepositoryTester assertFileExists(final String path) {
280         try {
281             final Session session = repository.login(credentials);
282             try {
283                 final Node node = session.getNode(path);
284                 assertTrue(path + " is not a file", node.isNodeType(NT_FILE));
285             } finally {
286                 session.logout();
287             }
288         } catch (final RepositoryException e) {
289             throw new AssertionError(path + " does not exist", e);
290         }
291         return this;
292     }
293 
294     /**
295      * Import file and folder nodes into the repository with from an XML resource on the class path.
296      *
297      * @param path The path of the XML resource on the class path.
298      * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
299      * @throws IOException         If there was a problem reading the XML resource.
300      * @throws RepositoryException If there was a problem importing the XML resource.
301      */
302     public JCRRepositoryTester importFromXML(final String path) throws IOException, RepositoryException {
303         return importFromXML(Thread.currentThread().getContextClassLoader().getResourceAsStream(path));
304     }
305 
306     /**
307      * Import file and folder nodes into the repository with from an XML resource on the class path using
308      * the specified credentials.
309      *
310      * @param credentials The credentials to use for the operation.
311      * @param path The path of the XML resource on the class path.
312      * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
313      * @throws IOException         If there was a problem reading the XML resource.
314      * @throws RepositoryException If there was a problem importing the XML resource.
315      */
316     public JCRRepositoryTester importFromXML(final Credentials credentials,
317                                              final String path)
318             throws IOException, RepositoryException {
319         return importFromXML(credentials, Thread.currentThread().getContextClassLoader().getResourceAsStream(path));
320     }
321 
322     /**
323      * Import file and folder nodes into the repository with from an XML resource loaded from an input stream.
324      *
325      * @param inputStream The input stream from which the XML resource can be loaded.
326      * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
327      * @throws IOException         If there was a problem reading the XML resource.
328      * @throws RepositoryException If there was a problem importing the XML resource.
329      */
330     public JCRRepositoryTester importFromXML(final InputStream inputStream) throws IOException, RepositoryException {
331         return importFromXML(credentials, inputStream);
332     }
333 
334     /**
335      * Import file and folder nodes into the repository with from an XML resource loaded from an input stream using
336      * the specified credentials.
337      *
338      * @param credentials The credentials to use for the operation.
339      * @param inputStream The input stream from which the XML resource can be loaded.
340      * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
341      * @throws IOException         If there was a problem reading the XML resource.
342      * @throws RepositoryException If there was a problem importing the XML resource.
343      */
344     public JCRRepositoryTester importFromXML(final Credentials credentials, final InputStream inputStream)
345             throws IOException, RepositoryException {
346         final Session session = repository.login(credentials);
347         try {
348             session.importXML("/", inputStream, IMPORT_UUID_COLLISION_THROW);
349             session.save();
350         } finally {
351             session.logout();
352         }
353         return this;
354     }
355 }