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

net.sf.saxon.ma.map.MapFunctionSet 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.map;

import net.sf.saxon.expr.*;
import net.sf.saxon.expr.parser.ContextItemStaticInfo;
import net.sf.saxon.expr.parser.ExpressionVisitor;
import net.sf.saxon.functions.InsertBefore;
import net.sf.saxon.functions.OptionsParameter;
import net.sf.saxon.functions.SystemFunction;
import net.sf.saxon.functions.registry.BuiltInFunctionSet;
import net.sf.saxon.lib.NamespaceConstant;
import net.sf.saxon.ma.arrays.ArrayItem;
import net.sf.saxon.ma.arrays.ArrayItemType;
import net.sf.saxon.ma.arrays.SimpleArrayItem;
import net.sf.saxon.ma.zeno.ZenoSequence;
import net.sf.saxon.om.*;
import net.sf.saxon.trace.ExpressionPresenter;
import net.sf.saxon.trans.Err;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.type.*;
import net.sf.saxon.value.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Function signatures (and pointers to implementations) of the functions defined in XPath 2.0
 */

public class MapFunctionSet extends BuiltInFunctionSet {

    public static MapFunctionSet THE_INSTANCE = new MapFunctionSet();

    public MapFunctionSet() {
        init();
    }

    public static MapFunctionSet getInstance() {
        return THE_INSTANCE;
    }



    private void init() {

        register("merge", 1, MapMerge.class, MapType.ANY_MAP_TYPE, ONE, 0)
                .arg(0, MapType.ANY_MAP_TYPE, STAR | INS, null);

        SpecificFunctionType ON_DUPLICATES_CALLBACK_TYPE = new SpecificFunctionType(
                new SequenceType[]{SequenceType.ANY_SEQUENCE, SequenceType.ANY_SEQUENCE},
                SequenceType.ANY_SEQUENCE
        );

        SequenceType oneOnDuplicatesFunction = SequenceType.makeSequenceType(
                ON_DUPLICATES_CALLBACK_TYPE, StaticProperty.EXACTLY_ONE);

        OptionsParameter mergeOptionDetails = new OptionsParameter();
        mergeOptionDetails.addAllowedOption("duplicates", SequenceType.SINGLE_STRING, StringValue.bmp("use-first"));
        // duplicates=unspecified is retained because that's what the XSLT 3.0 Rec incorrectly uses
        mergeOptionDetails.setAllowedValues("duplicates", "FOJS0005", "use-first", "use-last", "combine", "reject", "unspecified", "use-any", "use-callback");
        mergeOptionDetails.addAllowedOption(MapMerge.errorCodeKey, SequenceType.SINGLE_STRING, StringValue.bmp("FOJS0003"));
        mergeOptionDetails.addAllowedOption(MapMerge.keyTypeKey, SequenceType.SINGLE_STRING, StringValue.bmp("anyAtomicType"));
        mergeOptionDetails.addAllowedOption(MapMerge.finalKey, SequenceType.SINGLE_BOOLEAN, BooleanValue.FALSE);
        mergeOptionDetails.addAllowedOption(MapMerge.onDuplicatesKey, oneOnDuplicatesFunction, null);


        register("merge", 2, MapMerge.class, MapType.ANY_MAP_TYPE, ONE, 0)
                .arg(0, MapType.ANY_MAP_TYPE, STAR, null)
                .arg(1, MapType.ANY_MAP_TYPE, ONE, null)
                .setOptionDetails(mergeOptionDetails);

        register("entry", 2, MapEntry.class, MapType.ANY_MAP_TYPE, ONE, 0)
                .arg(0, BuiltInAtomicType.ANY_ATOMIC, ONE | ABS, null)
                .arg(1, AnyItemType.getInstance(), STAR | NAV, null);

        register("find", 2, MapFind.class, ArrayItemType.getInstance(), ONE, 0)
                .arg(0, AnyItemType.getInstance(), STAR | INS, null)
                .arg(1, BuiltInAtomicType.ANY_ATOMIC, ONE | ABS, null);

        register("get", 2, MapGet.class, AnyItemType.getInstance(), STAR, 0)
                .arg(0, MapType.ANY_MAP_TYPE, ONE | INS, null)
                .arg(1, BuiltInAtomicType.ANY_ATOMIC, ONE | ABS, null);

        SpecificFunctionType indexArg = new SpecificFunctionType(
                new SequenceType[]{SequenceType.SINGLE_ITEM},
                SequenceType.SINGLE_ATOMIC);

        register("index", 2, MapIndex.class, MapType.ANY_MAP_TYPE, ONE, 0)
                .arg(0, AnyItemType.getInstance(), STAR, null)
                .arg(1, indexArg, ONE, null);

        register("put", 3, MapPut.class, MapType.ANY_MAP_TYPE, ONE, 0)
                .arg(0, MapType.ANY_MAP_TYPE, ONE | INS, null)
                .arg(1, BuiltInAtomicType.ANY_ATOMIC, ONE | ABS, null)
                .arg(2, AnyItemType.getInstance(), STAR | NAV, null);

        register("contains", 2, MapContains.class, BuiltInAtomicType.BOOLEAN, ONE, 0)
                .arg(0, MapType.ANY_MAP_TYPE, ONE | INS, null)
                .arg(1, BuiltInAtomicType.ANY_ATOMIC, ONE | ABS, null);

        register("remove", 2, MapRemove.class, MapType.ANY_MAP_TYPE, ONE, 0)
                .arg(0, MapType.ANY_MAP_TYPE, ONE | INS, null)
                .arg(1, BuiltInAtomicType.ANY_ATOMIC, STAR | ABS, null);

        register("keys", 1, MapKeys.class, BuiltInAtomicType.ANY_ATOMIC, STAR, 0)
                .arg(0, MapType.ANY_MAP_TYPE, ONE | INS, null);

        register("size", 1, MapSize.class, BuiltInAtomicType.INTEGER, ONE, 0)
                .arg(0, MapType.ANY_MAP_TYPE, ONE | INS, null);

        ItemType actionType = new SpecificFunctionType(
                new SequenceType[]{SequenceType.SINGLE_ATOMIC, SequenceType.ANY_SEQUENCE},
                SequenceType.ANY_SEQUENCE);

        register("for-each", 2, MapForEach.class, AnyItemType.getInstance(), STAR, 0)
                .arg(0, MapType.ANY_MAP_TYPE, ONE | INS, null)
                .arg(1, actionType, ONE | INS, null);

        register("entries", 1, MapEntries.class, MapType.ANY_MAP_TYPE, STAR, 0)
                .arg(0, MapType.ANY_MAP_TYPE, ONE | INS, null);

        register("untyped-contains", 2, MapUntypedContains.class, BuiltInAtomicType.BOOLEAN, ONE, 0)
                .arg(0, MapType.ANY_MAP_TYPE, ONE | INS, null)
                .arg(1, BuiltInAtomicType.ANY_ATOMIC, ONE | ABS, null);


    }

    @Override
    public String getNamespace() {
        return NamespaceConstant.MAP_FUNCTIONS;
    }

    @Override
    public String getConventionalPrefix() {
        return "map";
    }


    /**
     * Implementation of the XPath 3.1 function map:contains(Map, key) => boolean
     */
    public static class MapContains extends SystemFunction {

        @Override
        public BooleanValue call(XPathContext context, Sequence[] arguments) throws XPathException {
            MapItem map = (MapItem) arguments[0].head();
            AtomicValue key = (AtomicValue) arguments[1].head();
            return BooleanValue.get(map.get(key) != null);
        }

    }

    /**
     * Implementation of the XPath 3.1 function map:get(Map, key) => value
     */
    public static class MapGet extends SystemFunction {

        String pendingWarning = null;

        /**
         * Method called during static type checking. This method may be implemented in subclasses so that functions
         * can take advantage of knowledge of the types of the arguments that will be supplied.
         *
         * @param visitor         an expression visitor, providing access to the static context and configuration
         * @param contextItemType information about whether the context item is set, and what its type is
         * @param arguments       the expressions appearing as arguments in the function call
         */
        @Override
        public void supplyTypeInformation(ExpressionVisitor visitor, ContextItemStaticInfo contextItemType, Expression[] arguments) throws XPathException {
            ItemType it = arguments[0].getItemType();
            if (it instanceof TupleType) {
                if (arguments[1] instanceof StringLiteral) {
                    String key = ((StringLiteral)arguments[1]).stringify();
                    if (((TupleType)it).getFieldType(key) == null) {
                        XPathException xe = new XPathException("Field " + key + " is not defined for tuple type " + it, "SXTT0001");
                        xe.setIsTypeError(true);
                        throw xe;
                    }
                }
                TypeHierarchy th = visitor.getConfiguration().getTypeHierarchy();
                Affinity relation = th.relationship(arguments[1].getItemType(), BuiltInAtomicType.STRING);
                if (relation == Affinity.DISJOINT) {
                    XPathException xe = new XPathException("Key for tuple type must be a string (actual type is " + arguments[1].getItemType(), "XPTY0004");
                    xe.setIsTypeError(true);
                    throw xe;
                }
            }
        }

        /**
         * Get the return type, given knowledge of the actual arguments
         *
         * @param args the actual arguments supplied
         * @return the best available item type that the function will return
         */
        @Override
        public ItemType getResultItemType(Expression[] args) {
            ItemType mapType = args[0].getItemType();
            if (mapType instanceof RecordTest && args[1] instanceof StringLiteral) {
                String key = ((StringLiteral) args[1]).stringify();
                RecordTest tit = (RecordTest) mapType;
                SequenceType valueType = tit.getFieldType(key);
                if (valueType == null) {
                    warning("Field " + key + " is not defined in tuple type");
                    return AnyItemType.getInstance();
                } else {
                    return valueType.getPrimaryType();
                }
            } else if (mapType instanceof MapType) {
                return ((MapType)mapType).getValueType().getPrimaryType();
            } else {
                return super.getResultItemType(args);
            }
        }

        /**
         * Get the cardinality, given knowledge of the actual arguments
         *
         * @param args the actual arguments supplied
         * @return the most precise available cardinality that the function will return
         */
        @Override
        public int getCardinality(Expression[] args) {
            ItemType mapType = args[0].getItemType();
            if (mapType instanceof RecordTest && args[1] instanceof StringLiteral) {
                String key = ((StringLiteral) args[1]).stringify();
                RecordTest tit = (RecordTest) mapType;
                SequenceType valueType = tit.getFieldType(key);
                if (valueType == null) {
                    warning("Field " + key + " is not defined in tuple type");
                    return StaticProperty.ALLOWS_MANY;
                } else {
                    return valueType.getCardinality();
                }
            } else if (mapType instanceof MapType) {
                return Cardinality.union(
                        ((MapType) mapType).getValueType().getCardinality(),
                        StaticProperty.ALLOWS_ZERO);
            } else {
                return super.getCardinality(args);
            }
        }

        /**
         * Allow the function to create an optimized call based on the values of the actual arguments
         *
         * @param visitor     the expression visitor
         * @param contextInfo information about the context item
         * @param arguments   the supplied arguments to the function call. Note: modifying the contents
         *                    of this array should not be attempted, it is likely to have no effect.
         * @return either a function call on this function, or an expression that delivers
         * the same result, or null indicating that no optimization has taken place
         * @throws XPathException if an error is detected
         */
        @Override
        public Expression makeOptimizedFunctionCall(ExpressionVisitor visitor, ContextItemStaticInfo contextInfo, Expression... arguments) throws XPathException {
            if (pendingWarning != null && !pendingWarning.equals("DONE")) {
                visitor.issueWarning(pendingWarning, arguments[0].getLocation());
                pendingWarning = "DONE";
            }
            return null;
        }

        private void warning(String message) {
            if (!"DONE".equals(pendingWarning)) {
                pendingWarning = message;
            }
        }

        @Override
        public Sequence call(XPathContext context, Sequence[] arguments) throws XPathException {
            MapItem map = (MapItem) arguments[0].head();
            assert map != null;
            AtomicValue key = (AtomicValue) arguments[1].head();
            Sequence value = map.get(key);
            if (value == null) {
                return EmptySequence.getInstance();
            } else {
                return value;
            }
        }

    }

    /**
     * Implementation of the XPath 3.1 function map:find(item()*, key) => array
     */
    public static class MapFind extends SystemFunction {

        @Override
        public ArrayItem call(XPathContext context, Sequence[] arguments) throws XPathException {
            List result = new ArrayList<>();
            AtomicValue key = (AtomicValue) arguments[1].head();
            processSequence(arguments[0], key, result);
            return new SimpleArrayItem(result);
        }

        private void processSequence(Sequence in, AtomicValue key, List result) throws XPathException {
            SequenceTool.supply(in.iterate(), (ItemConsumer) item -> {
                if (item instanceof ArrayItem) {
                    for (Sequence sequence : ((ArrayItem) item).members()) {
                        processSequence(sequence, key, result);
                    }
                } else if (item instanceof MapItem) {
                    GroundedValue value = ((MapItem) item).get(key);
                    if (value != null) {
                        result.add(value);
                    }
                    for (KeyValuePair entry : ((MapItem) item).keyValuePairs()) {
                        processSequence(entry.value, key, result);
                    }
                }
            });
        }

    }

    /**
     * Implementation of the extension function map:entry(key, value) => Map
     */
    public static class MapEntry extends SystemFunction {

        @Override
        public Sequence call(XPathContext context, Sequence[] arguments) throws XPathException {
            AtomicValue key = (AtomicValue) arguments[0].head();
            assert key != null;
            GroundedValue value = SequenceTool.toGroundedValue(arguments[1].iterate());
            return new SingleEntryMap(key, value);
        }

        /**
         * Get the return type, given knowledge of the actual arguments
         *
         * @param args the actual arguments supplied
         * @return the best available item type that the function will return
         */
        @Override
        public ItemType getResultItemType(Expression[] args) {
            PlainType ku = args[0].getItemType().getAtomizedItemType();
            AtomicType ka;
            if (ku instanceof AtomicType) {
                ka = (AtomicType)ku;
            } else {
                ka = ku.getPrimitiveItemType();
            }
            return new MapType(ka,
                               SequenceType.makeSequenceType(args[1].getItemType(), args[1].getCardinality()));
        }

        @Override
        public String getStreamerName() {
            return "MapEntry";
        }

    }

    /**
     * Implementation of the function map:for-each(Map, Function) => item()*
     */
    public static class MapForEach extends SystemFunction {

        @Override
        public Sequence call(XPathContext context, Sequence[] arguments) throws XPathException {
            MapItem map = (MapItem) arguments[0].head();
            Function fn = (Function) arguments[1].head();
            ZenoSequence results = new ZenoSequence();
            for (KeyValuePair pair : map.keyValuePairs()) {
                Sequence seq = dynamicCall(fn, context, new Sequence[]{pair.key, pair.value});
                GroundedValue val = seq.materialize();
                if (val.getLength() > 0) {
                    results = results.appendSequence(val);
                }
            }
            return results;
        }
    }

    /**
     * Implementation of the proposed 4.0 function map:entries(Map) => record(key, value*
     */
    public static class MapEntries extends SystemFunction {

        @Override
        public Sequence call(XPathContext context, Sequence[] arguments) throws XPathException {
            MapItem map = (MapItem) arguments[0].head();
            ZenoSequence results = new ZenoSequence();
            for (KeyValuePair pair : map.keyValuePairs()) {
                DictionaryMap kvp = new DictionaryMap(2);
                kvp.initialPut("key", pair.key);
                kvp.initialPut("value", pair.value);
                results = results.appendSequence(kvp);
            }
            return results;
        }
    }


    /**
     * Implementation of the proposed XPath 4.0 function
     * map:index($sequence, function(item()) as xs:anyAtomicType) => map(*)
     */
    public static class MapIndex extends SystemFunction {

        @Override
        public Sequence call(XPathContext context, Sequence[] arguments) throws XPathException {
            HashTrieMap map = new HashTrieMap();
            Function function = (Function)arguments[1];
            SequenceIterator iter = arguments[0].iterate();
            Item item;
            Sequence[] args = new Sequence[1];
            while ((item = iter.next()) != null) {
                args[0] = item;
                AtomicValue key = (AtomicValue)dynamicCall(function, context, args);
                map.initialPut(key, item);
            }
            return map;
        }
    }

    /**
     * Implementation of the XPath 3.1 function map:keys(Map) => atomicValue*
     */
    public static class MapKeys extends SystemFunction {

        @Override
        public Sequence call(XPathContext context, Sequence[] arguments) throws XPathException {
            MapItem map = (MapItem) arguments[0].head();
            assert map != null;
            return SequenceTool.toLazySequence(map.keys());
        }
    }

    /**
     * Implementation of the function map:merge() => Map
     * From 9.8, map:merge is also used to implement map constructors in XPath and the xsl:map
     * instruction in XSLT. For this purpose it accepts an additional option to define the error
     * code to be used to signal duplicates.
     */
    public static class MapMerge extends SystemFunction {

        public final static String finalKey = "Q{" + NamespaceConstant.SAXON + "}final";
        public final static String keyTypeKey ="Q{"+NamespaceConstant.SAXON +"}key-type";
        public final static String onDuplicatesKey = "Q{" + NamespaceConstant.SAXON + "}on-duplicates";
        public final static String errorCodeKey = "Q{" + NamespaceConstant.SAXON + "}duplicates-error-code";

        private String duplicates = "use-first";
        private String duplicatesErrorCode = "FOJS0003";
        private Function onDuplicates = null;
        private boolean allStringKeys = false;
        private boolean treatAsFinal = false;

        /**
         * Allow the function to create an optimized call based on the values of the actual arguments
         *
         * @param visitor     the expression visitor
         * @param contextInfo information about the context item
         * @param arguments   the supplied arguments to the function call. Note: modifying the contents
         *                    of this array should not be attempted, it is likely to have no effect.
         * @return either a function call on this function, or an expression that delivers
         * the same result, or null indicating that no optimization has taken place
         * @throws XPathException if an error is detected
         */
        @Override
        public Expression makeOptimizedFunctionCall(ExpressionVisitor visitor, ContextItemStaticInfo contextInfo, Expression... arguments) throws XPathException {
            if (arguments.length == 2 && arguments[1] instanceof Literal) {
                MapItem options = (MapItem) ((Literal)arguments[1]).getGroundedValue().head();
                Map values = getDetails().optionDetails.processSuppliedOptions(
                        options, visitor.getStaticContext().makeEarlyEvaluationContext());
                String duplicates = ((StringValue) values.get("duplicates")).getStringValue();
                String duplicatesErrorCode = ((StringValue) values.get(errorCodeKey)).getStringValue();
                Function onDuplicates = (Function)values.get(onDuplicatesKey);
                if (onDuplicates != null) {
                    duplicates = "use-callback";
                }

                boolean isFinal = ((BooleanValue) values.get(finalKey)).getBooleanValue();
                String keyType = ((StringValue) values.get(keyTypeKey)).getStringValue();
                MapMerge mm2 = (MapMerge)MapFunctionSet.getInstance().makeFunction("merge", 1);
                mm2.duplicates = duplicates;
                mm2.duplicatesErrorCode = duplicatesErrorCode;
                mm2.onDuplicates = onDuplicates;
                mm2.allStringKeys = keyType.equals("string");
                mm2.treatAsFinal = isFinal;
                return mm2.makeFunctionCall(arguments[0]);
            }
            return super.makeOptimizedFunctionCall(visitor, contextInfo, arguments);
        }

        /**
         * Get the return type, given knowledge of the actual arguments
         *
         * @param args the actual arguments supplied
         * @return the best available item type that the function will return
         */
        @Override
        public ItemType getResultItemType(Expression[] args) {
            ItemType it = args[0].getItemType();
            if (it == ErrorType.getInstance()) {
                return MapType.EMPTY_MAP_TYPE;
            } else if (it instanceof MapType) {
                boolean maybeCombined = true;  // see bug 3980
                if (args.length == 1) {
                    maybeCombined = false;
                } else if (args[1] instanceof Literal) {
                    MapItem options = (MapItem) ((Literal) args[1]).getGroundedValue().head();
                    GroundedValue dupes = options.get(StringValue.bmp("duplicates"));
                    try {
                        if (dupes != null && !"combine".equals(dupes.getStringValue())) {
                            maybeCombined = false;
                        }
                    } catch (XPathException e) {
                        //
                    }
                }
                if (maybeCombined) {
                    return new MapType(((MapType) it).getKeyType(),
                                       SequenceType.makeSequenceType(((MapType) it).getValueType().getPrimaryType(), StaticProperty.ALLOWS_ZERO_OR_MORE));
                } else {
                    return it;
                }
            } else {
                return super.getResultItemType(args);
            }
        }

        @Override
        public MapItem call(XPathContext context, Sequence[] arguments) throws XPathException {
            String duplicates = this.duplicates;
            String duplicatesErrorCode = this.duplicatesErrorCode;
            boolean allStringKeys = this.allStringKeys;
            boolean treatAsFinal = this.treatAsFinal;
            Function onDuplicates = this.onDuplicates;
            if (arguments.length > 1) {
                MapItem options = (MapItem) arguments[1].head();
                Map values = getDetails().optionDetails.processSuppliedOptions(options, context);
                duplicates = ((StringValue) values.get("duplicates")).getStringValue();
                duplicatesErrorCode = ((StringValue) values.get(errorCodeKey)).getStringValue();
                treatAsFinal = ((BooleanValue) values.get(finalKey)).getBooleanValue();
                allStringKeys = "string".equals(((StringValue)values.get(keyTypeKey)).getStringValue());
                onDuplicates = (Function) values.get(onDuplicatesKey);
                if (onDuplicates != null) {
                    duplicates = "use-callback";
                }
            }

            if (treatAsFinal && allStringKeys) {
                // Optimize for a map with string-valued keys that's unlikely to be modified
                SequenceIterator iter = arguments[0].iterate();
                DictionaryMap baseMap = new DictionaryMap();
                MapItem next;
                switch (duplicates) {
                    // Code is structured (a) to avoid testing "duplicates" within the loop unnecessarily,
                    // and (b) to avoid the "get" operation to look for duplicates when it's not needed.
                    case "unspecified":
                    case "use-any":
                    case "use-last":
                        while ((next = (MapItem) iter.next()) != null) {
                            for (KeyValuePair pair : next.keyValuePairs()) {
                                if (!(pair.key instanceof StringValue)) {
                                    throw new XPathException("The keys in this map must all be strings (found " + pair.key.getItemType() + ")");
                                }
                                baseMap.initialPut(pair.key.getStringValue(), pair.value);
                            }
                        }
                        return baseMap;
                    default:
                        while ((next = (MapItem) iter.next()) != null) {
                            for (KeyValuePair pair : next.keyValuePairs()) {
                                if (!(pair.key instanceof StringValue)) {
                                    throw new XPathException("The keys in this map must all be strings (found " + pair.key.getItemType() + ")");
                                }
                                Sequence existing = baseMap.get(pair.key);
                                if (existing != null) {
                                    switch (duplicates) {
                                        case "use-first":
                                            // no action
                                            break;
                                        case "combine":
                                            InsertBefore.InsertIterator combinedIter =
                                                    new InsertBefore.InsertIterator(pair.value.iterate(), existing.iterate(), 1);
                                            GroundedValue combinedValue = SequenceTool.toGroundedValue(combinedIter);
                                            baseMap.initialPut(pair.key.getStringValue(), combinedValue);
                                            break;
                                        case "use-callback":
                                            Sequence[] args = new Sequence[]{existing, pair.value};
                                            Sequence combined = onDuplicates.call(context, args);
                                            baseMap.initialPut(pair.key.getStringValue(), combined.materialize());
                                            break;
                                        default:
                                            throw new XPathException("Duplicate key in constructed map: " +
                                                                             Err.wrap(pair.key.getStringValue()), duplicatesErrorCode);
                                    }
                                } else {
                                    baseMap.initialPut(pair.key.getStringValue(), pair.value);
                                }
                            }
                        }
                        return baseMap;
                }
            } else {
                SequenceIterator iter = arguments[0].iterate();
                MapItem baseMap = (MapItem) iter.next();
                if (baseMap == null) {
                    return new HashTrieMap();
                } else {
                    MapItem next;
                    while ((next = (MapItem) iter.next()) != null) {
                        // Merge the next map and the base map. Merge the smaller of the two
                        // maps into the larger. The complication is that this affects duplicates handling.
                        // See bug #4865
                        boolean inverse = next.size() > baseMap.size();
                        MapItem larger = inverse ? next : baseMap;
                        MapItem smaller = inverse ? baseMap : next;
                        String dup = inverse ? invertDuplicates(duplicates) : duplicates;
                        for (KeyValuePair pair : smaller.keyValuePairs()) {
                            Sequence existing = larger.get(pair.key);
                            if (existing != null) {
                                switch (dup) {
                                    case "use-first":
                                    case "unspecified":
                                    case "use-any":
                                        // no action
                                        break;
                                    case "use-last":
                                        larger = larger.addEntry(pair.key, pair.value);
                                        break;
                                    case "combine": {
                                        InsertBefore.InsertIterator combinedIter =
                                                new InsertBefore.InsertIterator(pair.value.iterate(), existing.iterate(), 1);
                                        GroundedValue combinedValue = SequenceTool.toGroundedValue(combinedIter);
                                        larger = larger.addEntry(pair.key, combinedValue);
                                        break;
                                    }
                                    case "combine-reverse": {
                                        InsertBefore.InsertIterator combinedIter =
                                                new InsertBefore.InsertIterator(existing.iterate(), pair.value.iterate(), 1);
                                        GroundedValue combinedValue = SequenceTool.toGroundedValue(combinedIter);
                                        larger = larger.addEntry(pair.key, combinedValue);
                                        break;
                                    }
                                    case "use-callback":
                                        assert onDuplicates != null;
                                        Sequence[] args;
                                        if (inverse) {
                                            args = onDuplicates.getArity() == 2 ?
                                                    new Sequence[]{pair.value, existing} :
                                                    new Sequence[]{pair.value, existing, pair.key};
                                        } else {
                                            args = onDuplicates.getArity() == 2 ?
                                                    new Sequence[]{existing, pair.value} :
                                                    new Sequence[]{existing, pair.value, pair.key};
                                        }
                                        Sequence combined = onDuplicates.call(context, args);
                                        larger = larger.addEntry(pair.key, combined.materialize());
                                        break;
                                    default:
                                        throw new XPathException("Duplicate key in constructed map: " +
                                                                         Err.wrap(pair.key.getStringValue()), duplicatesErrorCode);
                                }
                            } else {
                                larger = larger.addEntry(pair.key, pair.value);
                            }
                        }
                        baseMap = larger;
                    }
                    return baseMap;
                }
            }

        }

        private String invertDuplicates(String duplicates) {
            switch (duplicates) {
                case "use-first":
                case "unspecified":
                case "use-any":
                    return "use-last";
                case "use-last":
                    return "use-first";
                case "combine":
                    return "combine-reverse";
                case "combine-reverse":
                    return "combine";
                default:
                    return duplicates;
            }
        }

        @Override
        public String getStreamerName() {
            return "NewMap";
        }

        /**
         * Export any implicit arguments held in optimized form within the SystemFunction call
         *
         * @param out the export destination
         */
        @Override
        public void exportAdditionalArguments(SystemFunctionCall call, ExpressionPresenter out) throws XPathException {
            if (call.getArity() == 1) {
                HashTrieMap options = new HashTrieMap();
                options.initialPut(StringValue.bmp("duplicates"), new StringValue(duplicates));
                options.initialPut(StringValue.bmp("duplicates-error-code"), new StringValue(duplicatesErrorCode));
                Literal.exportValue(options, out);
            }
        }
    }

    /**
     * Implementation of the extension function map:put() => Map
     */

    public static class MapPut extends SystemFunction {

        @Override
        public MapItem call(XPathContext context, Sequence[] arguments) throws XPathException {

            MapItem baseMap = (MapItem) arguments[0].head();

            if (!(baseMap instanceof HashTrieMap)) {
                baseMap = HashTrieMap.copy(baseMap);
            }

            AtomicValue key = (AtomicValue) arguments[1].head();
            GroundedValue value = arguments[2].materialize();
            return baseMap.addEntry(key, value);
        }
    }


    /**
     * Implementation of the XPath 3.1 function map:remove(Map, key) => value
     */
    public static class MapRemove extends SystemFunction {

        @Override
        public MapItem call(XPathContext context, Sequence[] arguments) throws XPathException {
            MapItem map = (MapItem) arguments[0].head();
            SequenceIterator iter = arguments[1].iterate();
            AtomicValue key;
            while ((key = (AtomicValue) iter.next()) != null) {
                map = map.remove(key);
            }
            return map;
        }

    }

    /**
     * Implementation of the extension function map:size(map) => integer
     */
    public static class MapSize extends SystemFunction {

        @Override
        public IntegerValue call(XPathContext context, Sequence[] arguments) throws XPathException {
            MapItem map = (MapItem) arguments[0].head();
            return new Int64Value(map.size());
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy