GeoGebraXMLHandler.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.tika.parser.geogebra;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.fasterxml.jackson.databind.JsonNode;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.Property;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.sax.XHTMLContentHandler;
import org.apache.tika.utils.StringUtils;
/**
* SAX handler for {@code geogebra.xml} and {@code geogebra_macro.xml}.
* <p>
* Extracts the document metadata from the {@code <geogebra>} root and its
* {@code <construction>} child (when asked to), and emits the user-visible
* text as XHTML paragraphs: the string literals of text object
* {@code <expression>}s, the text runs of {@code <content>} elements (inline
* text, tables, mind maps), element {@code <caption>}s and macro names and
* help texts.
*/
class GeoGebraXMLHandler extends DefaultHandler {
/**
* A GeoGebra string literal. GeoGebra writes strings between plain
* double quotes without any escaping, so a literal never contains one.
*/
private static final Pattern STRING_LITERAL = Pattern.compile("\"([^\"]*)\"");
/**
* Longest content JSON that is parsed; a real inline text, table or mind
* map is a few kilobytes, anything far beyond that is not worth a tree.
*/
private static final int MAX_CONTENT_LENGTH = 1024 * 1024;
private final XHTMLContentHandler xhtml;
private final Metadata metadata;
private final boolean documentMetadata;
private int depth = 0;
/**
* The {@code iconFile} of every macro, in document order: the path of
* the tool's icon inside the zip, if the tool has one.
*/
private final List<String> iconFiles = new ArrayList<>();
/**
* @param xhtml the handler paragraphs are written to
* @param metadata the metadata tool names are added to
* @param documentMetadata whether to also fill the document metadata from
* the root and construction elements
*/
GeoGebraXMLHandler(XHTMLContentHandler xhtml, Metadata metadata, boolean documentMetadata) {
this.xhtml = xhtml;
this.metadata = metadata;
this.documentMetadata = documentMetadata;
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
if (depth == 0 && "geogebra".equals(localName)) {
if (documentMetadata) {
setIfNotBlank(GeoGebraParser.APP_NAME, attributes.getValue("app"));
setIfNotBlank(GeoGebraParser.APP_VERSION, attributes.getValue("version"));
setIfNotBlank(GeoGebraParser.FORMAT_VERSION, attributes.getValue("format"));
setIfNotBlank(GeoGebraParser.ID, attributes.getValue("id"));
}
} else if (depth == 1 && "construction".equals(localName)) {
//only the document's own construction; a macro's construction is
//nested one level deeper inside its <macro> element
if (documentMetadata) {
setIfNotBlank(TikaCoreProperties.TITLE, attributes.getValue("title"));
setIfNotBlank(TikaCoreProperties.CREATOR, attributes.getValue("author"));
setIfNotBlank(GeoGebraParser.DATE, attributes.getValue("date"));
}
} else if ("expression".equals(localName)) {
handleExpression(attributes.getValue("exp"));
} else if ("content".equals(localName)) {
handleContent(attributes.getValue("val"));
} else if ("caption".equals(localName)) {
paragraph(attributes.getValue("val"));
} else if ("macro".equals(localName)) {
String toolName = attributes.getValue("toolName");
if (StringUtils.isBlank(toolName)) {
toolName = attributes.getValue("cmdName");
}
if (!StringUtils.isBlank(toolName)) {
metadata.add(GeoGebraParser.TOOL_NAME, toolName.trim());
}
paragraph(toolName);
paragraph(attributes.getValue("toolHelp"));
String iconFile = attributes.getValue("iconFile");
if (!StringUtils.isBlank(iconFile)) {
iconFiles.add(iconFile.trim());
}
}
depth++;
}
List<String> getIconFiles() {
return iconFiles;
}
@Override
public void endElement(String uri, String localName, String qName) {
depth--;
}
/**
* Emits the string literals of an expression. A text object's expression
* is either a single literal like {@code "some text"} or, for a dynamic
* text, literals combined with values like {@code "Area = " + a}; the
* literals are the user's text, everything else is geometry and skipped.
*/
private void handleExpression(String exp) throws SAXException {
if (exp == null || exp.indexOf('"') < 0) {
return;
}
StringBuilder sb = new StringBuilder();
Matcher m = STRING_LITERAL.matcher(exp);
while (m.find()) {
sb.append(m.group(1));
}
paragraph(sb.toString());
}
/**
* Emits the text runs of a rich-text {@code content} value, a JSON array
* of text runs like {@code [{"text":"Hello\n"}]}. All {@code text} fields
* are collected recursively (tables and mind maps nest them), joined, and
* emitted one paragraph per line.
*/
private void handleContent(String val) throws SAXException {
if (val == null) {
return;
}
String trimmed = val.trim();
if (trimmed.isEmpty() || trimmed.length() > MAX_CONTENT_LENGTH
|| (trimmed.charAt(0) != '[' && trimmed.charAt(0) != '{')) {
//not a JSON document; a plain string carries no text runs
return;
}
JsonNode root;
try {
root = GeoGebraParser.OBJECT_MAPPER.readTree(trimmed);
} catch (IOException e) {
return;
}
StringBuilder sb = new StringBuilder();
for (String text : root.findValuesAsText("text")) {
sb.append(text);
}
for (String line : sb.toString().split("\r\n|[\r\n]")) {
paragraph(line);
}
}
private void paragraph(String text) throws SAXException {
if (!StringUtils.isBlank(text)) {
xhtml.element("p", text.trim());
}
}
private void setIfNotBlank(Property property, String value) {
if (!StringUtils.isBlank(value)) {
metadata.set(property, value.trim());
}
}
}