diff --git a/core/src/main/java/org/apache/stormcrawler/bolt/JSoupParserBolt.java b/core/src/main/java/org/apache/stormcrawler/bolt/JSoupParserBolt.java
index dc8c32c92..c128082a6 100644
--- a/core/src/main/java/org/apache/stormcrawler/bolt/JSoupParserBolt.java
+++ b/core/src/main/java/org/apache/stormcrawler/bolt/JSoupParserBolt.java
@@ -286,11 +286,17 @@ public void execute(Tuple tuple) {
if (fastCharsetDetection) {
charset =
CharsetIdentification.getCharsetFast(
- metadata, content, maxLengthCharsetDetection);
+ metadata,
+ content,
+ maxLengthCharsetDetection,
+ protocolMetadataPrefix);
} else {
charset =
CharsetIdentification.getCharset(
- metadata, content, maxLengthCharsetDetection);
+ metadata,
+ content,
+ maxLengthCharsetDetection,
+ protocolMetadataPrefix);
}
LOG.debug(
diff --git a/core/src/main/java/org/apache/stormcrawler/util/CharsetIdentification.java b/core/src/main/java/org/apache/stormcrawler/util/CharsetIdentification.java
index 9fc778dd3..5954ad463 100644
--- a/core/src/main/java/org/apache/stormcrawler/util/CharsetIdentification.java
+++ b/core/src/main/java/org/apache/stormcrawler/util/CharsetIdentification.java
@@ -57,10 +57,15 @@ public class CharsetIdentification {
* guess and if that fails too returns UTF-8. This approach is expected to be faster but maybe
* less reliable than getCharset.
*
- * @since 1.18
+ * @param protocolMetadataPrefix prefix of the response headers in the metadata, including its
+ * separator (e.g. {@code "protocol."}); empty if they are not prefixed
+ * @since 4.0
*/
public static String getCharsetFast(
- final Metadata metadata, final byte[] content, final int maxLengthCharsetDetection) {
+ final Metadata metadata,
+ final byte[] content,
+ final int maxLengthCharsetDetection,
+ final String protocolMetadataPrefix) {
// let's look at the BOM first
String charset = getCharsetFromBOM(content);
@@ -69,7 +74,7 @@ public static String getCharsetFast(
}
// then look at what we get from HTTP headers
- charset = getCharsetFromHTTP(metadata);
+ charset = getCharsetFromHTTP(metadata, protocolMetadataPrefix);
if (charset != null) {
return charset;
}
@@ -92,11 +97,18 @@ public static String getCharsetFast(
/**
* Identifies the charset of a document based on the following logic: guess from the
* ByteOrderMark - else if the same charset is specified in the http headers and the html
- * metadata then use it - otherwise use ICU's charset detector to make an educated guess and if
- * that fails too returns UTF-8.
+ * metadata then use it - otherwise use ICU's charset detector to make an educated guess, with
+ * the html metadata or else the http headers as a hint, and if that fails too returns UTF-8.
+ *
+ * @param protocolMetadataPrefix prefix of the response headers in the metadata, including its
+ * separator (e.g. {@code "protocol."}); empty if they are not prefixed
+ * @since 4.0
*/
public static String getCharset(
- Metadata metadata, byte[] content, int maxLengthCharsetDetection) {
+ Metadata metadata,
+ byte[] content,
+ int maxLengthCharsetDetection,
+ String protocolMetadataPrefix) {
// let's look at the BOM first
String BOMCharset = getCharsetFromBOM(content);
@@ -105,7 +117,7 @@ public static String getCharset(
}
// then look at what we get from HTTP headers and HTML content
- String httpCharset = getCharsetFromHTTP(metadata);
+ String httpCharset = getCharsetFromHTTP(metadata, protocolMetadataPrefix);
String htmlCharset = getCharsetFromMeta(content, maxLengthCharsetDetection);
// both exist and agree
@@ -116,12 +128,7 @@ public static String getCharset(
}
// let's guess from the text - using a hint or not
- String hintCharset = null;
- if (httpCharset != null && htmlCharset == null) {
- hintCharset = httpCharset;
- } else if (httpCharset == null && htmlCharset != null) {
- hintCharset = htmlCharset;
- }
+ String hintCharset = htmlCharset != null ? htmlCharset : httpCharset;
String textCharset = getCharsetFromText(content, hintCharset, maxLengthCharsetDetection);
if (textCharset != null) {
@@ -133,8 +140,9 @@ public static String getCharset(
}
/** Returns the charset declared by the server if any. */
- private static String getCharsetFromHTTP(Metadata metadata) {
- return getCharsetFromContentType(metadata.getFirstValue(HttpHeaders.CONTENT_TYPE));
+ private static String getCharsetFromHTTP(Metadata metadata, String protocolMetadataPrefix) {
+ return getCharsetFromContentType(
+ metadata.getFirstValue(HttpHeaders.CONTENT_TYPE, protocolMetadataPrefix));
}
/** Detects any BOMs and returns the corresponding charset. */
diff --git a/core/src/test/java/org/apache/stormcrawler/bolt/JSoupParserBoltTest.java b/core/src/test/java/org/apache/stormcrawler/bolt/JSoupParserBoltTest.java
index ba28bc98c..ebd913398 100644
--- a/core/src/test/java/org/apache/stormcrawler/bolt/JSoupParserBoltTest.java
+++ b/core/src/test/java/org/apache/stormcrawler/bolt/JSoupParserBoltTest.java
@@ -22,6 +22,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.apache.http.HttpHeaders;
import org.apache.storm.task.OutputCollector;
import org.apache.stormcrawler.Constants;
import org.apache.stormcrawler.Metadata;
@@ -33,6 +34,8 @@
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
@@ -129,7 +132,10 @@ void charsetDetectionFailureIsReportedAsParseError() throws IOException {
.when(
() ->
CharsetIdentification.getCharset(
- Mockito.any(), Mockito.any(), Mockito.anyInt()))
+ Mockito.any(),
+ Mockito.any(),
+ Mockito.anyInt(),
+ Mockito.any()))
.thenThrow(new StackOverflowError());
parse("https://stormcrawler.apache.org", "stormcrawler.apache.org.html");
}
@@ -365,4 +371,41 @@ void testBaseHrefOutlinkResolution() throws IOException {
"https://www.example.be/fr/rayons/stockage-archivage-de-donnees/218725",
statusTuples.get(0).get(0));
}
+
+ /**
+ * The fetcher stores the response headers under protocol.md.prefix, so the charset declared in
+ * the Content-Type has to be read with that prefix. Without it the header is never seen, the
+ * header and the meta charset can never agree, and ICU guesses a UTF-8 page whose detection
+ * window is plain ASCII as ISO-8859-1, see issue #2250.
+ */
+ @ParameterizedTest(name = "detect.charset.fast={0}")
+ @ValueSource(booleans = {false, true})
+ void prefixedContentTypeCharsetIsUsed(boolean fast) throws IOException {
+ Map " + text + " café