|
|
|
@ -21,14 +21,19 @@ package ca.uhn.fhir.util;
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
import java.io.InputStream;
|
|
|
|
|
import java.io.OutputStream;
|
|
|
|
|
import java.io.OutputStreamWriter;
|
|
|
|
|
import java.io.Reader;
|
|
|
|
|
import java.io.UnsupportedEncodingException;
|
|
|
|
|
import java.io.Writer;
|
|
|
|
|
import java.net.MalformedURLException;
|
|
|
|
|
import java.net.URL;
|
|
|
|
|
import java.util.Collections;
|
|
|
|
|
import java.util.HashMap;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.jar.Attributes;
|
|
|
|
|
import java.util.jar.Manifest;
|
|
|
|
|
|
|
|
|
|
import javax.xml.stream.FactoryConfigurationError;
|
|
|
|
|
import javax.xml.stream.XMLEventReader;
|
|
|
|
@ -39,24 +44,38 @@ import javax.xml.stream.XMLResolver;
|
|
|
|
|
import javax.xml.stream.XMLStreamException;
|
|
|
|
|
import javax.xml.stream.XMLStreamWriter;
|
|
|
|
|
|
|
|
|
|
import org.apache.commons.io.IOUtils;
|
|
|
|
|
import org.apache.commons.lang3.StringEscapeUtils;
|
|
|
|
|
import org.codehaus.stax2.XMLOutputFactory2;
|
|
|
|
|
import org.codehaus.stax2.io.EscapingWriterFactory;
|
|
|
|
|
|
|
|
|
|
import com.ctc.wstx.api.WstxInputProperties;
|
|
|
|
|
import com.ctc.wstx.stax.WstxInputFactory;
|
|
|
|
|
import com.ctc.wstx.stax.WstxOutputFactory;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Utility methods for working with the StAX API.
|
|
|
|
|
*
|
|
|
|
|
* This class contains code adapted from the Apache Axiom project.
|
|
|
|
|
*/
|
|
|
|
|
public class XmlUtil {
|
|
|
|
|
|
|
|
|
|
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(XmlUtil.class);
|
|
|
|
|
private static volatile XMLOutputFactory ourOutputFactory;
|
|
|
|
|
private static volatile XMLInputFactory ourInputFactory;
|
|
|
|
|
private static volatile boolean ourHaveLoggedStaxImplementation;
|
|
|
|
|
private static final Map<String, Integer> VALID_ENTITY_NAMES;
|
|
|
|
|
private static final ExtendedEntityReplacingXmlResolver XML_RESOLVER = new ExtendedEntityReplacingXmlResolver();
|
|
|
|
|
|
|
|
|
|
private static final Attributes.Name IMPLEMENTATION_TITLE = new Attributes.Name("Implementation-Title");
|
|
|
|
|
|
|
|
|
|
private static final Attributes.Name IMPLEMENTATION_VENDOR = new Attributes.Name("Implementation-Vendor");
|
|
|
|
|
|
|
|
|
|
private static final Attributes.Name IMPLEMENTATION_VERSION = new Attributes.Name("Implementation-Version");
|
|
|
|
|
|
|
|
|
|
private static final Attributes.Name BUNDLE_SYMBOLIC_NAME = new Attributes.Name("Bundle-SymbolicName");
|
|
|
|
|
|
|
|
|
|
private static final Attributes.Name BUNDLE_VENDOR = new Attributes.Name("Bundle-Vendor");
|
|
|
|
|
|
|
|
|
|
private static final Attributes.Name BUNDLE_VERSION = new Attributes.Name("Bundle-Version");
|
|
|
|
|
|
|
|
|
|
public static void main(String[] args) {
|
|
|
|
|
|
|
|
|
|
System.out.println(Character.toString((char)167));
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static {
|
|
|
|
|
HashMap<String, Integer> validEntityNames = new HashMap<String, Integer>();
|
|
|
|
|
validEntityNames.put("nbsp", 160); // no-break space = non-breaking space, U+00A0 ISOnum -->
|
|
|
|
@ -70,7 +89,8 @@ public class XmlUtil {
|
|
|
|
|
validEntityNames.put("uml", 168); // diaeresis = spacing diaeresis, U+00A8 ISOdia -->
|
|
|
|
|
validEntityNames.put("copy", 169); // copyright sign, U+00A9 ISOnum -->
|
|
|
|
|
validEntityNames.put("ordf", 170); // feminine ordinal indicator, U+00AA ISOnum -->
|
|
|
|
|
validEntityNames.put("laquo", 171); // left-pointing double angle quotation mark = left pointing guillemet, U+00AB ISOnum -->
|
|
|
|
|
validEntityNames.put("laquo", 171); // left-pointing double angle quotation mark = left pointing guillemet,
|
|
|
|
|
// U+00AB ISOnum -->
|
|
|
|
|
validEntityNames.put("not", 172); // not sign = angled dash, U+00AC ISOnum -->
|
|
|
|
|
validEntityNames.put("shy", 173); // soft hyphen = discretionary hyphen, U+00AD ISOnum -->
|
|
|
|
|
validEntityNames.put("reg", 174); // registered sign = registered trade mark sign, U+00AE ISOnum -->
|
|
|
|
@ -86,17 +106,21 @@ public class XmlUtil {
|
|
|
|
|
validEntityNames.put("cedil", 184); // cedilla = spacing cedilla, U+00B8 ISOdia -->
|
|
|
|
|
validEntityNames.put("sup1", 185); // superscript one = superscript digit one, U+00B9 ISOnum -->
|
|
|
|
|
validEntityNames.put("ordm", 186); // masculine ordinal indicator, U+00BA ISOnum -->
|
|
|
|
|
validEntityNames.put("raquo", 187); // right-pointing double angle quotation mark = right pointing guillemet, U+00BB ISOnum -->
|
|
|
|
|
validEntityNames.put("raquo", 187); // right-pointing double angle quotation mark = right pointing guillemet,
|
|
|
|
|
// U+00BB ISOnum -->
|
|
|
|
|
validEntityNames.put("frac14", 188); // vulgar fraction one quarter = fraction one quarter, U+00BC ISOnum -->
|
|
|
|
|
validEntityNames.put("frac12", 189); // vulgar fraction one half = fraction one half, U+00BD ISOnum -->
|
|
|
|
|
validEntityNames.put("frac34", 190); // vulgar fraction three quarters = fraction three quarters, U+00BE ISOnum -->
|
|
|
|
|
validEntityNames.put("frac34", 190); // vulgar fraction three quarters = fraction three quarters, U+00BE ISOnum
|
|
|
|
|
// -->
|
|
|
|
|
validEntityNames.put("iquest", 191); // inverted question mark = turned question mark, U+00BF ISOnum -->
|
|
|
|
|
validEntityNames.put("Agrave", 192); // latin capital letter A with grave = latin capital letter A grave, U+00C0 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("Agrave", 192); // latin capital letter A with grave = latin capital letter A grave, U+00C0
|
|
|
|
|
// ISOlat1 -->
|
|
|
|
|
validEntityNames.put("Aacute", 193); // latin capital letter A with acute, U+00C1 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("Acirc", 194); // latin capital letter A with circumflex, U+00C2 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("Atilde", 195); // latin capital letter A with tilde, U+00C3 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("Auml", 196); // latin capital letter A with diaeresis, U+00C4 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("Aring", 197); // latin capital letter A with ring above = latin capital letter A ring, U+00C5 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("Aring", 197); // latin capital letter A with ring above = latin capital letter A ring,
|
|
|
|
|
// U+00C5 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("AElig", 198); // latin capital letter AE = latin capital ligature AE, U+00C6 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("Ccedil", 199); // latin capital letter C with cedilla, U+00C7 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("Egrave", 200); // latin capital letter E with grave, U+00C8 ISOlat1 -->
|
|
|
|
@ -115,7 +139,8 @@ public class XmlUtil {
|
|
|
|
|
validEntityNames.put("Otilde", 213); // latin capital letter O with tilde, U+00D5 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("Ouml", 214); // latin capital letter O with diaeresis, U+00D6 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("times", 215); // multiplication sign, U+00D7 ISOnum -->
|
|
|
|
|
validEntityNames.put("Oslash", 216); // latin capital letter O with stroke = latin capital letter O slash, U+00D8 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("Oslash", 216); // latin capital letter O with stroke = latin capital letter O slash,
|
|
|
|
|
// U+00D8 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("Ugrave", 217); // latin capital letter U with grave, U+00D9 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("Uacute", 218); // latin capital letter U with acute, U+00DA ISOlat1 -->
|
|
|
|
|
validEntityNames.put("Ucirc", 219); // latin capital letter U with circumflex, U+00DB ISOlat1 -->
|
|
|
|
@ -123,12 +148,14 @@ public class XmlUtil {
|
|
|
|
|
validEntityNames.put("Yacute", 221); // latin capital letter Y with acute, U+00DD ISOlat1 -->
|
|
|
|
|
validEntityNames.put("THORN", 222); // latin capital letter THORN, U+00DE ISOlat1 -->
|
|
|
|
|
validEntityNames.put("szlig", 223); // latin small letter sharp s = ess-zed, U+00DF ISOlat1 -->
|
|
|
|
|
validEntityNames.put("agrave", 224); // latin small letter a with grave = latin small letter a grave, U+00E0 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("agrave", 224); // latin small letter a with grave = latin small letter a grave, U+00E0
|
|
|
|
|
// ISOlat1 -->
|
|
|
|
|
validEntityNames.put("aacute", 225); // latin small letter a with acute, U+00E1 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("acirc", 226); // latin small letter a with circumflex, U+00E2 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("atilde", 227); // latin small letter a with tilde, U+00E3 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("auml", 228); // latin small letter a with diaeresis, U+00E4 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("aring", 229); // latin small letter a with ring above = latin small letter a ring, U+00E5 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("aring", 229); // latin small letter a with ring above = latin small letter a ring, U+00E5
|
|
|
|
|
// ISOlat1 -->
|
|
|
|
|
validEntityNames.put("aelig", 230); // latin small letter ae = latin small ligature ae, U+00E6 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("ccedil", 231); // latin small letter c with cedilla, U+00E7 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("egrave", 232); // latin small letter e with grave, U+00E8 ISOlat1 -->
|
|
|
|
@ -147,7 +174,8 @@ public class XmlUtil {
|
|
|
|
|
validEntityNames.put("otilde", 245); // latin small letter o with tilde, U+00F5 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("ouml", 246); // latin small letter o with diaeresis, U+00F6 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("divide", 247); // division sign, U+00F7 ISOnum -->
|
|
|
|
|
validEntityNames.put("oslash", 248); // latin small letter o with stroke, = latin small letter o slash, U+00F8 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("oslash", 248); // latin small letter o with stroke, = latin small letter o slash, U+00F8
|
|
|
|
|
// ISOlat1 -->
|
|
|
|
|
validEntityNames.put("ugrave", 249); // latin small letter u with grave, U+00F9 ISOlat1 -->
|
|
|
|
|
validEntityNames.put("uacute", 250); // latin small letter u with acute, U+00FA ISOlat1 -->
|
|
|
|
|
validEntityNames.put("ucirc", 251); // latin small letter u with circumflex, U+00FB ISOlat1 -->
|
|
|
|
@ -160,47 +188,143 @@ public class XmlUtil {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static XMLEventReader createXmlReader(Reader reader) throws FactoryConfigurationError, XMLStreamException {
|
|
|
|
|
XMLInputFactory inputFactory = XMLInputFactory.newInstance();
|
|
|
|
|
|
|
|
|
|
XMLResolver xmlResolver = new XMLResolver() {
|
|
|
|
|
@Override
|
|
|
|
|
public Object resolveEntity(String thePublicID, String theSystemID, String theBaseURI, String theNamespace) throws XMLStreamException {
|
|
|
|
|
if (thePublicID == null && theSystemID == null) {
|
|
|
|
|
if (theNamespace != null && VALID_ENTITY_NAMES.containsKey(theNamespace)) {
|
|
|
|
|
return new String(Character.toChars(VALID_ENTITY_NAMES.get(theNamespace)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// In the following two lines, you can uncomment the first and comment the second to disable
|
|
|
|
|
// automatic parsing of extended entities, e.g. §
|
|
|
|
|
// inputFactory.setProperty(WstxInputFactory.IS_REPLACING_ENTITY_REFERENCES, false);
|
|
|
|
|
inputFactory.setProperty(WstxInputProperties.P_UNDECLARED_ENTITY_RESOLVER, xmlResolver);
|
|
|
|
|
XMLInputFactory inputFactory = getOrCreateInputFactory();
|
|
|
|
|
|
|
|
|
|
// Now.. create the reader and return it
|
|
|
|
|
XMLEventReader er = inputFactory.createXMLEventReader(reader);
|
|
|
|
|
return er;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static XMLEventWriter createXmlWriter(Writer theWriter) throws FactoryConfigurationError, XMLStreamException {
|
|
|
|
|
XMLOutputFactory newInstance = XMLOutputFactory.newInstance();
|
|
|
|
|
private static XMLInputFactory getOrCreateInputFactory() throws FactoryConfigurationError {
|
|
|
|
|
if (ourInputFactory == null) {
|
|
|
|
|
XMLInputFactory inputFactory = XMLInputFactory.newInstance();
|
|
|
|
|
|
|
|
|
|
newInstance.setProperty(XMLOutputFactory2.P_TEXT_ESCAPER, new MyEscaper());
|
|
|
|
|
if (!ourHaveLoggedStaxImplementation) {
|
|
|
|
|
logStaxImplementation(inputFactory.getClass());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
XMLEventWriter ew = newInstance.createXMLEventWriter(theWriter);
|
|
|
|
|
return ew;
|
|
|
|
|
/*
|
|
|
|
|
* In the following few lines, you can uncomment the first and comment the second to disable automatic
|
|
|
|
|
* parsing of extended entities, e.g. §
|
|
|
|
|
*
|
|
|
|
|
* Note that these properties are Woodstox specific and they cause a crash in environments where SJSXP is
|
|
|
|
|
* being used (e.g. glassfish) so we don't set them there.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
if (inputFactory instanceof com.ctc.wstx.stax.WstxInputFactory) {
|
|
|
|
|
// inputFactory.setProperty(WstxInputFactory.IS_REPLACING_ENTITY_REFERENCES, false);
|
|
|
|
|
inputFactory.setProperty(WstxInputProperties.P_UNDECLARED_ENTITY_RESOLVER, XML_RESOLVER);
|
|
|
|
|
}
|
|
|
|
|
ourInputFactory = inputFactory;
|
|
|
|
|
}
|
|
|
|
|
return ourInputFactory;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void logStaxImplementation(Class<?> theClass) {
|
|
|
|
|
try {
|
|
|
|
|
URL rootUrl = getRootUrlForClass(theClass);
|
|
|
|
|
if (rootUrl == null) {
|
|
|
|
|
ourLog.info("Unable to determine location of StAX implementation containing class");
|
|
|
|
|
} else {
|
|
|
|
|
Manifest manifest;
|
|
|
|
|
URL metaInfUrl = new URL(rootUrl, "META-INF/MANIFEST.MF");
|
|
|
|
|
InputStream is = metaInfUrl.openStream();
|
|
|
|
|
try {
|
|
|
|
|
manifest = new Manifest(is);
|
|
|
|
|
} finally {
|
|
|
|
|
is.close();
|
|
|
|
|
}
|
|
|
|
|
Attributes attrs = manifest.getMainAttributes();
|
|
|
|
|
String title = attrs.getValue(IMPLEMENTATION_TITLE);
|
|
|
|
|
String symbolicName = attrs.getValue(BUNDLE_SYMBOLIC_NAME);
|
|
|
|
|
if (symbolicName != null) {
|
|
|
|
|
int i = symbolicName.indexOf(';');
|
|
|
|
|
if (i != -1) {
|
|
|
|
|
symbolicName = symbolicName.substring(0, i);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
String vendor = attrs.getValue(IMPLEMENTATION_VENDOR);
|
|
|
|
|
if (vendor == null) {
|
|
|
|
|
vendor = attrs.getValue(BUNDLE_VENDOR);
|
|
|
|
|
}
|
|
|
|
|
String version = attrs.getValue(IMPLEMENTATION_VERSION);
|
|
|
|
|
if (version == null) {
|
|
|
|
|
version = attrs.getValue(BUNDLE_VERSION);
|
|
|
|
|
}
|
|
|
|
|
if (ourLog.isDebugEnabled()) {
|
|
|
|
|
ourLog.debug("FHIR XML procesing will use StAX implementation at {}\n Title: {}\n Symbolic name: {}\n Vendor: {}\n Version: {}", rootUrl, title, symbolicName, vendor, version);
|
|
|
|
|
} else {
|
|
|
|
|
ourLog.info("FHIR XML procesing will use StAX implementation '{}' version '{}'", title, version);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (Throwable e) {
|
|
|
|
|
ourLog.info("Unable to determine StAX implementation: " + e.getMessage());
|
|
|
|
|
} finally {
|
|
|
|
|
ourHaveLoggedStaxImplementation = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static URL getRootUrlForClass(Class<?> cls) {
|
|
|
|
|
ClassLoader classLoader = cls.getClassLoader();
|
|
|
|
|
String resource = cls.getName().replace('.', '/') + ".class";
|
|
|
|
|
if (classLoader == null) {
|
|
|
|
|
// A null class loader means the bootstrap class loader. In this case we use the
|
|
|
|
|
// system class loader. This is safe since we can assume that the system class
|
|
|
|
|
// loader uses parent first as delegation policy.
|
|
|
|
|
classLoader = ClassLoader.getSystemClassLoader();
|
|
|
|
|
}
|
|
|
|
|
URL url = classLoader.getResource(resource);
|
|
|
|
|
if (url == null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
String file = url.getFile();
|
|
|
|
|
if (file.endsWith(resource)) {
|
|
|
|
|
try {
|
|
|
|
|
return new URL(url.getProtocol(), url.getHost(), url.getPort(), file.substring(0, file.length() - resource.length()));
|
|
|
|
|
} catch (MalformedURLException ex) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static XMLStreamWriter createXmlStreamWriter(Writer theWriter) throws FactoryConfigurationError, XMLStreamException {
|
|
|
|
|
XMLOutputFactory newInstance = XMLOutputFactory.newInstance();
|
|
|
|
|
|
|
|
|
|
newInstance.setProperty(XMLOutputFactory2.P_TEXT_ESCAPER, new MyEscaper());
|
|
|
|
|
|
|
|
|
|
XMLStreamWriter ew = newInstance.createXMLStreamWriter(theWriter);
|
|
|
|
|
return ew;
|
|
|
|
|
XMLOutputFactory outputFactory = getOrCreateOutputFactory();
|
|
|
|
|
XMLStreamWriter retVal = outputFactory.createXMLStreamWriter(theWriter);
|
|
|
|
|
return retVal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static XMLEventWriter createXmlWriter(Writer theWriter) throws FactoryConfigurationError, XMLStreamException {
|
|
|
|
|
XMLOutputFactory outputFactory = getOrCreateOutputFactory();
|
|
|
|
|
XMLEventWriter retVal = outputFactory.createXMLEventWriter(theWriter);
|
|
|
|
|
return retVal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void main(String[] args) {
|
|
|
|
|
System.out.println(Character.toString((char) 167));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static XMLOutputFactory getOrCreateOutputFactory() throws FactoryConfigurationError {
|
|
|
|
|
if (ourOutputFactory == null) {
|
|
|
|
|
XMLOutputFactory outputFactory = XMLOutputFactory.newInstance();
|
|
|
|
|
|
|
|
|
|
if (!ourHaveLoggedStaxImplementation) {
|
|
|
|
|
logStaxImplementation(outputFactory.getClass());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Note that these properties are Woodstox specific and they cause a crash in environments where SJSXP is
|
|
|
|
|
* being used (e.g. glassfish) so we don't set them there.
|
|
|
|
|
*/
|
|
|
|
|
if (outputFactory instanceof WstxOutputFactory) {
|
|
|
|
|
outputFactory.setProperty(XMLOutputFactory2.P_TEXT_ESCAPER, new MyEscaper());
|
|
|
|
|
}
|
|
|
|
|
ourOutputFactory = outputFactory;
|
|
|
|
|
}
|
|
|
|
|
return ourOutputFactory;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static class MyEscaper implements EscapingWriterFactory {
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
@ -249,4 +373,17 @@ public class XmlUtil {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static final class ExtendedEntityReplacingXmlResolver implements XMLResolver {
|
|
|
|
|
@Override
|
|
|
|
|
public Object resolveEntity(String thePublicID, String theSystemID, String theBaseURI, String theNamespace) throws XMLStreamException {
|
|
|
|
|
if (thePublicID == null && theSystemID == null) {
|
|
|
|
|
if (theNamespace != null && VALID_ENTITY_NAMES.containsKey(theNamespace)) {
|
|
|
|
|
return new String(Character.toChars(VALID_ENTITY_NAMES.get(theNamespace)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|