diff --git a/reyna-test/src/com/b2msolutions/reyna/DispatcherTest.java b/reyna-test/src/com/b2msolutions/reyna/DispatcherTest.java index bf17965..905beaa 100644 --- a/reyna-test/src/com/b2msolutions/reyna/DispatcherTest.java +++ b/reyna-test/src/com/b2msolutions/reyna/DispatcherTest.java @@ -4,13 +4,16 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.icu.util.Output; import android.net.ConnectivityManager; import android.net.NetworkInfo; import com.b2msolutions.reyna.Dispatcher.Result; import com.b2msolutions.reyna.blackout.Time; import com.b2msolutions.reyna.blackout.TimeRange; import com.b2msolutions.reyna.http.HttpPost; +import com.b2msolutions.reyna.http.OutputStreamFactory; import com.b2msolutions.reyna.shadows.ShadowAndroidHttpClient; +import com.b2msolutions.reyna.system.Header; import com.b2msolutions.reyna.system.Clock; import com.b2msolutions.reyna.system.Message; import com.b2msolutions.reyna.system.Preferences; @@ -25,8 +28,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Matchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -47,7 +53,9 @@ import java.util.GregorianCalendar; import java.util.zip.GZIPOutputStream; +import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNull; import static org.junit.Assert.*; import static org.mockito.Mockito.*; import static org.robolectric.Shadows.shadowOf; @@ -60,6 +68,7 @@ public class DispatcherTest { private Intent batteryStatus; @Mock NetworkInfo networkInfo; @Mock Date now; + @Mock OutputStreamFactory outputStreamFactory; @Before public void setup() { @@ -87,7 +96,7 @@ public void sendMessageHappyPathShouldSetExecuteCorrectHttpPostAndReturnOK() thr Time time = mock(Time.class); - assertEquals(Result.OK, new Dispatcher().sendMessage(message, httpPost, httpClient, this.context)); + assertEquals(Result.OK, new Dispatcher(this.outputStreamFactory).sendMessage(message, httpPost, httpClient, this.context)); this.verifyHttpPost(message, httpPost); @@ -112,7 +121,7 @@ public void sendMessageHappyPathWithChineseCharactersShouldSetExecuteCorrectHttp when(httpClient.execute(httpPost)).thenReturn(httpResponse); Clock clock = mock(Clock.class); - Dispatcher dispatcher = new Dispatcher(); + Dispatcher dispatcher = new Dispatcher(this.outputStreamFactory); dispatcher.clock = clock; assertEquals(Result.OK, dispatcher.sendMessage(message, httpPost, httpClient, this.context)); @@ -140,7 +149,7 @@ public void sendMessageHappyPathWithPortShouldSetPort() throws URISyntaxExceptio when(httpClient.execute(httpPost)).thenReturn(httpResponse); Clock clock = mock(Clock.class); - Dispatcher dispatcher = new Dispatcher(); + Dispatcher dispatcher = new Dispatcher(this.outputStreamFactory); dispatcher.clock = clock; assertEquals(Result.OK, dispatcher.sendMessage(message, httpPost, httpClient, this.context)); @@ -162,7 +171,7 @@ public void sendMessageShouldReturnBlackoutWhenInBlackout() { new Preferences(this.context).saveCellularDataBlackout(range); Clock clock = mock(Clock.class); - Dispatcher dispatcher = new Dispatcher(); + Dispatcher dispatcher = new Dispatcher(this.outputStreamFactory); dispatcher.clock = clock; assertEquals(Result.BLACKOUT, dispatcher.sendMessage(null, null, null, this.context)); @@ -173,7 +182,7 @@ public void sendMessageShouldReturnNotConnectedWhenNotConnected() { when(this.networkInfo.isConnectedOrConnecting()).thenReturn(false); Clock clock = mock(Clock.class); - Dispatcher dispatcher = new Dispatcher(); + Dispatcher dispatcher = new Dispatcher(this.outputStreamFactory); dispatcher.clock = clock; assertEquals(Result.NOTCONNECTED, dispatcher.sendMessage(null, null, null, this.context)); @@ -188,7 +197,7 @@ public void whenExecuteThrowsReturnTemporaryError() throws URISyntaxException, I when(httpClient.execute(httpPost)).thenThrow(new RuntimeException("")); Clock clock = mock(Clock.class); - Dispatcher dispatcher = new Dispatcher(); + Dispatcher dispatcher = new Dispatcher(this.outputStreamFactory); dispatcher.clock = clock; assertEquals(Result.TEMPORARY_ERROR, dispatcher.sendMessage(message, httpPost, httpClient, this.context)); @@ -209,6 +218,7 @@ public void getResultShouldReturnExpected() { @Test public void sendMessageWithGzipAndContentIsLessThanMinGzipLengthShouldRemoveGzipHeaderAndSendMessageAsString() throws Exception { Message message = RepositoryTest.getMessageWithGzipHeaders("body"); + message.addHeader(new Header("Content-Encoding", " gzip ")); StatusLine statusLine = mock(StatusLine.class); when(statusLine.getStatusCode()).thenReturn(200); @@ -221,24 +231,24 @@ public void sendMessageWithGzipAndContentIsLessThanMinGzipLengthShouldRemoveGzip when(httpClient.execute(httpPost)).thenReturn(httpResponse); Clock clock = mock(Clock.class); - Dispatcher dispatcher = new Dispatcher(); + Dispatcher dispatcher = new Dispatcher(this.outputStreamFactory); dispatcher.clock = clock; assertEquals(Result.OK, dispatcher.sendMessage(message, httpPost, httpClient, this.context)); this.verifyHttpPost(message, httpPost); - ArgumentCaptor byteArrayEntityCaptor = ArgumentCaptor.forClass(ByteArrayEntity.class); - verify(httpPost).setEntity(byteArrayEntityCaptor.capture()); - ByteArrayEntity entity = byteArrayEntityCaptor.getValue(); + ArgumentCaptor stringEntityArgumentCaptor = ArgumentCaptor.forClass(StringEntity.class); + verify(httpPost).setEntity(stringEntityArgumentCaptor.capture()); + AbstractHttpEntity entity = stringEntityArgumentCaptor.getValue(); assertNull(entity.getContentEncoding()); assertEquals(EntityUtils.toString(entity, "utf-8"), "body"); } @Test - public void sendMessageWithGzipHeaderShouldCompressContentAndReturnOK() throws URISyntaxException, IOException, KeyManagementException, UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException, CertificateException { - Message message = RepositoryTest.getMessageWithGzipHeaders("this any message body more than 10 bytes length"); - byte[] data = "this any message body more than 10 bytes length".getBytes("utf-8"); + public void sendMessageWithGzipHeaderAndMoreThan20BytesAndStreamCompressDataShouldSetCorrectContentEncoding() throws URISyntaxException, IOException, KeyManagementException, UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException, CertificateException { + Message message = RepositoryTest.getMessageWithGzipHeaders("this any message body more than 20 bytes length"); + byte[] data = "this any message body more than 20 bytes length".getBytes("utf-8"); StatusLine statusLine = mock(StatusLine.class); when(statusLine.getStatusCode()).thenReturn(200); @@ -249,8 +259,15 @@ public void sendMessageWithGzipHeaderShouldCompressContentAndReturnOK() throws U HttpClient httpClient = mock(HttpClient.class); when(httpClient.execute(httpPost)).thenReturn(httpResponse); + ArgumentCaptor outputStreamCaptor = ArgumentCaptor.forClass(OutputStream.class); + doAnswer(new Answer(){ + public Object answer(InvocationOnMock invocation) throws IOException { + OutputStream os = (OutputStream)invocation.getArguments()[0]; + return new GZIPOutputStream(os); + }}).when(this.outputStreamFactory).createGzipOutputStream(any(OutputStream.class)); + Clock clock = mock(Clock.class); - Dispatcher dispatcher = new Dispatcher(); + Dispatcher dispatcher = new Dispatcher(this.outputStreamFactory); dispatcher.clock = clock; Result actual = dispatcher.sendMessage(message, httpPost, httpClient, this.context); @@ -268,6 +285,76 @@ public void sendMessageWithGzipHeaderShouldCompressContentAndReturnOK() throws U assertArrayEquals(EntityUtils.toByteArray(byteArrayEntity), expected); } + @Test + public void sendMessageWithGzipHeaderAndMoreThan20BytesAndStreamDoNotCompressDataShouldSetCorrectContentEncoding() throws URISyntaxException, IOException, KeyManagementException, UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException, CertificateException { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 100; i++){ + sb.append("This should be a big message body. "); + } + + Message message = RepositoryTest.getMessageWithGzipHeaders(sb.toString()); + byte[] data = sb.toString().getBytes("UTF-8"); + + StatusLine statusLine = mock(StatusLine.class); + when(statusLine.getStatusCode()).thenReturn(200); + HttpResponse httpResponse = mock(HttpResponse.class); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + + HttpPost httpPost = mock(HttpPost.class); + HttpClient httpClient = mock(HttpClient.class); + when(httpClient.execute(httpPost)).thenReturn(httpResponse); + + when(this.outputStreamFactory.createGzipOutputStream(any(OutputStream.class))).thenReturn(new ByteArrayOutputStream()); + + Clock clock = mock(Clock.class); + Dispatcher dispatcher = new Dispatcher(this.outputStreamFactory); + dispatcher.clock = clock; + + Result actual = dispatcher.sendMessage(message, httpPost, httpClient, this.context); + + assertEquals(Result.OK, actual); + + this.verifyHttpPost(message, httpPost); + + ArgumentCaptor entityCaptor = ArgumentCaptor.forClass(ByteArrayEntity.class); + verify(httpPost).setEntity(entityCaptor.capture()); + AbstractHttpEntity byteArrayEntity = entityCaptor.getValue(); + assertEquals(byteArrayEntity.getContentEncoding().getValue(), ""); + + assertArrayEquals(EntityUtils.toByteArray(byteArrayEntity), data); + } + + @Test + public void sendMessageWithoutGzipHeaderAndBigContentShouldNotCompressAndReturnOk() throws URISyntaxException, IOException, KeyManagementException, UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException, CertificateException { + Message message = RepositoryTest.getMessageWithHeaders("this any message body more than 20 bytes length. Really. Should be really bigger then 20."); + + StatusLine statusLine = mock(StatusLine.class); + when(statusLine.getStatusCode()).thenReturn(200); + HttpResponse httpResponse = mock(HttpResponse.class); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + + HttpPost httpPost = mock(HttpPost.class); + HttpClient httpClient = mock(HttpClient.class); + when(httpClient.execute(httpPost)).thenReturn(httpResponse); + + Clock clock = mock(Clock.class); + Dispatcher dispatcher = new Dispatcher(this.outputStreamFactory); + dispatcher.clock = clock; + + Result actual = dispatcher.sendMessage(message, httpPost, httpClient, this.context); + + assertEquals(Result.OK, actual); + + this.verifyHttpPost(message, httpPost); + + ArgumentCaptor entityCaptor = ArgumentCaptor.forClass(ByteArrayEntity.class); + verify(httpPost).setEntity(entityCaptor.capture()); + AbstractHttpEntity entity = entityCaptor.getValue(); + assertNull(entity.getContentEncoding()); + String data = EntityUtils.toString(entity); + assertEquals(message.getBody(), data); + } + @Test public void canSendShouldReturnNOTCONNECTEDIfNoActiveNetwork() { ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); @@ -795,7 +882,7 @@ public void whenCallingSendMessageShouldAddSubmittedTimestamp() throws IOExcepti when(httpClient.execute(httpPost)).thenReturn(httpResponse); Clock clock = mock(Clock.class); - Dispatcher dispatcher = new Dispatcher(); + Dispatcher dispatcher = new Dispatcher(this.outputStreamFactory); when(clock.getCurrentTimeMillis()).thenReturn(42L); dispatcher.clock = clock; diff --git a/reyna-test/src/com/b2msolutions/reyna/http/OutputStreamFactoryTest.java b/reyna-test/src/com/b2msolutions/reyna/http/OutputStreamFactoryTest.java new file mode 100644 index 0000000..f8ace05 --- /dev/null +++ b/reyna-test/src/com/b2msolutions/reyna/http/OutputStreamFactoryTest.java @@ -0,0 +1,29 @@ +package com.b2msolutions.reyna.http; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.GZIPOutputStream; + +import static junit.framework.Assert.*; + +@RunWith(RobolectricTestRunner.class) +public class OutputStreamFactoryTest { + + @Test + public void whenConstructingShouldNotThrow(){ + assertNotNull(new OutputStreamFactory()); + } + + @Test + public void whenCreateGzipOutputStreamShouldReturnRightType() throws IOException { + OutputStreamFactory factory = new OutputStreamFactory(); + OutputStream outputStream = factory.createGzipOutputStream(new ByteArrayOutputStream()); + assertTrue(outputStream instanceof GZIPOutputStream); + } + +} diff --git a/reyna/src/com/b2msolutions/reyna/Dispatcher.java b/reyna/src/com/b2msolutions/reyna/Dispatcher.java index 691014b..7a5c2e0 100644 --- a/reyna/src/com/b2msolutions/reyna/Dispatcher.java +++ b/reyna/src/com/b2msolutions/reyna/Dispatcher.java @@ -10,27 +10,36 @@ import com.b2msolutions.reyna.blackout.TimeRange; import com.b2msolutions.reyna.http.HttpPost; import com.b2msolutions.reyna.blackout.BlackoutTime; +import com.b2msolutions.reyna.http.OutputStreamFactory; import com.b2msolutions.reyna.system.*; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.entity.AbstractHttpEntity; +import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.AbstractHttpClient; import org.apache.http.protocol.HTTP; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; import java.net.URI; import java.text.ParseException; import java.util.ArrayList; import java.util.GregorianCalendar; import java.util.Locale; +import java.util.zip.GZIPOutputStream; public class Dispatcher { private static final String TAG = "com.b2msolutions.reyna.Dispatcher"; + private OutputStreamFactory outputStreamFactory; + protected Clock clock; - public Dispatcher() { + public Dispatcher(OutputStreamFactory outputStreamFactory) { this.clock = new Clock(); + this.outputStreamFactory = outputStreamFactory; } public enum Result { @@ -179,7 +188,7 @@ private Result parseHttpPost(Message message, HttpPost httpPost, Context context URI uri = message.getURI(); httpPost.setURI(uri); - AbstractHttpEntity entity = Dispatcher.getEntity(message, context); + AbstractHttpEntity entity = this.getEntity(message, context); httpPost.setEntity(entity); this.setHeaders(httpPost, message.getHeaders()); @@ -232,8 +241,7 @@ private static Header[] removeGzipEncodingHeader(Header[] headers) { ArrayList
filteredHeaders = new ArrayList
(); for (Header header : headers) { - if (header.getKey().equalsIgnoreCase("content-encoding") - && header.getValue().equalsIgnoreCase("gzip")) { + if (header.getKey().equalsIgnoreCase("content-encoding")) { continue; } @@ -244,9 +252,44 @@ private static Header[] removeGzipEncodingHeader(Header[] headers) { return filteredHeaders.toArray(returnedHeaders); } - private static AbstractHttpEntity getCompressedEntity(String content, Context context) throws Exception { - byte[] data = content.getBytes(); - return AndroidHttpClient.getCompressedEntity(data, context.getContentResolver()); + private AbstractHttpEntity getEntity(Message message, Context context) throws Exception { + String content = message.getBody(); + + byte[] data = content.getBytes("UTF-8"); + if (!shouldGzip(message.getHeaders()) || data.length <= (AndroidHttpClient.getMinGzipSize(context.getContentResolver()) * 2)){ + return new StringEntity(content, HTTP.UTF_8); + } + else { + return getCompressedEntity(data, context); + } + } + + private AbstractHttpEntity getCompressedEntity(byte[] data, Context context) throws Exception { + ByteArrayOutputStream arr = new ByteArrayOutputStream(); + OutputStream zipper = this.outputStreamFactory.createGzipOutputStream(arr); + zipper.write(data); + zipper.close(); + + byte[] compressedData = arr.toByteArray(); + int end = compressedData.length > 500 ? 500 : compressedData.length; + + Boolean equal = true; + for (int i = 0; i < end; i++){ + if (compressedData[i] != data[i]){ + equal = false; + break; + } + } + AbstractHttpEntity entity; + if (!equal){ + entity = new ByteArrayEntity(compressedData); + entity.setContentEncoding("gzip"); + } + else { + entity = new ByteArrayEntity(data); + entity.setContentEncoding(""); + } + return entity; } private void setHeaders(HttpPost httpPost, Header[] headers) { @@ -263,16 +306,7 @@ private String getSubmittedTimestamp() { return String.valueOf(this.clock.getCurrentTimeMillis()); } - private static AbstractHttpEntity getEntity(Message message, Context context) throws Exception { - String content = message.getBody(); - AbstractHttpEntity entity = new StringEntity(content, HTTP.UTF_8); - - if (Dispatcher.shouldGzip(message.getHeaders())) { - entity = getCompressedEntity(content, context); - } - return entity; - } public static boolean isBatteryCharging(Context context) { Intent batteryStatus = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); diff --git a/reyna/src/com/b2msolutions/reyna/http/OutputStreamFactory.java b/reyna/src/com/b2msolutions/reyna/http/OutputStreamFactory.java new file mode 100644 index 0000000..cb7baaa --- /dev/null +++ b/reyna/src/com/b2msolutions/reyna/http/OutputStreamFactory.java @@ -0,0 +1,11 @@ +package com.b2msolutions.reyna.http; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.GZIPOutputStream; + +public class OutputStreamFactory { + public OutputStream createGzipOutputStream(OutputStream newStreamConstructorArg) throws IOException { + return new GZIPOutputStream(newStreamConstructorArg); + } +} diff --git a/reyna/src/com/b2msolutions/reyna/services/ForwardService.java b/reyna/src/com/b2msolutions/reyna/services/ForwardService.java index 7854f2f..c3c5f55 100644 --- a/reyna/src/com/b2msolutions/reyna/services/ForwardService.java +++ b/reyna/src/com/b2msolutions/reyna/services/ForwardService.java @@ -6,6 +6,7 @@ import com.b2msolutions.reyna.*; import com.b2msolutions.reyna.Dispatcher.Result; import com.b2msolutions.reyna.blackout.Time; +import com.b2msolutions.reyna.http.OutputStreamFactory; import com.b2msolutions.reyna.system.*; import com.b2msolutions.reyna.messageProvider.BatchProvider; import com.b2msolutions.reyna.messageProvider.IMessageProvider; @@ -35,7 +36,7 @@ public ForwardService() { Logger.v(TAG, "ForwardService()"); - this.dispatcher = new Dispatcher(); + this.dispatcher = new Dispatcher(new OutputStreamFactory()); this.thread = new Thread(); this.periodicBackoutCheck = new PeriodicBackoutCheck(this); this.repository = new Repository(this);