All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.sf.saxon.ma.json.JsonHandlerXML Maven / Gradle / Ivy

There is a newer version: 12.5
Show newest version
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2018-2022 Saxonica Limited
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

package net.sf.saxon.ma.json;

import net.sf.saxon.Configuration;
import net.sf.saxon.Version;
import net.sf.saxon.event.Builder;
import net.sf.saxon.event.ComplexContentOutputter;
import net.sf.saxon.event.Outputter;
import net.sf.saxon.event.ReceiverOption;
import net.sf.saxon.expr.XPathContext;
import net.sf.saxon.expr.parser.Loc;
import net.sf.saxon.lib.NamespaceConstant;
import net.sf.saxon.om.*;
import net.sf.saxon.str.StringView;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.type.*;
import net.sf.saxon.value.AtomicValue;

import javax.xml.transform.stream.StreamSource;
import java.io.InputStream;
import java.util.*;

/**
 * Handler to generate an XML representation of JSON from a series of events
 */
public class JsonHandlerXML extends JsonHandler {

    private final Outputter out;
    private final Builder builder;

    private Stack keys;
    private final Stack inMap = new Stack<>();

    private boolean allowAnyTopLevel;
    public boolean validate;
    private boolean checkForDuplicates;

    private static final String SCHEMA_URI = "http://www.w3.org/2005/xpath-functions.xsd";
    private static final String JSON_NS = NamespaceConstant.FN;
    public static final String PREFIX = "";

    private NamePool namePool;
    private FingerprintedQName mapQN;
    private FingerprintedQName arrayQN;
    private FingerprintedQName stringQN;
    private FingerprintedQName numberQN;
    private FingerprintedQName booleanQN;
    private FingerprintedQName nullQN;
    private FingerprintedQName keyQN;
    private FingerprintedQName escapedQN;
    private FingerprintedQName escapedKeyQN;

    private static final SimpleType SIMPLE_TYPE = AnySimpleType.getInstance();
    private static final SimpleType BOOLEAN_TYPE = BuiltInAtomicType.BOOLEAN;
    private static final SimpleType STRING_TYPE = BuiltInAtomicType.STRING;

    public HashMap types;
    private final Stack> mapKeys = new Stack<>();


    /**
     * Create a QName in null namespace
     *
     * @param s the local name
     * @return the QName
     */
    private FingerprintedQName qname(String s) {
        FingerprintedQName fp = new FingerprintedQName("", "", s);
        fp.obtainFingerprint(namePool);
        return fp;
    }

    /**
     * Create a QName in the JSON namespace
     *
     * @param s the local name
     * @return the QName
     */
    private FingerprintedQName qnameNS(String s) {
        FingerprintedQName fp = new FingerprintedQName(PREFIX, JSON_NS, s);
        fp.obtainFingerprint(namePool);
        return fp;
    }

    /**
     * Make the handler to construct the XML tree representation for JSON
     *
     * @param context       the context in which the result tree is to be built
     * @param staticBaseUri the static base URI, used for the base URI of the constructed tree
     * @param flags         flags indicating the chosen options
     * @throws XPathException if initialization fails, for example because of problems loading the schema
     */
    public JsonHandlerXML(XPathContext context, String staticBaseUri, int flags) throws XPathException {
        init(context, flags);
        builder = context.getController().makeBuilder();
        builder.setSystemId(staticBaseUri);
        builder.setTiming(false);
        out = new ComplexContentOutputter(builder);
        out.open();
        out.startDocument(ReceiverOption.NONE);
    }

    /**
     * Initialise the tree builder.
     * 

This also ensures the appropriate schema is loaded when type validation is required

* * @param context the context in which the result tree is to be built * @param flags flags indicating the chosen options * @throws XPathException if anything goes wrong (for example, with getting schema information) */ private void init(XPathContext context, int flags) throws XPathException { keys = new Stack<>(); /* This may not need to be a stack as there should only be at most one pre-selected key * However, the stack neatly indicates its empty state * */ setContext(context); charChecker = context.getConfiguration().getValidCharacterChecker(); escape = (flags & JsonParser.ESCAPE) != 0; allowAnyTopLevel = (flags & JsonParser.ALLOW_ANY_TOP_LEVEL) != 0; validate = (flags & JsonParser.VALIDATE) != 0; checkForDuplicates = validate || (flags & JsonParser.DUPLICATES_RETAINED) == 0; types = new HashMap<>(); namePool = context.getConfiguration().getNamePool(); mapQN = qnameNS("map"); arrayQN = qnameNS("array"); stringQN = qnameNS("string"); numberQN = qnameNS("number"); booleanQN = qnameNS("boolean"); nullQN = qnameNS("null"); keyQN = qname("key"); escapedQN = qname("escaped"); escapedKeyQN = qname("escaped-key"); if (validate) { // Note, we do not actually perform schema validation, because we assume the XML we are generating // is valid. Instead, we just set type annotations "on trust", as if we were validating. // Currently this means we aren't detecting duplicate keys, which would cause validation to fail. // The spec needs clarification in this area. try { Configuration config = context.getConfiguration(); //noinspection SynchronizationOnLocalVariableOrMethodParameter synchronized (config) { config.checkLicensedFeature(Configuration.LicenseFeature.SCHEMA_VALIDATION, "validation", -1); loadSchema(config); } String[] typeNames = {"mapType", "arrayType", "stringType", "numberType", "booleanType", "nullType", "mapWithinMapType", "arrayWithinMapType", "stringWithinMapType", "numberWithinMapType", "booleanWithinMapType", "nullWithinMapType"}; for (String t : typeNames) { setType(t, config.getSchemaType(new StructuredQName(PREFIX, JSON_NS, t))); } } catch (SchemaException e) { throw new XPathException(e); } } } private void loadSchema(Configuration config) throws SchemaException { if (!config.isSchemaAvailable(JSON_NS)) { List messages = new ArrayList<>(); InputStream stream = Version.platform.locateResource("xpath-functions.scm", messages); if (config.isTiming()) { config.getLogger().info("Loading schema for: " + JSON_NS); } config.addSchemaSource(new StreamSource(stream, "classpath:xpath-functions.xsd")); } } /** * Record a SchemaType for a particular name * * @param name the name to be used for the type, e.g. "arrayType" * @param st the schema type to be used for typing such entities */ public void setType(String name, SchemaType st) { types.put(name, st); } /** * Set the key to be written for the next entry in an object/map * * @param unEscaped the key for the entry (null implies no key) in unescaped form (backslashes, * if present, do not signal an escape sequence) * @param reEscaped the key for the entry (null implies no key) in reescaped form. In this form * special characters are represented as backslash-escaped sequences if the escape * option is yes; if escape=no, the reEscaped form is the same as the unEscaped form. * @return true if the key is already present in the map, false if it is not */ @Override public boolean setKey(String unEscaped, String reEscaped) { this.keys.push(unEscaped); return checkForDuplicates && !mapKeys.peek().add(reEscaped); } /** * Return the complete parsed result * * @return the XML document for this JSON * @throws XPathException if an error occurs downstream */ @Override public Item getResult() throws XPathException { out.endDocument(); out.close(); return builder.getCurrentRoot(); } /** * Check whether a string contains an escape sequence * * @param literal the string to be checked * @return true if the string contains a backslash */ private boolean containsEscape(String literal) { return literal.indexOf('\\') >= 0; } private boolean isInMap() { return !inMap.isEmpty() && inMap.peek(); } /** * Start an element in the result tree * * @param qn the QName of the element * @param typeName the string name of the type to use * @throws XPathException if a dynamic error occurs */ private void startElement(FingerprintedQName qn, String typeName) throws XPathException { startElement(qn, types.get(typeName)); } /** * Start an element in the result tree, which will have a key attribute if a key has been pre-selected * * @param qn the QName of the element * @param st the schema type to use when validating - null implies untyped. * @throws XPathException if a dynamic error occurs */ private void startElement(FingerprintedQName qn, SchemaType st) throws XPathException { out.startElement(qn, validate && st != null ? st : Untyped.getInstance(), Loc.NONE, ReceiverOption.NONE); if (isInMap()) { String k = keys.pop(); String uk = reEscape(k); if (escape) { markAsEscaped(uk, true); } out.attribute(keyQN, validate ? STRING_TYPE : SIMPLE_TYPE, uk, Loc.NONE, ReceiverOption.NONE); } } private void startContent() throws XPathException { out.startContent(); } /** * Write characters as a text node in the tree * * @param s the string to be added * @throws XPathException if a dynamic error occurs */ private void characters(String s) throws XPathException { out.characters(StringView.of(s), Loc.NONE, ReceiverOption.NONE); } /** * End the current element in the treel * * @throws XPathException if a dynamic error occurs */ private void endElement() throws XPathException { out.endElement(); } /** * Open a new array * * @throws XPathException if a dynamic error occurs */ @Override public void startArray() throws XPathException { startElement(arrayQN, isInMap() ? "arrayWithinMapType" : "arrayType"); inMap.push(false); startContent(); } /** * Close the current array * * @throws XPathException if a dynamic error occurs */ @Override public void endArray() throws XPathException { inMap.pop(); endElement(); } /** * Start a new object/map * * @throws XPathException if a dynamic error occurs */ @Override public void startMap() throws XPathException { startElement(mapQN, isInMap() ? "mapWithinMapType": "mapType"); if (checkForDuplicates) { mapKeys.push(new HashSet<>()); } inMap.push(true); startContent(); } /** * Close the current object/map * * @throws XPathException if a dynamic error occurs */ @Override public void endMap() throws XPathException { inMap.pop(); if (checkForDuplicates) { mapKeys.pop(); } endElement(); } /** * Write a numeric value * * @param asString the string representation of the value * @param parsedValue the double representation of the value * @throws XPathException if a dynamic error occurs */ @Override public void writeNumeric(String asString, AtomicValue parsedValue) throws XPathException { startElement(numberQN, isInMap() ? "numberWithinMapType" : "numberType"); startContent(); characters(asString); endElement(); } /** * Write a string value * * @param val the string to be written. This will be in unescaped form if unescaping was requested * in the flags, otherwise it may contain JSON escape sequences * @throws XPathException if a dynamic error occurs */ @Override public void writeString(String val) throws XPathException { startElement(stringQN, isInMap() ? "stringWithinMapType" : "stringType"); String escaped = reEscape(val); if (escape) { markAsEscaped(escaped, false); } startContent(); characters(escaped); endElement(); } @Override protected void markAsEscaped(String escaped, boolean isKey) throws XPathException { if (containsEscape(escaped) && escape) { NodeName name = isKey ? escapedKeyQN : escapedQN; out.attribute(name, validate ? BOOLEAN_TYPE : SIMPLE_TYPE, "true", Loc.NONE, ReceiverOption.NONE); } } /** * Write a boolean value * * @param value the boolean value to be written * @throws XPathException if a dynamic error occurs */ @Override public void writeBoolean(boolean value) throws XPathException { startElement(booleanQN, isInMap() ? "booleanWithinMapType" : "booleanType"); startContent(); characters(value ? "true" : "false"); endElement(); } /** * Write a null value * * @throws XPathException if a dynamic error occurs */ @Override public void writeNull() throws XPathException { startElement(nullQN, isInMap() ? "nullWithinMapType" : "nullType"); startContent(); endElement(); } } // Copyright (c) 2014-2022 Saxonica Limited




© 2015 - 2024 Weber Informatics LLC | Privacy Policy