001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.fcrepo.connector.file;
019
020import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
021import org.infinispan.schematic.document.Document;
022import org.infinispan.schematic.document.EditableDocument;
023import org.infinispan.schematic.document.Json;
024import org.junit.Before;
025import org.junit.Test;
026import org.junit.runner.RunWith;
027import org.mockito.Mock;
028import org.mockito.runners.MockitoJUnitRunner;
029import org.modeshape.jcr.cache.document.DocumentTranslator;
030import org.modeshape.jcr.value.Name;
031import org.modeshape.jcr.value.Property;
032import org.modeshape.jcr.value.basic.BasicName;
033import org.modeshape.jcr.value.basic.BasicSingleValueProperty;
034
035import java.io.File;
036import java.io.FileInputStream;
037import java.io.FileOutputStream;
038import java.io.IOException;
039import java.util.HashMap;
040import java.util.Map;
041
042import static java.nio.file.Files.createTempDirectory;
043import static org.junit.Assert.assertEquals;
044import static org.junit.Assert.assertFalse;
045import static org.junit.Assert.assertTrue;
046import static org.mockito.Matchers.any;
047import static org.mockito.Matchers.anyObject;
048import static org.mockito.Matchers.eq;
049import static org.mockito.Mockito.times;
050import static org.mockito.Mockito.verify;
051import static org.mockito.Mockito.when;
052
053/**
054 * @author Mike Durbin
055 */
056@RunWith(MockitoJUnitRunner.class)
057public class ExternalJsonSidecarExtraPropertyStoreTest {
058
059    @Mock
060    private FedoraFileSystemConnector mockConnector;
061
062    @Mock
063    private DocumentTranslator mockTranslator;
064
065    private static final String FEDERATION_ROOT = "/federation-root";
066    private static final String FILE_PATH = "/federation-root/file";
067    private static final Name KEY1 = new BasicName("info", "one");
068    private static final Name KEY2 = new BasicName("info", "two");
069    private static final Name KEY3 = new BasicName("info", "three");
070    private static final Name KEY4 = new BasicName("info", "four");
071    private static final Name LANG_PROP = new BasicName("lang", "de");
072    private static final Property PROP1 = new BasicSingleValueProperty(LANG_PROP, "eins");
073    private static final Property PROP2 = new BasicSingleValueProperty(LANG_PROP, "zwei");
074    private static final Property PROP3 = new BasicSingleValueProperty(LANG_PROP, "drei");
075
076    @Before
077    public void setUp() {
078        when(mockConnector.fileFor("/")).thenReturn(new File(FEDERATION_ROOT));
079        when(mockConnector.isContentNode("/")).thenReturn(false);
080        when(mockConnector.isRoot("/")).thenReturn(true);
081        when(mockConnector.fileFor("/file")).thenReturn(new File(FILE_PATH));
082        when(mockConnector.isContentNode("/file")).thenReturn(false);
083        when(mockConnector.fileFor("/file/fcr:content")).thenReturn(new File(FILE_PATH));
084        when(mockConnector.isContentNode("/file/fcr:content")).thenReturn(true);
085        when(mockConnector.fileFor("/test/test")).thenReturn(new File(FEDERATION_ROOT + "/test/test"));
086        when(mockConnector.isContentNode("/test/test")).thenReturn(false);
087    }
088
089    @Test
090    public void testSidecarFile() throws IOException {
091        final File tmp = createTempDirectory("filesystem-federation").toFile();
092        tmp.deleteOnExit();
093
094        final ExternalJsonSidecarExtraPropertyStore store =
095                new ExternalJsonSidecarExtraPropertyStore(mockConnector, mockTranslator, tmp);
096        assertEquals(new File(tmp, "federation-root.modeshape.json"), store.sidecarFile("/"));
097        assertEquals(new File(tmp, "file.modeshape.json"), store.sidecarFile("/file"));
098        assertEquals(new File(tmp, "file.content.modeshape.json"), store.sidecarFile("/file/fcr:content"));
099        assertEquals(new File(tmp + "/test", "test.modeshape.json"), store.sidecarFile("/test/test"));
100    }
101
102    @Test
103    public void testEmptyPropertyFile() throws IOException {
104        final File tmp = createTempDirectory("filesystem-federation").toFile();
105        tmp.deleteOnExit();
106
107        final ExternalJsonSidecarExtraPropertyStore store =
108                new ExternalJsonSidecarExtraPropertyStore(mockConnector, mockTranslator, tmp);
109
110        assertFalse(store.contains("/file"));
111        assertTrue(store.getProperties("/file").isEmpty());
112    }
113
114    @Test
115    public void testStoreProperties() throws IOException {
116        final File tmp = createTempDirectory("filesystem-federation").toFile();
117        tmp.deleteOnExit();
118
119        final ExternalJsonSidecarExtraPropertyStore store =
120                new ExternalJsonSidecarExtraPropertyStore(mockConnector, mockTranslator, tmp);
121
122        final Map<Name, Property> properties = new HashMap<>();
123
124        store.storeProperties("/file", properties);
125
126        assertFalse(store.contains("/file"));
127        assertTrue(store.getProperties("/file").isEmpty());
128
129        properties.put(KEY1, PROP1);
130        properties.put(KEY2, PROP2);
131        properties.put(KEY3, PROP3);
132        properties.put(KEY4, null);
133
134        store.storeProperties("/file", properties);
135        assertTrue(store.contains("/file"));
136
137        properties.forEach((key, property) -> {
138            if (property != null) {
139                verify(mockTranslator).setProperty(any(EditableDocument.class), eq(property), anyObject(), anyObject());
140            }
141        });
142        verify(mockTranslator, times(3)).setProperty(any(EditableDocument.class),
143                    any(Property.class), anyObject(), anyObject());
144    }
145
146    @Test
147    public void testStoreExistingProperties() throws IOException {
148        final File tmp = createTempDirectory("filesystem-federation").toFile();
149        final File sidecarFile = new File(tmp, "file.modeshape.json");
150        tmp.deleteOnExit();
151        final String jsonString = "{}";
152        try (final FileOutputStream fos = new FileOutputStream(sidecarFile)) {
153            fos.write(jsonString.getBytes("UTF-8"));
154            fos.close();
155
156            final ExternalJsonSidecarExtraPropertyStore store =
157                    new ExternalJsonSidecarExtraPropertyStore(mockConnector, mockTranslator, tmp);
158
159            final Map<Name, Property> properties = new HashMap<>();
160
161            assertTrue(store.contains("/file"));
162
163            properties.put(KEY1, PROP1);
164            properties.put(KEY2, PROP2);
165            properties.put(KEY3, PROP3);
166            properties.put(KEY4, null);
167
168            store.storeProperties("/file", properties);
169
170            properties.forEach((key, property) -> {
171                if (property != null) {
172                    verify(mockTranslator).setProperty(any(EditableDocument.class), eq(property), anyObject(),
173                            anyObject());
174                }
175            });
176            verify(mockTranslator, times(3)).setProperty(any(EditableDocument.class),
177                    any(Property.class), anyObject(), anyObject());
178        }
179    }
180
181
182    @Test
183    public void testUpdateProperties() throws IOException {
184        final File tmp = createTempDirectory("filesystem-federation").toFile();
185        tmp.deleteOnExit();
186
187        final ExternalJsonSidecarExtraPropertyStore store =
188                new ExternalJsonSidecarExtraPropertyStore(mockConnector, mockTranslator, tmp);
189
190        final Map<Name, Property> properties = new HashMap<>();
191
192        store.updateProperties("/file", properties);
193
194        assertFalse(store.contains("/file"));
195        assertTrue(store.getProperties("/file").isEmpty());
196
197        properties.put(KEY1, PROP1);
198        properties.put(KEY2, PROP2);
199        properties.put(KEY3, null);
200
201        store.updateProperties("/file", properties);
202
203        properties.forEach((key, property) -> {
204            if (property == null) {
205                verify(mockTranslator).removeProperty(any(EditableDocument.class), eq(key), anyObject(), anyObject());
206            } else {
207                verify(mockTranslator).setProperty(any(EditableDocument.class), eq(property), anyObject(), anyObject());
208            }
209        });
210    }
211
212    @Test
213    public void testUpdateExistingProperties() throws IOException {
214        final File tmp = createTempDirectory("filesystem-federation").toFile();
215        tmp.deleteOnExit();
216        final File sidecarFile = new File(tmp, "file.modeshape.json");
217        final String jsonString = "{}";
218        try (final FileOutputStream fos = new FileOutputStream(sidecarFile)) {
219            fos.write(jsonString.getBytes("UTF-8"));
220            fos.close();
221
222            final ExternalJsonSidecarExtraPropertyStore store =
223                    new ExternalJsonSidecarExtraPropertyStore(mockConnector, mockTranslator, tmp);
224
225            assertTrue(store.contains("/file"));
226
227            final Map<Name, Property> properties = new HashMap<>();
228
229            properties.put(KEY1, PROP1);
230            properties.put(KEY2, PROP2);
231            properties.put(KEY3, null);
232
233            store.updateProperties("/file", properties);
234
235            properties.forEach((key, property) -> {
236                if (property == null) {
237                    verify(mockTranslator).removeProperty(any(EditableDocument.class), eq(key), anyObject(),
238                            anyObject());
239                } else {
240                    verify(mockTranslator).setProperty(any(EditableDocument.class), eq(property), anyObject(),
241                            anyObject());
242                }
243            });
244        }
245    }
246
247    @Test
248    public void testRemoveProperties() throws IOException {
249        final File tmp = createTempDirectory("filesystem-federation").toFile();
250        tmp.deleteOnExit();
251
252        final ExternalJsonSidecarExtraPropertyStore store =
253                new ExternalJsonSidecarExtraPropertyStore(mockConnector, mockTranslator, tmp);
254
255        final Map<Name, Property> properties = new HashMap<>();
256
257        properties.put(KEY1, PROP1);
258        properties.put(KEY2, PROP2);
259        properties.put(KEY3, PROP3);
260
261        store.updateProperties("/file", properties);
262        assertTrue(store.contains("/file"));
263
264        store.removeProperties("/file");
265        assertFalse(store.contains("/file"));
266    }
267
268    @Test
269    public void testGetProperties() throws IOException {
270        final File tmp = createTempDirectory("filesystem-federation").toFile();
271        tmp.deleteOnExit();
272        final File sidecarFile = new File(tmp, "file.content.modeshape.json");
273        final String jsonString = "{" +
274            "\"properties\" : { " +
275                "\"http://www.jcp.org/jcr/1.0\" : {" +
276                    "\"created\" : { " +
277                        "\"$date\" : \"2008-09-23T15:19:20.000-04:00\" } } ," +
278                "\"http://fedora.info/definitions/v4/repository#\" : {" +
279                    "\"digest\" : { " +
280                        "\"$uri\" : \"urn:sha1:6e1a2e24a4cc3dde495877019f53830b8f1d20e3\" } } } }";
281        try (final FileOutputStream fos = new FileOutputStream(sidecarFile)) {
282            fos.write(jsonString.getBytes("UTF-8"));
283        }
284
285        final ExternalJsonSidecarExtraPropertyStore store =
286                new ExternalJsonSidecarExtraPropertyStore(mockConnector, mockTranslator, tmp);
287
288        assertTrue(store.contains("/file/fcr:content"));
289
290        try (final FileInputStream sidecarStream = new FileInputStream(sidecarFile)) {
291            final Document document = Json.read(sidecarStream, false);
292            final Map<Name, Property> results = new HashMap<>();
293
294            store.getProperties("/file/fcr:content");
295
296            verify(mockTranslator).getProperties(eq(document), eq(results));
297        }
298    }
299
300    @Test(expected = RepositoryRuntimeException.class)
301    public void testGetPropertiesWithException() throws IOException {
302        final File tmp = createTempDirectory("filesystem-federation").toFile();
303        tmp.deleteOnExit();
304        final File sidecarFile = new File(tmp, "file.modeshape.json");
305        final String jsonString = "{ THIS ISN'T JSON !";
306        try (final FileOutputStream fos = new FileOutputStream(sidecarFile)) {
307            fos.write(jsonString.getBytes("UTF-8"));
308        }
309
310        final ExternalJsonSidecarExtraPropertyStore store =
311                new ExternalJsonSidecarExtraPropertyStore(mockConnector, mockTranslator, tmp);
312
313        store.getProperties("/file");
314    }
315
316}