Java tutorial
/* * Licensed to DuraSpace under one or more contributor license agreements. * See the NOTICE file distributed with this work for additional information * regarding copyright ownership. * * DuraSpace licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.fcrepo.apix.integration; import org.apache.camel.CamelContext; import org.apache.camel.builder.AdviceWithRouteBuilder; import org.apache.camel.model.ModelCamelContext; import org.apache.commons.io.input.NullInputStream; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.ResourceFactory; import org.fcrepo.client.FcrepoOperationFailedException; import org.fcrepo.client.FcrepoResponse; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import org.junit.runner.RunWith; import org.ops4j.pax.exam.junit.PaxExam; import org.ops4j.pax.exam.util.Filter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** * Integration tests relating to streaming content proxied by API-X * * @author esm */ @RunWith(PaxExam.class) public class StreamingIT implements KarafIT { private static final Logger LOG = LoggerFactory.getLogger(StreamingIT.class); private static final URI APIX_BASE_URI = URI.create(apixBaseURI); private static final String INTERCEPT_ROUTE_ID = "execute-intercept"; private static final String CONTEXT_NAME = "apix-core"; private static final String CONTEXT_ROLE = "routing-context"; private URI binaryContainer; private URI binaryResource; private String binaryResourceSha; private static MessageDigest sha1; @Rule public TestName name = new TestName(); @Inject @Filter("(role=" + CONTEXT_ROLE + ")") private CamelContext ctx; @Override public String testClassName() { return this.getClass().getSimpleName(); } @Override public String testMethodName() { return name.getMethodName(); } @BeforeClass public static void initMessageDigest() throws NoSuchAlgorithmException { sha1 = MessageDigest.getInstance("SHA-1"); } /** * Creates a container and a binary resource of 2MiB + 1 bytes long. Retrieves checksum of the resource. * * @throws FcrepoOperationFailedException if unexpected things go wrong * @throws IOException if unexpected things go wrong * @throws URISyntaxException if unexpected things go wrong */ @Before public void initBinaryResources() throws FcrepoOperationFailedException, IOException, URISyntaxException { binaryContainer = URI.create(String.format("%s%s", fcrepoBaseURI, testClassName() + "/binaries/")); // Create container if it doesn't already exist if (!resourceExists(binaryContainer)) { try (FcrepoResponse r = client.put(binaryContainer) .body(new FileInputStream(new File(testResources, "objects/binary_container.ttl")), "text/turtle") .perform()) { assertEquals("Failed to create binary container '" + binaryContainer + "'", 201, r.getStatusCode()); } } // Create the binary resource if it doesn't already exist final URI expectedBinaryResource = appendToPath(binaryContainer, "large-binary"); if (!resourceExists(expectedBinaryResource)) { LOG.warn("Expected resource did not exist {}", expectedBinaryResource); try { binaryResource = postFromStream(new NullInputStream((2 * 1024 * 1024) + 1), binaryContainer, "application/octet-stream", "large-binary"); } catch (Exception e) { fail(String.format("Failed to create binary LDPR: %s", e.getMessage())); } } else { binaryResource = expectedBinaryResource; } // Retrieve the checksum calculated by Fedora binaryResourceSha = ModelFactory.createDefaultModel() .read(client.get(appendToPath(binaryResource, "/fcr:metadata")).accept("application/rdf+xml") .perform().getBody(), null) .listObjectsOfProperty( ResourceFactory.createProperty("http://www.loc.gov/premis/rdf/v1#", "hasMessageDigest")) .mapWith((digestValue) -> digestValue.toString().substring("urn:sha1:".length())).next(); assertNotNull("Missing http://www.loc.gov/premis/rdf/v1#hasMessageDigest on " + appendToPath(binaryResource, "/fcr:metadata").toString(), binaryResourceSha); } /** * Smoke test insuring Karaf is doing what we think it is doing */ @Before public void verifyContextAndRoute() { assertNotNull("No context", ctx); assertEquals("Unexpected context " + ctx.getName(), CONTEXT_NAME, ctx.getName()); assertNotNull("No route (ctx name: " + ctx.getName() + ")", ctx.getRouteDefinition(INTERCEPT_ROUTE_ID)); } /** * Verify the binary can be retrieved from Fedora. The request should <em>not</em> be intercepted. * * @throws Exception if unexpected things go wrong */ @Test public void testRetrieveLargeBinaryFromFedora() throws Exception { // Record 'true' if the intercepting route is triggered final AtomicBoolean intercepted = new AtomicBoolean(false); ctx.getRouteDefinition(INTERCEPT_ROUTE_ID).adviceWith((ModelCamelContext) ctx, new AdviceWithRouteBuilder() { @Override public void configure() throws Exception { weaveAddFirst().process((ex) -> intercepted.set(true)); } }); final long expectedSize = (2 * 1024 * 1024) + 1; final long actualSize; final String actualDigest; try (FcrepoResponse r = client.get(binaryResource).perform(); DigestInputStream body = new DigestInputStream(r.getBody(), sha1)) { actualSize = drain(body); actualDigest = asHex(body.getMessageDigest().digest()); } // The resource can be retrieved intact assertEquals(expectedSize, actualSize); assertEquals(binaryResourceSha, actualDigest); // And the request was not proxied by API-X assertFalse(String.format("Unexpected interception of a Fedora resource URI %s by route %s", binaryResource.toString(), INTERCEPT_ROUTE_ID), intercepted.get()); } /** * Verify the binary can be retrieved through the API-X proxy. The request should be intercepted and proxied * by API-X. * * @throws Exception if unexpected things go wrong */ @Test public void testRetrieveLargeBinaryFromApix() throws Exception { // Record 'true' if the intercepting route is triggered final AtomicBoolean intercepted = new AtomicBoolean(false); ctx.getRouteDefinition(INTERCEPT_ROUTE_ID).adviceWith((ModelCamelContext) ctx, new AdviceWithRouteBuilder() { @Override public void configure() throws Exception { weaveAddFirst().process((ex) -> intercepted.set(true)); } }); final long expectedSize = (2 * 1024 * 1024) + 1; final long actualSize; final String actualDigest; final URI proxiedResource = proxied(binaryResource); try (FcrepoResponse r = KarafIT.attempt(30, () -> client.get(proxiedResource).perform()); DigestInputStream body = new DigestInputStream(r.getBody(), sha1)) { actualSize = drain(body); actualDigest = asHex(body.getMessageDigest().digest()); } // The request _was_ proxied by API-X assertTrue(String.format("Expected the retrieval of %s to be proxied by API-X, route id %s", proxiedResource, INTERCEPT_ROUTE_ID), intercepted.get()); // And resource can be retrieved intact assertEquals(expectedSize, actualSize); assertEquals(binaryResourceSha, actualDigest); } /** * Returns true if the URI exists (i.e. responds with a 200 to a HEAD request). * * @param resource some HTTP resource * @return true if the resource exists, false otherwise * @throws IOException if there is an error determining whether the resource exists */ private boolean resourceExists(final URI resource) throws IOException { try (FcrepoResponse r = client.head(resource).perform()) { if (r.getStatusCode() == 200) { return true; } } catch (FcrepoOperationFailedException e) { // Probably the resource doesn't exist. LOG.debug("Error retrieving resource '" + resource + "': " + e.getMessage(), e); } return false; } /** * Reads the input stream to exhaustion and returns the number of bytes read. * * @param in the stream to exhaust * @return the number of bytes read * @throws IOException */ private static long drain(final InputStream in) throws IOException { final byte[] buf = new byte[1024 * 128]; long size = 0; for (int i = in.read(buf, 0, buf.length); i > -1; i = in.read(buf, 0, i)) { size += i; } return size; } /** * Coverts the supplied byte array to a String hexadecimal representation, starting with the most significant bit. * * @param digest a byte array containing a message digest * @return a hexadecimal string representation of the message digest */ private static String asHex(final byte[] digest) { final StringBuilder buf = new StringBuilder(); for (int i = 0; i < digest.length; i++) { buf.append(String.format("%02x", Byte.toUnsignedInt(digest[i]))); } return buf.toString(); } /** * Assuming the supplied URI targets the <em>Fedora repository</em> (i.e. <em>a URI that is not proxied by * API-X</em>), returns an equivalent URI that targets the same resource through the API-X proxy. * * @param toProxy a URI that targets an un-proxied Fedora resource * @return an equivalent URI targeting the proxied Fedora resource * @throws URISyntaxException */ private static URI proxied(final URI toProxy) throws URISyntaxException { if (isProxied(toProxy)) { return toProxy; } return appendToPath(APIX_BASE_URI, toProxy.getPath()); } /** * Appends the path to the URI. All other components of the URI are preserved. * * @param uri the URI with the path being appended to * @param toAppend the path to be appended to the URI * @return a new URI with a path component ending with {@code toAppend} * @throws URISyntaxException */ private static URI appendToPath(final URI uri, final String toAppend) throws URISyntaxException { return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath() + toAppend, uri.getRawQuery(), uri.getRawFragment()); } /** * Returns true if the supplied URI will be proxied by API-X * * @param uri a candidate uri that might be proxied API-X * @return true if the supplied URI will be proxied by API-X, false otherwise */ private static boolean isProxied(final URI uri) { return uri.getScheme().equals(APIX_BASE_URI.getScheme()) && uri.getHost().equals(APIX_BASE_URI.getHost()) && uri.getPort() == APIX_BASE_URI.getPort(); } }