blob: 10832f8a8d80d2a46dbe4f39bb06accb4c48ba34 [file] [log] [blame]
/*
* 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.jena.datatypes.xsd.impl;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Objects;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.jena.atlas.lib.Lib;
import org.apache.jena.atlas.logging.Log;
import org.apache.jena.datatypes.BaseDatatype;
import org.apache.jena.datatypes.DatatypeFormatException;
import org.apache.jena.datatypes.RDFDatatype;
import org.apache.jena.graph.impl.LiteralLabel;
import org.apache.jena.shared.JenaException;
import org.apache.jena.util.JenaXMLInput;
import org.apache.jena.util.JenaXMLOutput;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
/**
* <a href="https://www.w3.org/TR/rdf-concepts/#section-XMLLiteral"<rdf:XMLLiteral</a>.
* <p>
* This implementation has RDF 1.1/RDF1.2 semantics.
* <p>
* Valid XML is legal. The value space is an XML document fragment.
*/
public class XMLLiteralType extends BaseDatatype implements RDFDatatype {
public static String XMLLiteralTypeURI = "http://www.w3.org/1999/02/22-rdf-syntax-ns#XMLLiteral";
/** Singleton instance */
// Include the string for the RDF namespace, do not use RDF.getURI(), to avoid the risk of an initializer circularity.
public static final RDFDatatype theXMLLiteralType = new XMLLiteralType(XMLLiteralTypeURI);
private static final String xmlWrapperTagName = "xml-literal-fragment";
private static final String xmlWrapperTagStart = "<"+xmlWrapperTagName+">";
private static final String xmlWrapperTagEnd = "</"+xmlWrapperTagName+">";
/**
* Test where an {@link RDFDatatype} is that for {@code rdf:XMLLiteral}.
*/
public static boolean isXMLLiteral(RDFDatatype rdfDatatype) {
Objects.requireNonNull(rdfDatatype);
return XMLLiteralTypeURI.equals(rdfDatatype.getURI());
}
private XMLLiteralType(String uri) {
super(uri);
}
/**
* Compares two instances of values of the given datatype.
*/
@Override
public boolean isEqual(LiteralLabel value1, LiteralLabel value2) {
try {
if ( ! value1.getDatatype().getURI().equals(XMLLiteralTypeURI) )
return false;
if ( ! value2.getDatatype().getURI().equals(XMLLiteralTypeURI) )
return false;
DocumentFragment f1 = (DocumentFragment)value1.getValue();
DocumentFragment f2 = (DocumentFragment)value2.getValue();
return f1.isEqualNode(f2);
} catch (Exception ex) {
throw new DatatypeFormatException();
}
}
/** {@inheritDoc} */
@Override
public Object parse(String lexicalForm) {
try {
return xmlLiteralValue(lexicalForm);
} catch (Exception ex) {
throw new DatatypeFormatException();
}
}
/**
* Parse a lexical form for an rdf:XMLLiteral to produce the literal value.
* The value is an XML {@link DocumentFragment}.
*/
public static DocumentFragment xmlLiteralValue(String string) {
DocumentBuilder builder = newDocumentBuilder();
builder.isNamespaceAware();
ErrorHandlerCounting eh = new ErrorHandlerCounting();
builder.setErrorHandler(eh);
// Wrap in an outer start-finish to make it into a XML document
String xmlString = xmlWrapperTagStart+string+xmlWrapperTagEnd;
// Parse document.
InputSource source = new InputSource(new StringReader(xmlString));
final Document doc;
try {
doc = builder.parse(source);
if ( eh.errorHappened() )
return null;
doc.normalizeDocument();
} catch (IOException ex) {
throw new java.io.UncheckedIOException(ex);
} catch (SAXException ex) {
throw Lib.runtimeException(ex);
}
// Unwrap the outer start-finish element.
NodeList nodeList0 = doc.getChildNodes();
if ( nodeList0.getLength() != 1 )
throw new JenaException("XML parser did not produce exactly one child node");
// The DocumentFragment as a NodeList.
NodeList nodeList = nodeList0.item(0).getChildNodes();
// The value of an rdf:XMLLIteral is a org.w3c.dom.DocumentFragment object.
// Build the DocumentFragment -- NB "appendChild" removes from the doc DOM
// if the node comes form the parsed document, which is it in our situation.
// Hence the "item(0)" removes the nodes in order.
DocumentFragment docFrag = doc.createDocumentFragment();
while(nodeList.getLength() > 0) {
Node n = nodeList.item(0);
docFrag.appendChild(n); // Destructive - removes from doc, changes nodeList
}
return docFrag;
}
/** {@inheritDoc} */
@Override
public String unparse(Object value) {
// "unparse" value to string.
if ( value instanceof DocumentFragment docFrag )
return xmlDocumentFragmentToString(docFrag);
throw new IllegalArgumentException("Value is not a ocumentFragment");
}
/**
* Turn an org.w3c.dom.DocumentFragment into a string.
*/
public static String xmlDocumentFragmentToString(DocumentFragment fragment) {
StringWriter sw = new StringWriter();
Source source = new DOMSource(fragment);
try {
Transformer transformer = JenaXMLOutput.xmlTransformer();
// Essential for a DocumentFragment
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
//transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StreamResult output = new StreamResult(sw);
transformer.transform(source, output);
} catch (TransformerException ex) {
Log.error(XMLLiteralType.class, "Failed to convert an org.w3c.dom.Node to a string", ex);
// Fall through
}
return sw.toString();
}
private static DocumentBuilder newDocumentBuilder() {
try {
return docBuilderFactory.newDocumentBuilder();
} catch (ParserConfigurationException ex) {
throw Lib.runtimeException(ex);
}
}
private static DocumentBuilderFactory docBuilderFactory = createDocumentBuilderFactory();
private static DocumentBuilderFactory createDocumentBuilderFactory() {
try {
return JenaXMLInput.newDocumentBuilderFactory();
} catch (ParserConfigurationException ex) {
Log.error(XMLLiteralType.class, "Failed to build a javax.xml.parsers.DocumentBuilderFactory", ex);
return null;
}
}
private static class ErrorHandlerCounting implements ErrorHandler {
int warning = 0;
int errors = 0;
int fatalErrors = 0;
boolean errorHappened() {
return errors > 0 || fatalErrors > 0;
}
@Override
public void warning(SAXParseException exception) {
warning++;
}
@Override
public void error(SAXParseException exception) {
errors++;
}
@Override
public void fatalError(SAXParseException exception) throws SAXException {
fatalErrors++;
throw exception;
}
}
}