Skip to content

Commit 5e95e7d

Browse files
committed
refactor: extract XlsxStreamingParser and RtfStripper with full Javadoc
1 parent a53f437 commit 5e95e7d

5 files changed

Lines changed: 274 additions & 144 deletions

File tree

src/main/java/fastcontentparse/FastContentParse.java

Lines changed: 43 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,35 @@
11
package fastcontentparse;
22

33
import java.io.IOException;
4-
import java.io.InputStream;
4+
import java.io.Writer;
55
import java.nio.charset.StandardCharsets;
66
import java.nio.file.Files;
77
import java.nio.file.Path;
8-
import java.util.ArrayList;
98
import java.util.List;
109
import java.util.Locale;
1110
import java.util.Map;
12-
import java.util.regex.Pattern;
1311

12+
import fastocr.FastOCR;
13+
import fastregex.FastRegex;
1414
import org.apache.pdfbox.Loader;
1515
import org.apache.pdfbox.pdmodel.PDDocument;
16-
import org.apache.pdfbox.text.PDFTextStripper;
1716

17+
/**
18+
* High-performance, zero-bloat content extraction and normalization engine.
19+
* <p>
20+
* Routes supported document formats (PDF, XLSX, CSV, RTF, Markdown, Plaintext, OCR images)
21+
* to dedicated lightweight streaming parsers and normalizes textual output into clean UTF-8 tokens.
22+
*/
1823
public class FastContentParse {
1924

25+
/**
26+
* Parses a file from the local file system into a {@link ParsedDocument}.
27+
*
28+
* @param path target file path
29+
* @return normalized document representation with detected MIME type
30+
* @throws NullPointerException if {@code path} is {@code null}
31+
* @throws IOException if the file does not exist, cannot be read, or parsing fails
32+
*/
2033
public ParsedDocument parseFile(Path path) throws IOException {
2134
if (path == null) {
2235
throw new NullPointerException("path must not be null");
@@ -52,16 +65,36 @@ public ParsedDocument parseFile(Path path) throws IOException {
5265
return parseString(raw, fileName, type);
5366
}
5467

68+
/**
69+
* Parses an in-memory string inferred by a source file name.
70+
*
71+
* @param rawText raw text content
72+
* @param sourceName original filename used for MIME type detection
73+
* @return normalized document representation
74+
*/
5575
public ParsedDocument parseString(String rawText, String sourceName) {
5676
return parseString(rawText, sourceName, detectType(sourceName));
5777
}
5878

79+
/**
80+
* Parses an in-memory string with explicit MIME type routing and normalization.
81+
*
82+
* @param rawText raw text content
83+
* @param sourceName original filename or identifier
84+
* @param explicitType explicit MIME type or {@code null} to infer from {@code sourceName}
85+
* @return normalized document representation
86+
*/
5987
public ParsedDocument parseString(String rawText, String sourceName, String explicitType) {
6088
String type = explicitType != null ? explicitType : detectType(sourceName);
6189
String normalized = normalize(rawText, type);
6290
return new ParsedDocument(type, normalized);
6391
}
6492

93+
/**
94+
* Deprecated method stub retained for migration compatibility.
95+
*
96+
* @deprecated Use the dedicated FastContentChunk library for token and character chunking.
97+
*/
6598
@Deprecated(forRemoval = true)
6699
public List<String> chunkText(String text, int maxChunkSize, int overlap) {
67100
if (text == null || text.isBlank()) {
@@ -78,7 +111,7 @@ private String normalize(String rawText, String explicitType) {
78111
String text = rawText.replace("\r\n", "\n").replace('\r', '\n');
79112

80113
if (explicitType != null && explicitType.toLowerCase(Locale.ROOT).contains("rtf")) {
81-
text = stripRtf(text);
114+
text = RtfStripper.strip(text);
82115
}
83116

84117
if (explicitType != null && explicitType.toLowerCase(Locale.ROOT).contains("pdf")) {
@@ -88,50 +121,10 @@ private String normalize(String rawText, String explicitType) {
88121
return normalizeWhitespace(text);
89122
}
90123

91-
private String stripRtf(String input) {
92-
if (input == null || input.isEmpty()) {
93-
return "";
94-
}
95-
96-
StringBuilder sb = new StringBuilder(input.length());
97-
int i = 0;
98-
final int len = input.length();
99-
100-
while (i < len) {
101-
char c = input.charAt(i);
102-
103-
if (c == '{' || c == '}') {
104-
i++;
105-
continue;
106-
}
107-
108-
if (c == '\\') {
109-
i++;
110-
if (i >= len) break;
111-
112-
char next = input.charAt(i);
113-
if (Character.isLetter(next)) {
114-
while (i < len && Character.isLetter(input.charAt(i))) i++;
115-
while (i < len && (Character.isDigit(input.charAt(i)) || input.charAt(i) == '-')) i++;
116-
if (i < len && input.charAt(i) == ' ') i++;
117-
} else {
118-
i++;
119-
}
120-
continue;
121-
}
122-
123-
sb.append(c);
124-
i++;
125-
}
126-
127-
return normalizeWhitespace(sb.toString());
128-
}
129-
130124
private String normalizeWhitespace(String text) {
131-
return fastregex.FastRegex.normalizeWhitespace(text);
125+
return FastRegex.normalizeWhitespace(text);
132126
}
133127

134-
private static final javax.xml.stream.XMLInputFactory XML_FACTORY;
135128
private static final Map<String, String> EXTENSION_TYPES = Map.ofEntries(
136129
Map.entry(".pdf", "application/pdf"),
137130
Map.entry(".rtf", "text/rtf"),
@@ -147,12 +140,6 @@ private String normalizeWhitespace(String text) {
147140
Map.entry(".bmp", "image/ocr")
148141
);
149142

150-
static {
151-
XML_FACTORY = javax.xml.stream.XMLInputFactory.newDefaultFactory();
152-
XML_FACTORY.setProperty(javax.xml.stream.XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE);
153-
XML_FACTORY.setProperty(javax.xml.stream.XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
154-
}
155-
156143
private String detectType(String sourceName) {
157144
if (sourceName == null) {
158145
return "text/plain";
@@ -177,102 +164,14 @@ private ParsedDocument parseCsv(Path path) throws IOException {
177164
}
178165

179166
private ParsedDocument parseXlsx(Path path) throws IOException {
180-
List<String> sharedStrings = new ArrayList<>();
181-
StringBuilder textBuilder = new StringBuilder(128 * 1024);
182-
183-
try (java.util.zip.ZipFile zip = new java.util.zip.ZipFile(path.toFile())) {
184-
// 1. Read sharedStrings.xml if present
185-
java.util.zip.ZipEntry sstEntry = zip.getEntry("xl/sharedStrings.xml");
186-
if (sstEntry != null) {
187-
try (InputStream is = zip.getInputStream(sstEntry)) {
188-
javax.xml.stream.XMLStreamReader reader = XML_FACTORY.createXMLStreamReader(is);
189-
190-
StringBuilder currentText = null;
191-
while (reader.hasNext()) {
192-
int event = reader.next();
193-
if (event == javax.xml.stream.XMLStreamConstants.START_ELEMENT) {
194-
if ("t".equals(reader.getLocalName())) {
195-
currentText = new StringBuilder();
196-
}
197-
} else if (event == javax.xml.stream.XMLStreamConstants.CHARACTERS) {
198-
if (currentText != null) {
199-
currentText.append(reader.getText());
200-
}
201-
} else if (event == javax.xml.stream.XMLStreamConstants.END_ELEMENT) {
202-
if ("t".equals(reader.getLocalName())) {
203-
if (currentText != null) {
204-
sharedStrings.add(currentText.toString());
205-
currentText = null;
206-
}
207-
}
208-
}
209-
}
210-
} catch (Exception e) {
211-
throw new IOException("Failed parsing XLSX shared strings: " + e.getMessage(), e);
212-
}
213-
}
214-
215-
// 2. Read all sheets (sheet1.xml, sheet2.xml, etc.)
216-
java.util.Enumeration<? extends java.util.zip.ZipEntry> entries = zip.entries();
217-
while (entries.hasMoreElements()) {
218-
java.util.zip.ZipEntry entry = entries.nextElement();
219-
String entryName = entry.getName();
220-
if (entryName.startsWith("xl/worksheets/sheet") && entryName.endsWith(".xml")) {
221-
try (InputStream is = zip.getInputStream(entry)) {
222-
javax.xml.stream.XMLStreamReader reader = XML_FACTORY.createXMLStreamReader(is);
223-
224-
String cellType = null;
225-
StringBuilder cellVal = null;
226-
227-
while (reader.hasNext()) {
228-
int event = reader.next();
229-
if (event == javax.xml.stream.XMLStreamConstants.START_ELEMENT) {
230-
String name = reader.getLocalName();
231-
if ("c".equals(name)) {
232-
cellType = reader.getAttributeValue(null, "t");
233-
} else if ("v".equals(name)) {
234-
cellVal = new StringBuilder();
235-
}
236-
} else if (event == javax.xml.stream.XMLStreamConstants.CHARACTERS) {
237-
if (cellVal != null) {
238-
cellVal.append(reader.getText());
239-
}
240-
} else if (event == javax.xml.stream.XMLStreamConstants.END_ELEMENT) {
241-
String name = reader.getLocalName();
242-
if ("v".equals(name)) {
243-
if (cellVal != null) {
244-
String rawVal = cellVal.toString().trim();
245-
if ("s".equals(cellType)) {
246-
try {
247-
int idx = Integer.parseInt(rawVal);
248-
if (idx >= 0 && idx < sharedStrings.size()) {
249-
textBuilder.append(sharedStrings.get(idx)).append("\t");
250-
}
251-
} catch (NumberFormatException ignored) {}
252-
} else if (!rawVal.isEmpty()) {
253-
textBuilder.append(rawVal).append("\t");
254-
}
255-
cellVal = null;
256-
}
257-
} else if ("row".equals(name)) {
258-
textBuilder.append("\n");
259-
}
260-
}
261-
}
262-
} catch (Exception e) {
263-
throw new IOException("Failed parsing XLSX worksheet " + entryName + ": " + e.getMessage(), e);
264-
}
265-
}
266-
}
267-
}
268-
269-
String normalized = normalize(textBuilder.toString(), "text/plain");
167+
String raw = XlsxStreamingParser.extractText(path);
168+
String normalized = normalize(raw, "text/plain");
270169
return new ParsedDocument("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", normalized);
271170
}
272171

273172
private ParsedDocument parseImageOcr(Path path) throws IOException {
274173
try {
275-
fastocr.FastOCR ocr = new fastocr.FastOCR("en");
174+
FastOCR ocr = new FastOCR("en");
276175
String text = ocr.read(path.toAbsolutePath().toString());
277176
String normalized = normalize(text, "text/plain");
278177
return new ParsedDocument("image/ocr", normalized);
@@ -284,7 +183,7 @@ private ParsedDocument parseImageOcr(Path path) throws IOException {
284183
private ParsedDocument parsePdf(Path path) throws IOException {
285184
try (PDDocument document = Loader.loadPDF(path.toFile())) {
286185
VisualParagraphPDFTextStripper stripper = new VisualParagraphPDFTextStripper();
287-
stripper.writeText(document, java.io.Writer.nullWriter());
186+
stripper.writeText(document, Writer.nullWriter());
288187
String raw = stripper.buildVisualText();
289188
String normalized = normalize(raw, "application/pdf");
290189
return new ParsedDocument("application/pdf", normalized);

src/main/java/fastcontentparse/ParsedDocument.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,37 @@
11
package fastcontentparse;
22

3+
/**
4+
* Immutable value container representing the result of parsing a document or text buffer.
5+
*/
36
public class ParsedDocument {
47
private final String type;
58
private final String text;
69

10+
/**
11+
* Constructs a parsed document entity.
12+
*
13+
* @param type detected or explicit MIME type
14+
* @param text normalized textual content
15+
*/
716
public ParsedDocument(String type, String text) {
817
this.type = type;
918
this.text = text == null ? "" : text;
1019
}
1120

21+
/**
22+
* Returns the MIME type of the parsed document (e.g., {@code "application/pdf"}).
23+
*
24+
* @return MIME type string
25+
*/
1226
public String getType() {
1327
return type;
1428
}
1529

30+
/**
31+
* Returns the normalized textual content of the parsed document.
32+
*
33+
* @return clean, whitespace-normalized UTF-8 text
34+
*/
1635
public String getText() {
1736
return text;
1837
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package fastcontentparse;
2+
3+
import fastregex.FastRegex;
4+
5+
/**
6+
* Lightweight, single-pass zero-regex RTF lexer and control word stripper.
7+
*/
8+
final class RtfStripper {
9+
10+
private RtfStripper() {
11+
}
12+
13+
/**
14+
* Strips RTF group brackets, control words, and symbols from raw RTF text.
15+
*
16+
* @param input raw RTF text
17+
* @return clean extracted text with normalized whitespace
18+
*/
19+
static String strip(String input) {
20+
if (input == null || input.isEmpty()) {
21+
return "";
22+
}
23+
24+
StringBuilder sb = new StringBuilder(input.length());
25+
int i = 0;
26+
final int len = input.length();
27+
28+
while (i < len) {
29+
char c = input.charAt(i);
30+
31+
if (c == '{' || c == '}') {
32+
i++;
33+
continue;
34+
}
35+
36+
if (c == '\\') {
37+
i++;
38+
if (i >= len) break;
39+
40+
char next = input.charAt(i);
41+
if (Character.isLetter(next)) {
42+
while (i < len && Character.isLetter(input.charAt(i))) i++;
43+
while (i < len && (Character.isDigit(input.charAt(i)) || input.charAt(i) == '-')) i++;
44+
if (i < len && input.charAt(i) == ' ') i++;
45+
} else {
46+
i++;
47+
}
48+
continue;
49+
}
50+
51+
sb.append(c);
52+
i++;
53+
}
54+
55+
return FastRegex.normalizeWhitespace(sb.toString());
56+
}
57+
}

0 commit comments

Comments
 (0)