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