diff --git a/search-services/alfresco-solrclient-lib/src/main/java/org/alfresco/solr/client/LookAheadBufferedReader.java b/search-services/alfresco-solrclient-lib/src/main/java/org/alfresco/solr/client/LookAheadBufferedReader.java new file mode 100644 index 000000000..50edf4018 --- /dev/null +++ b/search-services/alfresco-solrclient-lib/src/main/java/org/alfresco/solr/client/LookAheadBufferedReader.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2005-2020 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.solr.client; + +import java.io.BufferedReader; +import java.util.LinkedList; + +import static java.util.stream.Collector.of; + +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.Reader; + +/** + * This is an enhanced {@link BufferedReader} used for transparently collect data from the incoming stream before + * it gets consumed by the usual buffered reader logic. The data is not actually consumed, it is just buffered beside and + * it can be printed in case we want to debug the underlying stream content. + * + * The name refers to the usage pattern: this reader is used for wrapping a character stream coming from a remote call and, in case + * of issues, it collects the remaining (i.e. unread) part of the stream so it will be available for debugging purposes. + * + * It provides two different collecting modes: + * + * + * + * The two modes are activated on each instance depending on the level of the {@link Logger} passed in input. + * Specifically: + * + * + * + */ +public class LookAheadBufferedReader extends BufferedReader +{ + final static String BUFFERING_DISABLED_INFO_MESSAGE = "Not available: please set the logging LEVEL to DEBUG or TRACE."; + + private interface BufferingMode + { + void append(char ch); + + void forceAppend(char ch); + + boolean canAccept(boolean force); + } + + private static class Windowing implements BufferingMode + { + private final LinkedList window = new LinkedList<>(); + private final int maxSize; + + private Windowing(int maxSize) + { + this.maxSize = maxSize; + } + + @Override + public void append(char ch) + { + window.add(ch); + if (window.size() == maxSize) + { + window.removeFirst(); + } + } + + @Override + public void forceAppend(char ch) + { + window.add(ch); + } + + @Override + public boolean canAccept(boolean force) + { + return window.size() < (maxSize * 2); + } + + @Override + public String toString() + { + return window.stream() + .collect( + of( + StringBuilder::new, + StringBuilder::append, + StringBuilder::append, + StringBuilder::toString)); + } + } + + private static class WholeValue implements BufferingMode + { + private final StringBuilder content = new StringBuilder(); + + @Override + public void append(char ch) + { + content.append(ch); + } + + @Override + public void forceAppend(char ch) + { + content.append(ch); + } + + @Override + public String toString() + { + return content.toString(); + } + + @Override + public boolean canAccept(boolean force) + { + return true; + } + } + + private static class NoOp implements BufferingMode + { + @Override + public void append(char ch) + { + // Nothing to be done here + } + + @Override + public void forceAppend(char ch) + { + // Nothing to be done here + } + + @Override + public boolean canAccept(boolean force) + { + return false; + } + + @Override + public String toString() + { + return BUFFERING_DISABLED_INFO_MESSAGE; + } + } + + private final BufferingMode bufferingMode; + + LookAheadBufferedReader(Reader in, final int windowSize, boolean isDebugEnabled, boolean isTraceEnabled) + { + super(in); + if (isTraceEnabled) + { + bufferingMode = new WholeValue(); + } + else if(isDebugEnabled) + { + bufferingMode = new Windowing(windowSize); + } + else { + bufferingMode = new NoOp(); + } + } + + public LookAheadBufferedReader(Reader in, final int windowSize, Logger logger) + { + this(in, windowSize, logger.isDebugEnabled(), logger.isTraceEnabled()); + } + + public LookAheadBufferedReader(Reader in, Logger logger) + { + this(in, 250, logger); + } + + @Override + public int read() throws IOException + { + int ch = super.read(); + + if (ch != -1) bufferingMode.append((char)ch); + + return ch; + } + + public String lookAheadAndGetBufferedContent() + { + try + { + int ch; + while ((ch = super.read()) != -1 && bufferingMode.canAccept(true)) + { + bufferingMode.forceAppend((char) ch); + } + } + catch (Exception ignore) + { + // Ignore any I/O exception causing further reading on the underlying stream + // Just return the collected data + } + return bufferingMode.toString(); + } + + boolean isInWindowingMode() + { + return bufferingMode instanceof Windowing; + } + + boolean isInCollectEverythingMode() + { + return bufferingMode instanceof WholeValue; + } + + boolean isBufferingDisabled() + { + return bufferingMode instanceof NoOp; + } +} \ No newline at end of file diff --git a/search-services/alfresco-solrclient-lib/src/main/java/org/alfresco/solr/client/SOLRAPIClient.java b/search-services/alfresco-solrclient-lib/src/main/java/org/alfresco/solr/client/SOLRAPIClient.java index a4cbe6b4a..628114700 100644 --- a/search-services/alfresco-solrclient-lib/src/main/java/org/alfresco/solr/client/SOLRAPIClient.java +++ b/search-services/alfresco-solrclient-lib/src/main/java/org/alfresco/solr/client/SOLRAPIClient.java @@ -25,11 +25,13 @@ */ package org.alfresco.solr.client; -import java.io.BufferedReader; +import static java.util.Optional.ofNullable; + +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.Reader; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -46,6 +48,7 @@ import org.alfresco.httpclient.AlfrescoHttpClient; import org.alfresco.httpclient.AuthenticationException; import org.alfresco.httpclient.GetRequest; import org.alfresco.httpclient.PostRequest; +import org.alfresco.httpclient.Request; import org.alfresco.httpclient.Response; import org.alfresco.repo.dictionary.M2Model; import org.alfresco.repo.dictionary.NamespaceDAO; @@ -94,7 +97,7 @@ import com.fasterxml.jackson.core.JsonToken; */ public class SOLRAPIClient { - protected final static Logger log = LoggerFactory.getLogger(SOLRAPIClient.class); + protected final static Logger LOGGER = LoggerFactory.getLogger(SOLRAPIClient.class); private static final String GET_ACL_CHANGESETS_URL = "api/solr/aclchangesets"; private static final String GET_ACLS = "api/solr/acls"; private static final String GET_ACLS_READERS = "api/solr/aclsReaders"; @@ -142,7 +145,7 @@ public class SOLRAPIClient this.jsonFactory = new JsonFactory(); this.compression = compression; } - + /** * Get the ACL ChangeSets * @@ -179,32 +182,7 @@ public class SOLRAPIClient url.append(args); GetRequest req = new GetRequest(url.toString()); - Response response = null; - JSONObject json = null; - try - { - response = repositoryHttpClient.sendRequest(req); - - if (response.getStatus() != HttpStatus.SC_OK) - { - throw new AlfrescoRuntimeException(GET_ACL_CHANGESETS_URL + " return status:" + response.getStatus()); - } - - Reader reader = new BufferedReader(new InputStreamReader(response.getContentAsStream(), "UTF-8")); - json = new JSONObject(new JSONTokener(reader)); - } - finally - { - if(response != null) - { - response.release(); - } - } - - if (log.isDebugEnabled()) - { - log.debug(json.toString(3)); - } + JSONObject json = callRepository(GET_ACL_CHANGESETS_URL, req); JSONArray aclChangeSetsJSON = json.getJSONArray("aclChangeSets"); List aclChangeSets = new ArrayList(aclChangeSetsJSON.length()); @@ -217,8 +195,7 @@ public class SOLRAPIClient AclChangeSet aclChangeSet = new AclChangeSet(aclChangeSetId, commitTimeMs, aclCount); aclChangeSets.add(aclChangeSet); } - - + Long maxChangeSetCommitTime = null; if(json.has("maxChangeSetCommitTime")) { @@ -273,32 +250,7 @@ public class SOLRAPIClient jsonReq.put("aclChangeSetIds", aclChangeSetIdsJSON); PostRequest req = new PostRequest(url.toString(), jsonReq.toString(), "application/json"); - Response response = null; - JSONObject json = null; - try - { - response = repositoryHttpClient.sendRequest(req); - - if (response.getStatus() != HttpStatus.SC_OK) - { - throw new AlfrescoRuntimeException(GET_ACL_CHANGESETS_URL + " return status:" + response.getStatus()); - } - - Reader reader = new BufferedReader(new InputStreamReader(response.getContentAsStream(), "UTF-8")); - json = new JSONObject(new JSONTokener(reader)); - } - finally - { - if(response != null) - { - response.release(); - } - } - - if (log.isDebugEnabled()) - { - log.debug(json.toString(3)); - } + JSONObject json = callRepository(GET_ACL_CHANGESETS_URL, req); JSONArray aclsJSON = json.getJSONArray("acls"); List acls = new ArrayList(aclsJSON.length()); @@ -336,32 +288,7 @@ public class SOLRAPIClient jsonReq.put("aclIds", aclIdsJSON); PostRequest req = new PostRequest(url.toString(), jsonReq.toString(), "application/json"); - Response response = null; - JSONObject json = null; - try - { - response = repositoryHttpClient.sendRequest(req); - - if (response.getStatus() != HttpStatus.SC_OK) - { - throw new AlfrescoRuntimeException(GET_ACLS_READERS + " return status:" + response.getStatus()); - } - - Reader reader = new BufferedReader(new InputStreamReader(response.getContentAsStream(), "UTF-8")); - json = new JSONObject(new JSONTokener(reader)); - } - finally - { - if(response != null) - { - response.release(); - } - } - - if (log.isDebugEnabled()) - { - log.debug(json.toString(3)); - } + JSONObject json = callRepository(GET_ACLS_READERS, req); JSONArray aclsReadersJSON = json.getJSONArray("aclsReaders"); List aclsReaders = new ArrayList(aclsReadersJSON.length()); @@ -421,7 +348,6 @@ public class SOLRAPIClient public Transactions getTransactions(Long fromCommitTime, Long minTxnId, Long toCommitTime, Long maxTxnId, int maxResults, ShardState shardState) throws AuthenticationException, IOException, JSONException, EncoderException { - log.debug("### get transactions ###"); URLCodec encoder = new URLCodec(); StringBuilder url = new StringBuilder(GET_TRANSACTIONS_URL); @@ -448,7 +374,7 @@ public class SOLRAPIClient } if(shardState != null) { - log.debug("### Shard state exists ###"); + LOGGER.debug("### Shard state exists ###"); args.append(args.length() == 0 ? "?" : "&"); args.append(encoder.encode("baseUrl")).append("=").append(encoder.encode(shardState.getShardInstance().getBaseUrl())); args.append("&").append(encoder.encode("hostName")).append("=").append(encoder.encode(shardState.getShardInstance().getHostName())); @@ -497,12 +423,14 @@ public class SOLRAPIClient } url.append(args); - log.debug("### GetRequest: " + url.toString()); + LOGGER.debug("### GetRequest: " + url.toString()); GetRequest req = new GetRequest(url.toString()); Response response = null; List transactions = new ArrayList(); Long maxTxnCommitTime = null; Long maxTxnIdOnServer = null; + + LookAheadBufferedReader reader = null; try { response = repositoryHttpClient.sendRequest(req); @@ -510,7 +438,8 @@ public class SOLRAPIClient { throw new AlfrescoRuntimeException("GetTransactions return status is " + response.getStatus()); } - Reader reader = new BufferedReader(new InputStreamReader(response.getContentAsStream(), "UTF-8")); + + reader = new LookAheadBufferedReader(new InputStreamReader(response.getContentAsStream(), StandardCharsets.UTF_8), LOGGER); JsonParser parser = jsonFactory.createParser(reader); JsonToken token = parser.nextValue(); @@ -556,18 +485,25 @@ public class SOLRAPIClient token = parser.nextValue(); } parser.close(); - reader.close(); - + } + catch (JSONException exception) + { + String message = "Received a malformed JSON payload. Request was \"" + + req.getFullUri() + + "Data: " + + ofNullable(reader) + .map(LookAheadBufferedReader::lookAheadAndGetBufferedContent) + .orElse("Not available"); + LOGGER.error(message); + throw exception; } finally { - log.debug("## end getTransactions"); - if(response != null) - { - response.release(); - } + ofNullable(response).ifPresent(Response::release); + ofNullable(reader).ifPresent(this::silentlyClose); } - log.debug("### Transactions found maxTxnCommitTime: " + maxTxnCommitTime ); + + LOGGER.debug("### Transactions found maxTxnCommitTime: " + maxTxnCommitTime ); return new Transactions(transactions, maxTxnCommitTime, maxTxnIdOnServer); } @@ -637,35 +573,10 @@ public class SOLRAPIClient PostRequest req = new PostRequest(url.toString(), body.toString(), "application/json"); - - Response response = null; - JSONObject json = null; - try - { - response = repositoryHttpClient.sendRequest(req); - if(response.getStatus() != HttpStatus.SC_OK) - { - throw new AlfrescoRuntimeException("GetNodes return status is " + response.getStatus()); - } - - Reader reader = new BufferedReader(new InputStreamReader(response.getContentAsStream(), "UTF-8")); - json = new JSONObject(new JSONTokener(reader)); - } - finally - { - if(response != null) - { - response.release(); - } - } - - if(log.isDebugEnabled()) - { - log.debug(json.toString()); - } + JSONObject json = callRepository(GET_NODES_URL, req); JSONArray jsonNodes = json.getJSONArray("nodes"); - List nodes = new ArrayList(jsonNodes.length()); + List nodes = new ArrayList<>(jsonNodes.length()); for(int i = 0; i < jsonNodes.length(); i++) { JSONObject jsonNodeInfo = jsonNodes.getJSONObject(i); @@ -893,34 +804,9 @@ public class SOLRAPIClient body.put("maxResults", maxResults); PostRequest req = new PostRequest(url.toString(), body.toString(), "application/json"); - Response response = null; - JSONObject json = null; - try - { - response = repositoryHttpClient.sendRequest(req); - if(response.getStatus() != HttpStatus.SC_OK) - { - throw new AlfrescoRuntimeException("GetNodeMetaData return status is " + response.getStatus()); - } - - Reader reader = new BufferedReader(new InputStreamReader(response.getContentAsStream(), "UTF-8")); - json = new JSONObject(new JSONTokener(reader)); - } - finally - { - if(response != null) - { - response.release(); - } - } + JSONObject json = callRepository(GET_METADATA_URL, req); - if (log.isDebugEnabled()) - { - log.debug(json.toString(3)); - } - JSONArray jsonNodes = json.getJSONArray("nodes"); - List nodes = new ArrayList(jsonNodes.length()); for(int i = 0; i < jsonNodes.length(); i++) { @@ -1159,7 +1045,7 @@ public class SOLRAPIClient return new GetTextContentResponse(response); } - public AlfrescoModel getModel(String coreName, QName modelName) throws AuthenticationException, IOException, JSONException + public AlfrescoModel getModel(String coreName, QName modelName) throws AuthenticationException, IOException { // If the model is new to the SOLR side the prefix will be unknown so we can not generate prefixes for the request! // Always use the full QName with explicit URI @@ -1209,31 +1095,8 @@ public class SOLRAPIClient body.put("models", jsonModels); PostRequest req = new PostRequest(url.toString(), body.toString(), "application/json"); - Response response = null; - JSONObject json = null; - try - { - response = repositoryHttpClient.sendRequest(req); - if(response.getStatus() != HttpStatus.SC_OK) - { - throw new AlfrescoRuntimeException(coreName + " GetModelsDiff return status is " + response.getStatus()); - } - - Reader reader = new BufferedReader(new InputStreamReader(response.getContentAsStream(), "UTF-8")); - json = new JSONObject(new JSONTokener(reader)); - } - finally - { - if(response != null) - { - response.release(); - } - } - - if(log.isDebugEnabled()) - { - log.debug(json.toString()); - } + JSONObject json = callRepository(GET_MODELS_DIFF, req); + JSONArray jsonDiffs = json.getJSONArray("diffs"); if(jsonDiffs == null) { @@ -1271,6 +1134,7 @@ public class SOLRAPIClient GetRequest get = new GetRequest(url.toString()); Response response = null; JSONObject json = null; + LookAheadBufferedReader reader = null; try { response = repositoryHttpClient.sendRequest(get); @@ -1280,19 +1144,29 @@ public class SOLRAPIClient + response.getStatus() + " when invoking " + url); } - Reader reader = new BufferedReader(new InputStreamReader(response.getContentAsStream(), "UTF-8")); - json = new JSONObject(new JSONTokener(reader)); + reader = new LookAheadBufferedReader(new InputStreamReader(response.getContentAsStream(), StandardCharsets.UTF_8), LOGGER); + json = new JSONObject(new JSONTokener(reader)); + } + catch (JSONException exception) + { + String message = "Received a malformed JSON payload. Request was \"" + + get.getFullUri() + + "Data: " + + ofNullable(reader) + .map(LookAheadBufferedReader::lookAheadAndGetBufferedContent) + .orElse("Not available"); + LOGGER.error(message); + throw exception; } finally { - if (response != null) - { - response.release(); - } + ofNullable(response).ifPresent(Response::release); + ofNullable(reader).ifPresent(this::silentlyClose); } - if (log.isDebugEnabled()) + + if (LOGGER.isDebugEnabled()) { - log.debug(json.toString()); + LOGGER.debug(json.toString()); } return Long.parseLong(json.get("nextTransactionCommitTimeMs").toString()); @@ -1318,6 +1192,7 @@ public class SOLRAPIClient GetRequest get = new GetRequest(url.toString()); Response response = null; JSONObject json = null; + LookAheadBufferedReader reader = null; try { response = repositoryHttpClient.sendRequest(get); @@ -1327,19 +1202,29 @@ public class SOLRAPIClient + response.getStatus() + " when invoking " + url); } - Reader reader = new BufferedReader(new InputStreamReader(response.getContentAsStream(), "UTF-8")); + reader = new LookAheadBufferedReader(new InputStreamReader(response.getContentAsStream(), StandardCharsets.UTF_8), LOGGER); json = new JSONObject(new JSONTokener(reader)); } + catch(JSONException exception) + { + String message = "Received a malformed JSON payload. Request was \"" + + get.getFullUri() + + "Data: " + + ofNullable(reader) + .map(LookAheadBufferedReader::lookAheadAndGetBufferedContent) + .orElse("Not available"); + LOGGER.error(message); + throw exception; + } finally { - if (response != null) - { - response.release(); - } + ofNullable(response).ifPresent(Response::release); + ofNullable(reader).ifPresent(this::silentlyClose); } - if (log.isDebugEnabled()) + + if (LOGGER.isDebugEnabled()) { - log.debug(json.toString()); + LOGGER.debug(json.toString()); } return new Pair(Long.parseLong(json.get("minTransactionCommitTimeMs").toString()), @@ -1692,4 +1577,56 @@ public class SOLRAPIClient { repositoryHttpClient.close(); } + + private JSONObject callRepository(String msgId, Request req) throws IOException, AuthenticationException + { + Response response = null; + LookAheadBufferedReader reader = null; + JSONObject json; + try + { + response = repositoryHttpClient.sendRequest(req); + if (response.getStatus() != HttpStatus.SC_OK) + { + throw new AlfrescoRuntimeException(msgId + " return status:" + response.getStatus()); + } + + reader = new LookAheadBufferedReader(new InputStreamReader(response.getContentAsStream(), StandardCharsets.UTF_8), LOGGER); + json = new JSONObject(new JSONTokener(reader)); + + if (LOGGER.isDebugEnabled()) + { + LOGGER.debug(json.toString(3)); + } + return json; + } + catch (JSONException exception) + { + String message = "Received a malformed JSON payload. Request was \"" + + req.getFullUri() + + "Data: " + + ofNullable(reader) + .map(LookAheadBufferedReader::lookAheadAndGetBufferedContent) + .orElse("Not available"); + LOGGER.error(message); + throw exception; + } + finally + { + ofNullable(response).ifPresent(Response::release); + ofNullable(reader).ifPresent(this::silentlyClose); + } + } + + private void silentlyClose(Closeable closeable) + { + try + { + closeable.close(); + } + catch (Exception ignore) + { + // Nothing to be done here + } + } } diff --git a/search-services/alfresco-solrclient-lib/src/test/java/org/alfresco/solr/client/LookAheadBufferedReaderTest.java b/search-services/alfresco-solrclient-lib/src/test/java/org/alfresco/solr/client/LookAheadBufferedReaderTest.java new file mode 100644 index 000000000..f51142c43 --- /dev/null +++ b/search-services/alfresco-solrclient-lib/src/test/java/org/alfresco/solr/client/LookAheadBufferedReaderTest.java @@ -0,0 +1,210 @@ +/*- + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.solr.client; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.Reader; +import java.io.StringReader; + +import static java.util.stream.IntStream.range; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(MockitoJUnitRunner.class) +public class LookAheadBufferedReaderTest +{ + @Mock + Reader reader; + + private final String data = "1234567890ABCDEFGHILMNOPQRSTUVYXZ"; + + @Test + public void windowingModeEnabled() + { + LookAheadBufferedReader classUnderTest = new LookAheadBufferedReader(reader, data.length(), true, false); + assertTrue(classUnderTest.isInWindowingMode()); + assertFalse(classUnderTest.isInCollectEverythingMode()); + assertFalse(classUnderTest.isBufferingDisabled()); + } + + @Test + public void collectEverythingModeEnabled() + { + LookAheadBufferedReader classUnderTest = new LookAheadBufferedReader(reader, data.length(), false, true); + assertTrue(classUnderTest.isInCollectEverythingMode()); + assertFalse(classUnderTest.isInWindowingMode()); + assertFalse(classUnderTest.isBufferingDisabled()); + } + + @Test + public void bufferingDisabled() + { + LookAheadBufferedReader classUnderTest = new LookAheadBufferedReader(reader, data.length(), false, false); + assertTrue(classUnderTest.isBufferingDisabled()); + assertFalse(classUnderTest.isInCollectEverythingMode()); + assertFalse(classUnderTest.isInWindowingMode()); + } + + @Test + public void collectEverythingWinsOverWindowing() + { + LookAheadBufferedReader classUnderTest = new LookAheadBufferedReader(reader, data.length(), true, true); + assertTrue(classUnderTest.isInCollectEverythingMode()); + assertFalse(classUnderTest.isInWindowingMode()); + assertFalse(classUnderTest.isBufferingDisabled()); + } + + @Test + public void windowingModeShouldCollectPartialWindowsOfData() + { + int windowSize = 10; + Reader reader = new StringReader(data); + LookAheadBufferedReader classUnderTest = new LookAheadBufferedReader(reader, windowSize, true, false); + + // Read only 12 chars from the underlying stream + range(0, 12).forEach(index -> consume(classUnderTest)); + + String collectedWindow = classUnderTest.lookAheadAndGetBufferedContent(); + + assertEquals(windowSize * 2, collectedWindow.length()); + assertEquals("4567890ABCDEFGHILMNO", collectedWindow); + } + + @Test + public void notEnoughCharsForTheWindow() + { + int windowSize = 10; + Reader reader = new StringReader(data); + LookAheadBufferedReader classUnderTest = new LookAheadBufferedReader(reader, windowSize, true, false); + + range(0, data.length() - 3).forEach(index -> consume(classUnderTest)); + + String collectedWindow = classUnderTest.lookAheadAndGetBufferedContent(); + + assertEquals(windowSize + 2, collectedWindow.length()); + assertEquals("NOPQRSTUVYXZ", collectedWindow); + } + + @Test + public void emptyDataShouldCollectEmptyWindow() + { + Reader reader = new StringReader(""); + LookAheadBufferedReader classUnderTest = new LookAheadBufferedReader(reader, 10, true, false); + + range(0, data.length() - 3).forEach(index -> consume(classUnderTest)); + + String collectedWindow = classUnderTest.lookAheadAndGetBufferedContent(); + + assertEquals(0, collectedWindow.length()); + assertEquals("", collectedWindow); + } + + @Test + public void collectEverythingModeShouldCollectTheWholeStream() + { + Reader reader = new StringReader(data); + LookAheadBufferedReader classUnderTest = + new LookAheadBufferedReader( + reader, + 10, // this has no effect + false, + true); + + // Read only 12 chars from the underlying stream + range(0, 12).forEach(index -> consume(classUnderTest)); + + String collectedData = classUnderTest.lookAheadAndGetBufferedContent(); + + assertEquals(data.length(), collectedData.length()); + assertEquals(data, collectedData); + } + + @Test + public void emptyDataShouldCollectEmptyStringInCollectEverythingMode() + { + Reader reader = new StringReader(""); + LookAheadBufferedReader classUnderTest = + new LookAheadBufferedReader( + reader, + 10, // this has no effect + false, + true); + + range(0, data.length() - 3).forEach(index -> consume(classUnderTest)); + + String collectedData = classUnderTest.lookAheadAndGetBufferedContent(); + + assertEquals(0, collectedData.length()); + assertEquals("", collectedData); + } + + @Test + public void bufferingModeDisableShouldCollectNoData() + { + int windowSize = 10; + Reader reader = new StringReader(data); + LookAheadBufferedReader classUnderTest = new LookAheadBufferedReader(reader, windowSize, false, false); + + // Read only 12 chars from the underlying stream + range(0, 12).forEach(index -> consume(classUnderTest)); + assertEquals(LookAheadBufferedReader.BUFFERING_DISABLED_INFO_MESSAGE, classUnderTest.lookAheadAndGetBufferedContent()); + } + + @Test + public void emptyDataShouldCollectEmptyDataWhenBufferingIsDisabled() + { + Reader reader = new StringReader(""); + LookAheadBufferedReader classUnderTest = + new LookAheadBufferedReader( + reader, + 10, // this has no effect + false, + false); + + range(0, data.length() - 3).forEach(index -> consume(classUnderTest)); + + String collectedData = classUnderTest.lookAheadAndGetBufferedContent(); + + assertEquals(LookAheadBufferedReader.BUFFERING_DISABLED_INFO_MESSAGE, collectedData); + } + + private void consume(Reader reader) + { + try + { + reader.read(); + } + catch (Exception exception) + { + throw new RuntimeException(exception); + } + } +} \ No newline at end of file