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.integration.connector.file;
019
020import static com.hp.hpl.jena.rdf.model.ResourceFactory.createResource;
021import static java.lang.System.clearProperty;
022import static java.lang.System.getProperty;
023import static java.lang.System.setProperty;
024import static java.nio.file.Files.write;
025import static java.util.Arrays.asList;
026import static com.google.common.collect.Lists.transform;
027import static org.fcrepo.kernel.api.FedoraTypes.CONTENT_SIZE;
028import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_BINARY;
029import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_NON_RDF_SOURCE_DESCRIPTION;
030import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_CONTAINER;
031import static org.fcrepo.kernel.api.RdfLexicon.HAS_MESSAGE_DIGEST;
032import static org.fcrepo.kernel.api.RdfCollectors.toModel;
033import static org.fcrepo.kernel.api.utils.ContentDigest.asURI;
034import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getJcrNode;
035import static org.junit.Assert.assertEquals;
036import static org.junit.Assert.assertFalse;
037import static org.junit.Assert.assertNotEquals;
038import static org.junit.Assert.assertNotNull;
039import static org.junit.Assert.assertTrue;
040import static org.junit.Assert.fail;
041import static org.modeshape.common.util.SecureHash.getHash;
042import static org.modeshape.common.util.SecureHash.Algorithm.SHA_1;
043import static org.slf4j.LoggerFactory.getLogger;
044
045import java.io.File;
046import java.io.IOException;
047import java.net.URI;
048import java.nio.file.Files;
049import java.nio.file.Paths;
050import java.security.NoSuchAlgorithmException;
051import java.util.Collection;
052import java.util.Iterator;
053
054import javax.inject.Inject;
055import javax.jcr.Node;
056import javax.jcr.Repository;
057import javax.jcr.RepositoryException;
058import javax.jcr.Session;
059import javax.jcr.nodetype.NodeType;
060
061import com.hp.hpl.jena.rdf.model.Model;
062
063import org.apache.commons.io.FileUtils;
064import org.apache.commons.io.filefilter.TrueFileFilter;
065import org.apache.commons.io.filefilter.WildcardFileFilter;
066
067import org.fcrepo.kernel.api.models.Container;
068import org.fcrepo.kernel.api.models.FedoraBinary;
069import org.fcrepo.kernel.api.models.FedoraResource;
070import org.fcrepo.kernel.api.services.BinaryService;
071import org.fcrepo.kernel.api.services.ContainerService;
072import org.fcrepo.kernel.api.services.NodeService;
073import org.fcrepo.kernel.modeshape.rdf.impl.DefaultIdentifierTranslator;
074
075import org.junit.AfterClass;
076import org.junit.BeforeClass;
077import org.junit.Test;
078import org.junit.runner.RunWith;
079import org.slf4j.Logger;
080import org.springframework.test.context.ContextConfiguration;
081import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
082
083/**
084 * An abstract suite of tests that should work against any configuration
085 * of a FedoraFileSystemFederation.  Tests that only work on certain
086 * configurations (ie, require read/write capabilities) should be implemented
087 * in subclasses.
088 *
089 * @author Andrew Woods
090 * @since 2014-2-3
091 */
092@ContextConfiguration({"/spring-test/repo.xml"})
093@RunWith(SpringJUnit4ClassRunner.class)
094public abstract class AbstractFedoraFileSystemConnectorIT {
095
096    @Inject
097    protected Repository repo;
098
099    @Inject
100    protected NodeService nodeService;
101
102    @Inject
103    protected ContainerService containerService;
104
105    @Inject
106    protected BinaryService binaryService;
107
108    /**
109     * Gets the path (relative to the filesystem federation) of a directory
110     * that's expected to be present.
111     *
112     * @return string that contains the path to the dir
113     */
114    protected abstract String testDirPath();
115
116    /**
117     * Gets the path (relative to the filesystem federation) of a file
118     * that's expected to be present.
119     *
120     * @return string that contains the path to the file
121     */
122    protected abstract String testFilePath();
123
124    /**
125     * The name (relative path) of the federation to be tested.  This
126     * must coincide with the "projections" provided in repository.json.
127     *
128     * @return string that contains the path to the federation
129     */
130    protected abstract String federationName();
131
132    /**
133     * The filesystem path for the root of the filesystem federation being
134     * tested.  This must coincide with the "directoryPath" provided in
135     * repository.json (or the system property that's populating the relevant
136     * configuration".
137     *
138     * @return string that contains the path to root
139     */
140    protected abstract String getFederationRoot();
141
142    private final static String PROP_TEST_DIR1 = "fcrepo.test.connector.file.directory1";
143    private final static String PROP_TEST_DIR2 = "fcrepo.test.connector.file.directory2";
144    private final static String PROP_EXT_TEST_DIR = "fcrepo.test.connector.file.properties.directory";
145
146    protected String getReadWriteFederationRoot() {
147        return getProperty(PROP_TEST_DIR1);
148    }
149
150    protected String getReadOnlyFederationRoot() {
151        return getProperty(PROP_TEST_DIR2);
152    }
153
154    private static final Logger logger =
155            getLogger(AbstractFedoraFileSystemConnectorIT.class);
156
157    /**
158     * Sets a system property and ensures artifacts from previous tests are
159     * cleaned up.
160     */
161    @BeforeClass
162    public static void setSystemPropertiesAndCleanUp() {
163
164        // Instead of creating dummy files over which to federate,
165        // we configure the FedoraFileSystemFederation instances to
166        // point to paths within the "target" directory.
167        final File testDir1 = new File("target/test-classes/config/testing");
168        setProperty(PROP_TEST_DIR1, testDir1.getAbsolutePath());
169
170        final File testDir2 = new File("target/test-classes/spring-test");
171        cleanUpJsonFilesFiles(testDir2);
172        setProperty(PROP_TEST_DIR2, testDir2.getAbsolutePath());
173
174        final File testPropertiesDir = new File("target/test-classes-properties");
175        if (testPropertiesDir.exists()) {
176            cleanUpJsonFilesFiles(testPropertiesDir);
177        } else {
178            testPropertiesDir.mkdir();
179        }
180        setProperty(PROP_EXT_TEST_DIR, testPropertiesDir.getAbsolutePath());
181    }
182
183    @AfterClass
184    public static void unsetSystemPropertiesAndCleanUp() {
185        clearProperty(PROP_TEST_DIR1);
186        clearProperty(PROP_TEST_DIR2);
187        clearProperty(PROP_EXT_TEST_DIR);
188    }
189
190    protected static void cleanUpJsonFilesFiles(final File directory) {
191        final WildcardFileFilter filter = new WildcardFileFilter("*.modeshape.json");
192        final Collection<File> files = FileUtils.listFiles(directory, filter, TrueFileFilter.INSTANCE);
193        final Iterator<File> iterator = files.iterator();
194
195        // Clean up files persisted in previous runs
196        while (iterator.hasNext()) {
197            final File f = iterator.next();
198            final String path = f.getAbsolutePath();
199            try {
200                Files.deleteIfExists(Paths.get(path));
201            } catch (final IOException e) {
202                logger.error("Error in clean up", e);
203                fail("Unable to delete work files from a previous test run. File=" + path);
204            }
205        }
206    }
207
208    @Test
209    public void testGetFederatedObject() throws RepositoryException {
210        final Session session = repo.login();
211
212        final Container object = containerService.findOrCreate(session, testDirPath());
213        assertNotNull(object);
214
215        final Node node = getJcrNode(object);
216        final NodeType[] mixins = node.getMixinNodeTypes();
217        assertEquals(2, mixins.length);
218
219        final boolean found = transform(asList(mixins), NodeType::getName).contains(FEDORA_CONTAINER);
220        assertTrue("Mixin not found: " + FEDORA_CONTAINER, found);
221
222        session.save();
223        session.logout();
224    }
225
226    @Test
227    public void testGetFederatedDatastream() throws RepositoryException {
228        final Session session = repo.login();
229
230        final FedoraResource description = binaryService.findOrCreate(session, testFilePath()).getDescription();
231        assertNotNull(description);
232
233        final Node node = getJcrNode(description);
234        final NodeType[] mixins = node.getMixinNodeTypes();
235        assertEquals(2, mixins.length);
236
237        final boolean found = transform(asList(mixins), NodeType::getName)
238                .contains(FEDORA_NON_RDF_SOURCE_DESCRIPTION);
239        assertTrue("Mixin not found: " + FEDORA_NON_RDF_SOURCE_DESCRIPTION, found);
240
241        session.save();
242        session.logout();
243    }
244
245    @Test
246    public void testGetFederatedContent() throws RepositoryException {
247        final Session session = repo.login();
248
249        final Node node = getJcrNode(nodeService.find(session, testFilePath() + "/jcr:content"));
250        assertNotNull(node);
251
252        final NodeType[] mixins = node.getMixinNodeTypes();
253        assertEquals(2, mixins.length);
254
255        final boolean found = transform(asList(mixins), NodeType::getName).contains(FEDORA_BINARY);
256        assertTrue("Mixin not found: " + FEDORA_BINARY, found);
257
258        final File file = fileForNode();
259
260        assertTrue(file.getAbsolutePath(), file.exists());
261        assertEquals(file.length(), node.getProperty(CONTENT_SIZE).getLong());
262
263        session.save();
264        session.logout();
265    }
266
267    @Test
268    public void testFixity() throws RepositoryException, IOException, NoSuchAlgorithmException {
269        final Session session = repo.login();
270
271        checkFixity(binaryService.findOrCreate(session, testFilePath()));
272
273        session.save();
274        session.logout();
275    }
276
277    @Test
278    public void testChangedFileFixity() throws RepositoryException, IOException, NoSuchAlgorithmException {
279        final Session session = repo.login();
280
281        final FedoraBinary binary = binaryService.findOrCreate(session, testFilePath());
282
283        final String originalFixity = checkFixity(binary);
284
285        final File file = fileForNode();
286        write(file.toPath(), " ".getBytes("UTF-8"));
287
288        final String newFixity = checkFixity(binary);
289
290        assertNotEquals("Checksum is expected to have changed!", originalFixity, newFixity);
291
292        session.save();
293        session.logout();
294    }
295
296    private String checkFixity(final FedoraBinary binary)
297            throws IOException, NoSuchAlgorithmException, RepositoryException {
298        assertNotNull(binary);
299
300        final File file = fileForNode();
301        final byte[] hash = getHash(SHA_1, file);
302
303        final URI calculatedChecksum = asURI(SHA_1.toString(), hash);
304
305        final DefaultIdentifierTranslator graphSubjects = new DefaultIdentifierTranslator(repo.login());
306        final Model results = binary.getFixity(graphSubjects).collect(toModel());
307        assertNotNull(results);
308
309        assertFalse("Found no results!", results.isEmpty());
310
311
312        assertTrue("Expected to find checksum",
313                results.contains(null,
314                        HAS_MESSAGE_DIGEST,
315                        createResource(calculatedChecksum.toString())));
316
317        return calculatedChecksum.toString();
318    }
319
320    protected File fileForNode() {
321        return new File(getFederationRoot(), testFilePath().replace(federationName(), ""));
322    }
323
324    /**
325     * The following is painfully tied to some implementation details
326     * but it's critical that we test that the json files are actually written
327     * somewhere, so it's the best I can do without further opening up the
328     * internals of JsonSidecarExtraPropertiesStore.
329     *
330     * @param node The node to access for the file reference
331     * @return A reference to the nodes property file
332     */
333    protected File propertyFileForNode(final Node node) {
334        try {
335            System.out.println("NODE PATH: " + node.getPath());
336        } catch (final RepositoryException e) {
337            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
338        }
339        return new File(getProperty(PROP_EXT_TEST_DIR),
340                testFilePath().replace(federationName(), "") + ".modeshape.json");
341    }
342}