Skip to content

Commit 478049c

Browse files
committed
First steps in porting 4.x sax ooxml -> 3.x - checkpoint
1 parent 47a0c7f commit 478049c

5 files changed

Lines changed: 378 additions & 14 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.tika.parser.microsoft.ooxml;
18+
19+
import java.io.ByteArrayOutputStream;
20+
import java.nio.charset.StandardCharsets;
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
import java.util.Set;
24+
25+
import org.xml.sax.Attributes;
26+
import org.xml.sax.SAXException;
27+
import org.xml.sax.helpers.DefaultHandler;
28+
29+
/**
30+
* Generic SAX handler that collects raw XML content by ID from OOXML part files.
31+
* Works with any part that contains wrapper elements with {@code w:id} attributes
32+
* containing body content (paragraphs, tables, formatting, etc.).
33+
* <p>
34+
* Used for:
35+
* <ul>
36+
* <li>footnotes.xml — wrapper element "footnote"</li>
37+
* <li>endnotes.xml — wrapper element "endnote"</li>
38+
* <li>comments.xml — wrapper element "comment"</li>
39+
* </ul>
40+
* <p>
41+
* IDs "0" and "-1" are skipped (these are separator/continuation elements in
42+
* footnotes/endnotes).
43+
*/
44+
class OOXMLPartContentCollector extends DefaultHandler {
45+
46+
private static final String W_NS =
47+
"http://schemas.openxmlformats.org/wordprocessingml/2006/main";
48+
49+
private final Set<String> wrapperElementNames;
50+
private final Set<String> skipIds;
51+
private final Map<String, byte[]> contentMap = new HashMap<>();
52+
private final Map<String, String> namespaceMappings = new HashMap<>();
53+
54+
private String currentId = null;
55+
private ByteArrayOutputStream buffer = null;
56+
private int depth = 0;
57+
58+
/**
59+
* @param wrapperElementNames local names of wrapper elements to collect
60+
* (e.g., "footnote", "endnote", "comment")
61+
*/
62+
OOXMLPartContentCollector(Set<String> wrapperElementNames) {
63+
this(wrapperElementNames, Set.of("0", "-1"));
64+
}
65+
66+
/**
67+
* @param wrapperElementNames local names of wrapper elements to collect
68+
* @param skipIds IDs to skip (e.g., "0", "-1" for footnote
69+
* separator/continuation elements)
70+
*/
71+
OOXMLPartContentCollector(Set<String> wrapperElementNames, Set<String> skipIds) {
72+
this.wrapperElementNames = wrapperElementNames;
73+
this.skipIds = skipIds;
74+
}
75+
76+
@Override
77+
public void startPrefixMapping(String prefix, String uri) {
78+
namespaceMappings.put(prefix, uri);
79+
}
80+
81+
Map<String, byte[]> getContentMap() {
82+
return contentMap;
83+
}
84+
85+
@Override
86+
public void startElement(String uri, String localName, String qName,
87+
Attributes atts) throws SAXException {
88+
if (currentId != null) {
89+
depth++;
90+
appendStartTag(localName, qName, atts);
91+
return;
92+
}
93+
94+
if (wrapperElementNames.contains(localName)) {
95+
String id = atts.getValue(W_NS, "id");
96+
if (id != null && !skipIds.contains(id)) {
97+
currentId = id;
98+
buffer = new ByteArrayOutputStream();
99+
// Don't write wrapper open tag yet — inline xmlns declarations
100+
// (e.g., xmlns:a on nested elements) haven't been captured via
101+
// startPrefixMapping. Defer to endElement when all are known.
102+
depth = 0;
103+
}
104+
}
105+
}
106+
107+
@Override
108+
public void endElement(String uri, String localName, String qName)
109+
throws SAXException {
110+
if (currentId == null) {
111+
return;
112+
}
113+
114+
if (depth == 0) {
115+
// Build the wrapper now — all startPrefixMapping calls from nested
116+
// elements have been captured, so inline xmlns declarations are included.
117+
byte[] wrapperOpen = buildWrapperOpenTag().getBytes(StandardCharsets.UTF_8);
118+
byte[] content = buffer.toByteArray();
119+
ByteArrayOutputStream combined =
120+
new ByteArrayOutputStream(wrapperOpen.length + content.length + 16);
121+
combined.write(wrapperOpen, 0, wrapperOpen.length);
122+
combined.write(content, 0, content.length);
123+
writeString(combined, "</w:body>");
124+
contentMap.put(currentId, combined.toByteArray());
125+
currentId = null;
126+
buffer = null;
127+
return;
128+
}
129+
130+
depth--;
131+
if (qName != null && !qName.isEmpty()) {
132+
writeString("</" + qName + ">");
133+
} else {
134+
writeString("</" + localName + ">");
135+
}
136+
}
137+
138+
@Override
139+
public void characters(char[] ch, int start, int length) throws SAXException {
140+
if (currentId != null) {
141+
writeString(escape(new String(ch, start, length)));
142+
}
143+
}
144+
145+
private String buildWrapperOpenTag() {
146+
StringBuilder sb = new StringBuilder("<w:body");
147+
// include all namespace declarations from the source document
148+
for (Map.Entry<String, String> entry : namespaceMappings.entrySet()) {
149+
String prefix = entry.getKey();
150+
String nsUri = entry.getValue();
151+
if (prefix == null || prefix.isEmpty()) {
152+
sb.append(" xmlns=\"").append(escape(nsUri)).append("\"");
153+
} else {
154+
sb.append(" xmlns:").append(prefix).append("=\"")
155+
.append(escape(nsUri)).append("\"");
156+
}
157+
}
158+
// ensure w namespace is present
159+
if (!namespaceMappings.containsKey("w")) {
160+
sb.append(" xmlns:w=\"").append(W_NS).append("\"");
161+
}
162+
sb.append(">");
163+
return sb.toString();
164+
}
165+
166+
private void appendStartTag(String localName, String qName, Attributes atts) {
167+
String tagName = (qName != null && !qName.isEmpty()) ? qName : localName;
168+
StringBuilder sb = new StringBuilder();
169+
sb.append('<').append(tagName);
170+
for (int i = 0; i < atts.getLength(); i++) {
171+
String attName = atts.getQName(i);
172+
if (attName == null || attName.isEmpty()) {
173+
attName = atts.getLocalName(i);
174+
}
175+
sb.append(' ').append(attName).append("=\"");
176+
sb.append(escape(atts.getValue(i)));
177+
sb.append('"');
178+
}
179+
sb.append('>');
180+
writeString(sb.toString());
181+
}
182+
183+
private void writeString(String s) {
184+
writeString(buffer, s);
185+
}
186+
187+
private static void writeString(ByteArrayOutputStream target, String s) {
188+
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
189+
target.write(bytes, 0, bytes.length);
190+
}
191+
192+
static String escape(String s) {
193+
if (s == null) {
194+
return "";
195+
}
196+
StringBuilder sb = null;
197+
for (int i = 0; i < s.length(); i++) {
198+
char c = s.charAt(i);
199+
String replacement = null;
200+
switch (c) {
201+
case '&':
202+
replacement = "&amp;";
203+
break;
204+
case '<':
205+
replacement = "&lt;";
206+
break;
207+
case '>':
208+
replacement = "&gt;";
209+
break;
210+
case '"':
211+
replacement = "&quot;";
212+
break;
213+
default:
214+
if (sb != null) {
215+
sb.append(c);
216+
}
217+
continue;
218+
}
219+
if (sb == null) {
220+
sb = new StringBuilder(s.length() + 16);
221+
sb.append(s, 0, i);
222+
}
223+
sb.append(replacement);
224+
}
225+
return sb != null ? sb.toString() : s;
226+
}
227+
}

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/SXSLFPowerPointExtractorDecorator.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141

4242
import org.apache.tika.exception.TikaException;
4343
import org.apache.tika.metadata.Metadata;
44+
import org.apache.tika.metadata.Office;
4445
import org.apache.tika.metadata.TikaCoreProperties;
4546
import org.apache.tika.parser.ParseContext;
4647
import org.apache.tika.parser.microsoft.ooxml.xslf.XSLFEventBasedPowerPointExtractor;
@@ -177,11 +178,17 @@ private void handleSlidePart(PackagePart slidePart, XHTMLContentHandler xhtml)
177178

178179
// Map<String, String> hyperlinks = loadHyperlinkRelationships(packagePart);
179180
xhtml.startElement("div", "class", "slide-content");
181+
//pass metadata so the body handler can emit HAS_* signals for the slide, and keep a
182+
//reference so we can flag hidden slides after the parse
183+
OOXMLWordAndPowerPointTextHandler wordAndPPTHandler =
184+
new OOXMLWordAndPowerPointTextHandler(
185+
new OOXMLTikaBodyPartHandler(xhtml, metadata), linkedRelationships);
180186
try (InputStream stream = slidePart.getInputStream()) {
181187
XMLReaderUtils.parseSAX(CloseShieldInputStream.wrap(stream),
182-
new EmbeddedContentHandler(new OOXMLWordAndPowerPointTextHandler(
183-
new OOXMLTikaBodyPartHandler(xhtml), linkedRelationships)), context);
184-
188+
new EmbeddedContentHandler(wordAndPPTHandler), context);
189+
if (wordAndPPTHandler.isHiddenSlide()) {
190+
metadata.set(Office.HAS_HIDDEN_SLIDES, true);
191+
}
185192
} catch (TikaException | IOException e) {
186193
metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
187194
ExceptionUtils.getStackTrace(e));

0 commit comments

Comments
 (0)