package org.basex.query.item.map; import static org.basex.query.QueryText.*; import static org.basex.query.util.Err.*; import java.io.IOException; import java.util.HashMap; import org.basex.io.serial.Serializer; import org.basex.query.QueryContext; import org.basex.query.QueryException; import org.basex.query.item.AtomType; import org.basex.query.item.Bln; import org.basex.query.item.Dbl; import org.basex.query.item.Empty; import org.basex.query.item.FItem; import org.basex.query.item.Flt; import org.basex.query.item.FuncType; import org.basex.query.item.Item; import org.basex.query.item.Int; import org.basex.query.item.MapType; import org.basex.query.item.QNm; import org.basex.query.item.SeqType; import org.basex.query.item.Str; import org.basex.query.item.Type; import org.basex.query.item.Value; import org.basex.query.iter.ItemCache; import org.basex.query.iter.ValueIter; import org.basex.util.InputInfo; import org.basex.util.Token; import org.basex.util.Util; import org.basex.util.hash.TokenObjMap; /** * The map item. * * @author BaseX Team 2005-12, BSD License * @author Leo Woerteler */ public final class Map extends FItem { /** The empty map. */ public static final Map EMPTY = new Map(TrieNode.EMPTY); /** Number of bits per level, maximum is 5 because {@code 1 << 5 == 32}. */ static final int BITS = 5; /** Wrapped immutable map. */ private final TrieNode root; /** Key sequence. */ private Value keys; /** Size. */ private Int size; /** * Constructor. * @param m map */ private Map(final TrieNode m) { super(SeqType.ANY_MAP); root = m; } @Override public int arity() { return 1; } @Override public QNm fName() { return null; } @Override public Value invValue(final QueryContext ctx, final InputInfo ii, final Value... args) throws QueryException { return get(args[0].item(ctx, ii), ii); } /** * Checks the key item. * @param it item * @param ii input info * @return possibly atomized item if non {@code NaN}, {@code null} otherwise * @throws QueryException query exception */ private Item key(final Item it, final InputInfo ii) throws QueryException { // no empty sequence allowed if(it == null) throw XPEMPTY.thrw(ii, description()); // function items can't be keys if(it instanceof FItem) throw FNATM.thrw(ii, it.description()); // NaN can't be stored as key, as it isn't equal to anything if(it == Flt.NAN || it == Dbl.NAN) return null; // untyped items are converted to strings return it.type.isUntyped() ? Str.get(it.string(ii)) : it; } /** * Deletes a key from this map. * @param key key to delete * @param ii input info * @return updated map if changed, {@code this} otherwise * @throws QueryException query exception */ public Map delete(final Item key, final InputInfo ii) throws QueryException { final Item k = key(key, ii); if(k == null) return this; final TrieNode del = root.delete(k.hash(ii), k, 0, ii); return del == root ? this : del != null ? new Map(del) : EMPTY; } /** * Gets the value from this map. * @param key key to look for * @param ii input info * @return bound value if found, the empty sequence {@code ()} otherwise * @throws QueryException query exception */ public Value get(final Item key, final InputInfo ii) throws QueryException { final Item k = key(key, ii); if(k == null) return Empty.SEQ; final Value val = root.get(k.hash(ii), k, 0, ii); return val == null ? Empty.SEQ : val; } /** * Checks if the given key exists in the map. * @param k key to look for * @param ii input info * @return {@code true()}, if the key exists, {@code false()} otherwise * @throws QueryException query exception */ public Bln contains(final Item k, final InputInfo ii) throws QueryException { final Item key = key(k, ii); return Bln.get(key != null && root.contains(key.hash(ii), key, 0, ii)); } /** * Adds all bindings from the given map into {@code this}. * @param other map to add * @param ii input info * @return updated map if changed, {@code this} otherwise * @throws QueryException query exception */ public Map addAll(final Map other, final InputInfo ii) throws QueryException { if(other == EMPTY) return this; final TrieNode upd = root.addAll(other.root, 0, ii); return upd == other.root ? other : new Map(upd); } /** * Checks if the map has the given type. * @param t type * @return {@code true} if the type fits, {@code false} otherwise */ public boolean hasType(final MapType t) { return root.hasType(t.keyType == AtomType.AAT ? null : t.keyType, t.ret.eq(SeqType.ITEM_ZM) ? null : t.ret); } @Override public Map coerceTo(final FuncType ft, final QueryContext ctx, final InputInfo ii) throws QueryException { if(!(ft instanceof MapType) || !hasType((MapType) ft)) throw cast(ii, ft, this); return this; } /** * Inserts the given value into this map. * @param k key to insert * @param v value to insert * @param ii input info * @return updated map if changed, {@code this} otherwise * @throws QueryException query exception */ public Map insert(final Item k, final Value v, final InputInfo ii) throws QueryException { final Item key = key(k, ii); if(key == null) return this; final TrieNode ins = root.insert(key.hash(ii), key, v, 0, ii); return ins == root ? this : new Map(ins); } /** * Number of values contained in this map. * @return size */ public Int mapSize() { if(size == null) size = Int.get(root.size); return size; } /** * All keys defined in this map. * @return list of keys */ public Value keys() { if(keys == null) { final ItemCache res = new ItemCache(root.size); root.keys(res); keys = res.value(); } return keys; } /** * Collation of this map. * @return collation */ public Str collation() { return Str.get(URLCOLL); } /** * Checks if the this map is deep-equal to the given one. * @param ii input info * @param o other map * @return result of check * @throws QueryException query exception */ public boolean deep(final InputInfo ii, final Map o) throws QueryException { return root.deep(ii, o.root); } /** * Converts the map to a map with tokens as keys and java objects as values. * @param ii input info * @return token map * @throws QueryException query exception */ public TokenObjMap<Object> tokenJavaMap(final InputInfo ii) throws QueryException { final TokenObjMap<Object> tm = new TokenObjMap<Object>(); final ValueIter vi = keys().iter(); for(Item k; (k = vi.next()) != null;) { final Type kt = k.type; if(!kt.isString()) FUNCMP.thrw(ii, description(), AtomType.STR, kt); tm.add(k.string(null), get(k, ii).toJava()); } return tm; } @Override public HashMap<Object, Object> toJava() throws QueryException { final HashMap<Object, Object> map = new HashMap<Object, Object>(); final ValueIter vi = keys().iter(); for(Item k; (k = vi.next()) != null;) { map.put(k.toJava(), get(k, null).toJava()); } return map; } @Override public int hash(final InputInfo ii) throws QueryException { return root.hash(ii); } @Override public String description() { return MAPSTR + BRACE1 + DOTS + BRACE2; } @Override public void plan(final Serializer ser) throws IOException { final long s = mapSize().itr(null); ser.openElement(MAP, SIZE, Token.token(s)); final Value ks = keys(); try { for(long i = 0, max = Math.min(s, 5); i < max; i++) { final Item key = ks.itemAt(i); final Value val = get(key, null); ser.openElement(ENTRY, KEY, key.string(null)); val.plan(ser); ser.closeElement(); } } catch(final QueryException ex) { Util.notexpected(ex); } ser.closeElement(); } @Override public String toString() { final StringBuilder sb = root.toString(new StringBuilder("map{ ")); // remove superfluous comma if(root.size > 0) sb.deleteCharAt(sb.length() - 2); return sb.append('}').toString(); } }