rzo1 opened a new issue, #2108:
URL: https://github.com/apache/stormcrawler/issues/2108
## What happens
`ParseResult.get(url)` is documented as a convenience accessor, but on a
miss it creates a `ParseData` and stores it in the map before returning it.
`put(url, key, value)` and `set(url, metadata)` go through the same path. Both
parser bolts iterate the whole map and emit one document tuple per entry, so an
entry created by a lookup is emitted like a parsed document. The auto-created
entry also has a null content field, while the `(text, metadata)` constructor
sets an empty byte array, so the two paths do not agree on what an empty
document looks like.
## Where
`core/src/main/java/org/apache/stormcrawler/parse/ParseResult.java:72-79`:
```java
ParseData parse = parseMap.get(url);
if (parse == null) {
parse = new ParseData();
parseMap.put(url, parse);
```
Emitted at
`core/src/main/java/org/apache/stormcrawler/bolt/JSoupParserBolt.java:498` and
`external/tika/src/main/java/org/apache/stormcrawler/tika/ParserBolt.java:301`.
Constructors that disagree on the content default: `ParseData.java:27` and
`:37` against `:31`.
## Why it matters
Nothing in the tree hits this today: every non-test caller in core and in
the external modules passes the URL of the page being parsed, so the map only
ever gains entries for documents that exist. The problem is the API. A parse
filter written against the Javadoc may reasonably call `get()` to check whether
an earlier filter produced a sub-document, and if the URL it probes comes from
page content, the probe creates an entry that the bolt emits as a document.
Sub-documents are not filtered by `URLFilters`, only outlinks are, so that
entry reaches the indexer as a document URL with no content. This is API
hygiene rather than something operators need to act on.
## Reproduction
Save as
`core/src/test/java/org/apache/stormcrawler/parse/ParseResultLookupTest.java`.
```java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.stormcrawler.parse;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/**
* Documents the current behaviour of ParseResult.get(): it inserts an entry
for a URL that was
* never parsed, and that entry has a null content field. A read-only lookup
should leave the map
* untouched, and an entry that reaches the parser bolt should not carry
null content.
*/
class ParseResultLookupTest {
@Test
void getInsertsAnEntryForAnUnknownUrl() {
ParseResult parse = new ParseResult();
Assertions.assertEquals(0, parse.size());
ParseData data = parse.get("https://example.com/never-parsed");
// current behaviour: the lookup created and stored an entry
Assertions.assertEquals(1, parse.size());
Assertions.assertTrue(parse.getParseMap().containsKey("https://example.com/never-parsed"));
// should be: 0 and false
// current behaviour: the auto-created entry has null content
Assertions.assertNull(data.getContent());
Assertions.assertNull(data.getText());
// should be: an empty byte array, as set by the (text, metadata)
constructor
}
@Test
void metadataOnlyConstructorLeavesContentNull() {
ParseData withText = new ParseData("text", new
org.apache.stormcrawler.Metadata());
Assertions.assertNotNull(withText.getContent());
ParseData metadataOnly = new ParseData(new
org.apache.stormcrawler.Metadata());
// current behaviour: inconsistent with the constructor above
Assertions.assertNull(metadataOnly.getContent());
}
}
```
Run it:
```
mvn -pl core test -Dtest=ParseResultLookupTest
```
It asserts the present behaviour and passes, with comments saying what the
behaviour should be; a failing test would prejudge whether `get()` keeps its
name and its semantics.
```
[INFO] Running org.apache.stormcrawler.parse.ParseResultLookupTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.035
s
```
The first case shows `size()` going from 0 to 1 after a lookup of a URL that
was never parsed, and the created entry carrying null content. The second shows
the constructor disagreement.
## Suggested fix
Add a non-mutating lookup to `ParseResult`, for example `getIfPresent(url)`
returning null, and say in the `get()` Javadoc that it creates an entry.
Renaming `get()` to `getOrCreate()` and keeping `get()` as a deprecated
delegate would make the two apart at the call site, at the cost of a source
change for filters outside this repository. As a backstop, have
`JSoupParserBolt` and the Tika `ParserBolt` skip entries with no content, no
text and empty metadata when they emit. Also set `content` to an empty array in
the no-arg and metadata-only `ParseData` constructors so no path emits a null
content field.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]