Skip to content

Commit

Permalink
Improve HtBody performance yegor256#3
Browse files Browse the repository at this point in the history
  • Loading branch information
victornoel committed Apr 21, 2018
1 parent 92ae400 commit b05a0ac
Show file tree
Hide file tree
Showing 3 changed files with 273 additions and 11 deletions.
130 changes: 119 additions & 11 deletions src/main/java/org/cactoos/http/HtBody.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,20 @@

import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
import org.cactoos.Bytes;
import org.cactoos.Input;
import org.cactoos.io.InputOf;
import org.cactoos.text.TextOf;
import org.cactoos.io.BytesOf;

/**
* Head of HTTP response.
*
* @author Yegor Bugayenko (yegor256@gmail.com)
* @author Victor Noel (victor.noel@crazydwarves.org)
* @version $Id$
* @since 0.1
* @todo #1:30min The implementation of method stream() is less than
* effective, because it reads the entire content of the response, turning
* it into text and then splitting. What if the content is binary and
* can't be converted to string? What if the content is super big
* and must be presented as an stream, not a piece of text. Let's fix
* it.
*/
public final class HtBody implements Input {

Expand All @@ -52,6 +50,7 @@ public final class HtBody implements Input {

/**
* Ctor.
*
* @param rsp Response
*/
public HtBody(final Input rsp) {
Expand All @@ -60,8 +59,117 @@ public HtBody(final Input rsp) {

@Override
public InputStream stream() throws IOException {
return new InputOf(
new TextOf(this.response).asString().split("\r\n\r\n")[1]
).stream();
return new SkipInput(this.response, new BytesOf("\r\n\r\n")).stream();
}

/**
* {@link Input} that skips until it find some defined bytes.
*
* @author Victor Noel (victor.noel@crazydwarves.org)
* @version $Id$
* @since 0.1
*/
static final class SkipInput implements Input {

/**
* The input.
*/
private final Input origin;

/**
* The delimiter.
*/
private final Bytes delimiter;

/**
* Ctor.
*
* @param origin The input.
* @param delimiter The bytes delimiter to skip until.
*/
SkipInput(final Input origin, final Bytes delimiter) {
this.origin = origin;
this.delimiter = delimiter;
}

@Override
public InputStream stream() throws IOException {
final byte[] bytes = this.delimiter.asBytes();
final BoundedByteBuffer buffer = new BoundedByteBuffer(
bytes.length
);
final InputStream stream = this.origin.stream();
boolean eof = false;
while (!eof && !buffer.isEqualsTo(bytes)) {
final int read = stream.read();
if (read < 0) {
eof = true;
} else {
buffer.offer((byte) read);
}
}
return stream;
}
}

/**
* A very simple circular buffer of bytes.
*
* @author Victor Noel (victor.noel@crazydwarves.org)
* @version $Id$
* @since 0.1
*/
static final class BoundedByteBuffer {

/**
* The buffer.
*/
private final Queue<Byte> internal;

/**
* The size limit.
*/
private final int limit;

/**
* Ctor.
*
* @param limit The size limit.
*/
BoundedByteBuffer(final int limit) {
this.limit = limit;
this.internal = new LinkedList<>();
}

/**
* Add a byte to the buffer, potentially by removing the oldest one to
* satisfy the size limit.
*
* @param add The byte to add
*/
public void offer(final byte add) {
if (this.internal.size() == this.limit) {
this.internal.remove();
}
this.internal.add(add);
}

/**
* Test if the buffer contains exactly the <code>bytes</code>.
*
* @param bytes The bytes to compare to.
* @return The value <code>true</code> if the buffer contains exactly
* the <code>bytes</code>.
*/
public boolean isEqualsTo(final byte[] bytes) {
boolean equal = bytes.length == this.internal.size();
final Iterator<Byte> iter = this.internal.iterator();
int idx = 0;
while (iter.hasNext() && equal) {
equal = iter.next() == bytes[idx];
idx = idx + 1;
}
return equal;
}
}
}
55 changes: 55 additions & 0 deletions src/test/java/org/cactoos/http/HtBodyBoundedByteBufferTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* The MIT License (MIT)
*
* Copyright (c) 2018 Yegor Bugayenko
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package org.cactoos.http;

import java.util.Arrays;
import org.cactoos.http.HtBody.BoundedByteBuffer;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.Test;

/**
* Test case for {@link HtBody.BoundedByteBuffer}.
*
* @author Victor Noel (victor.noel@crazydwarves.org)
* @version $Id$
* @since 0.1
* @checkstyle JavadocMethodCheck (500 lines)
*/
public final class HtBodyBoundedByteBufferTest {

@Test
public void onlyKeepsTheLastNBytes() {
final int limit = 4;
final BoundedByteBuffer buffer = new HtBody.BoundedByteBuffer(limit);
final int[] bytes = new int[] {1, 2, 3, 4, 5, 6, 7, 8 };
Arrays.stream(bytes).forEach(b -> buffer.offer((byte) b));
MatcherAssert.assertThat(
// @checkstyle MagicNumberCheck (1 line)
buffer.isEqualsTo(new byte[] {5, 6, 7, 8 }),
Matchers.is(true)
);
}
}
99 changes: 99 additions & 0 deletions src/test/java/org/cactoos/http/HtBodySkipInputTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* The MIT License (MIT)
*
* Copyright (c) 2018 Yegor Bugayenko
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package org.cactoos.http;

import org.cactoos.io.BytesOf;
import org.cactoos.io.InputOf;
import org.cactoos.text.JoinedText;
import org.cactoos.text.TextOf;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.Test;

/**
* Test case for {@link HtBody.SkipInput}.
*
* @author Victor Noel (victor.noel@crazydwarves.org)
* @version $Id$
* @since 0.1
* @checkstyle JavadocMethodCheck (500 lines)
*/
public final class HtBodySkipInputTest {

@Test
public void skipsSomeBytes() throws Exception {
final String prefix = "Hello dude!";
final String suffix = "How are you?";
final String delimiter = "\r";
MatcherAssert.assertThat(
new TextOf(
new HtBody.SkipInput(
new InputOf(
new JoinedText(
delimiter,
prefix,
suffix
)
),
new BytesOf(delimiter)
)
).asString(),
Matchers.equalTo(suffix)
);
}

@Test
public void skipsEverythingWhenNoDelimiter() throws Exception {
MatcherAssert.assertThat(
new TextOf(
new HtBody.SkipInput(
new InputOf("Hello, dude! How are you?"),
new BytesOf("\n")
)
).asString(),
Matchers.equalTo("")
);
}

@Test
public void skipsEverythingWhenEndingWithDelimiter() throws Exception {
final String delimiter = "\r\n";
MatcherAssert.assertThat(
new TextOf(
new HtBody.SkipInput(
new InputOf(
new JoinedText(
"",
"Hello dude! How are you?",
delimiter
)
),
new BytesOf(delimiter)
)
).asString(),
Matchers.equalTo("")
);
}
}

0 comments on commit b05a0ac

Please sign in to comment.