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 }