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