package eu.europeana.cloud.service.mcs.rest; import com.google.common.collect.ImmutableMap; import com.google.common.io.BaseEncoding; import eu.europeana.cloud.common.model.File; import eu.europeana.cloud.common.web.ParamConstants; import eu.europeana.cloud.service.mcs.ApplicationContextUtils; import eu.europeana.cloud.service.mcs.RecordService; import org.glassfish.jersey.client.ClientConfig; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.RequestEntityProcessing; import org.glassfish.jersey.media.multipart.FormDataMultiPart; import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.test.JerseyTest; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.springframework.context.ApplicationContext; import javax.ws.rs.Path; import javax.ws.rs.client.Entity; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Application; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.io.IOException; import java.io.InputStream; import java.security.DigestInputStream; import java.security.MessageDigest; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.reset; /** * This tests checks if content is streamed (not put entirely into memory) when uploading file. */ public class HugeFileResourceUploadIT extends JerseyTest { private static RecordService recordService; private static final int HUGE_FILE_SIZE = 1 << 30; @Before public void mockUp() throws Exception { ApplicationContext applicationContext = ApplicationContextUtils.getApplicationContext(); recordService = applicationContext.getBean(RecordService.class); } @After public void cleanUp() throws Exception { reset(recordService); } @Override public Application configure() { return new JerseyConfig().property("contextConfigLocation", "classpath:hugeFileResourceTestContext.xml"); } @Override protected void configureClient(ClientConfig config) { config.register(MultiPartFeature.class); config.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.CHUNKED); } @Test public void testUploadingHugeFile() throws Exception { String globalId = "globalId", schema = "schema", version = "v1"; // mock answers MockPutContentMethod mockPutContent = new MockPutContentMethod(); doAnswer(mockPutContent).when(recordService).putContent(anyString(), anyString(), anyString(), any(File.class), any(InputStream.class)); WebTarget webTarget = target(FilesResource.class.getAnnotation(Path.class).value()).resolveTemplates( ImmutableMap.<String, Object> of( // ParamConstants.P_CLOUDID, globalId, // ParamConstants.P_REPRESENTATIONNAME, schema, // ParamConstants.P_VER, version)); MessageDigest md = MessageDigest.getInstance("MD5"); DigestInputStream inputStream = new DigestInputStream(new DummyStream(HUGE_FILE_SIZE), md); FormDataMultiPart multipart = new FormDataMultiPart().field(ParamConstants.F_FILE_MIME, MediaType.APPLICATION_OCTET_STREAM).field(ParamConstants.F_FILE_DATA, inputStream, MediaType.APPLICATION_OCTET_STREAM_TYPE); Response response = webTarget.request().post(Entity.entity(multipart, multipart.getMediaType())); assertEquals("Unsuccessful request", Response.Status.Family.SUCCESSFUL, response.getStatusInfo().getFamily()); assertEquals("Wrong size of read content", HUGE_FILE_SIZE, mockPutContent.totalBytes); String contentMd5Hex = BaseEncoding.base16().lowerCase().encode(md.digest()); assertEquals("Content hash mismatch", contentMd5Hex, response.getEntityTag().getValue()); } /** * Mock answer for * {@link ContentService#putContent(eu.europeana.cloud.common.model.Representation, eu.europeana.cloud.common.model.File, java.io.InputStream) * putContent} method. Only counts bytes in input stream. */ static class MockPutContentMethod implements Answer<Object> { int totalBytes; @Override public Object answer(InvocationOnMock invocation) throws Throwable { Object[] args = invocation.getArguments(); File file = (File) args[3]; MessageDigest md = MessageDigest.getInstance("MD5"); DigestInputStream inputStream = new DigestInputStream((InputStream) args[4], md); consume(inputStream); file.setFileName("terefere"); file.setMd5(BaseEncoding.base16().lowerCase().encode(md.digest())); return null; } private void consume(InputStream is) throws IOException { int nRead; byte[] data = new byte[16384]; while ((nRead = is.read(data, 0, data.length)) != -1) { totalBytes += nRead; } } } /** * Input stream that generates unspecified bytes. The total length of stream is specified in constuctor. */ static class DummyStream extends InputStream { /** * Total number of bytes that will be produced in this stream. */ private final int totalLength; /** * Number of bytes that have already been read from this stream. */ private int readLength = 0; public DummyStream(int totalLength) { this.totalLength = totalLength; } @Override public int read() throws IOException { if (readLength >= totalLength) { return -1; } else { readLength++; return 1; } } } }