diff options
author | Vlad Dumitrescu <[email protected]> | 2014-02-11 14:17:07 +0100 |
---|---|---|
committer | Vlad Dumitrescu <[email protected]> | 2014-02-11 19:48:53 +0100 |
commit | 2b4582d2154b157092fc05c387655e5426ed7d8e (patch) | |
tree | 3225373219ff8ffe7450d4e3ad39290482eafe77 | |
parent | d7d240bc3d03cf708f6feaf20efac1addcd76242 (diff) | |
download | otp-2b4582d2154b157092fc05c387655e5426ed7d8e.tar.gz otp-2b4582d2154b157092fc05c387655e5426ed7d8e.tar.bz2 otp-2b4582d2154b157092fc05c387655e5426ed7d8e.zip |
jinterface: implement support for maps
The API and implementation are simplistic, like for lists and tuples,
using arrays and without any connection to java.util.Map.
8 files changed, 443 insertions, 1 deletions
diff --git a/lib/jinterface/java_src/com/ericsson/otp/erlang/OtpErlangMap.java b/lib/jinterface/java_src/com/ericsson/otp/erlang/OtpErlangMap.java new file mode 100644 index 0000000000..7c1cf84e98 --- /dev/null +++ b/lib/jinterface/java_src/com/ericsson/otp/erlang/OtpErlangMap.java @@ -0,0 +1,293 @@ +/* + * %CopyrightBegin% + * + * Copyright Ericsson AB 2000-2013. All Rights Reserved. + * + * The contents of this file are subject to the Erlang Public License, + * Version 1.1, (the "License"); you may not use this file except in + * compliance with the License. You should have received a copy of the + * Erlang Public License along with this software. If not, it can be + * retrieved online at http://www.erlang.org/. + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * %CopyrightEnd% + */ +package com.ericsson.otp.erlang; + +import java.io.Serializable; + +/** + * Provides a Java representation of Erlang maps. Maps are created from one or + * more arbitrary Erlang terms. + * + * <p> + * The arity of the map is the number of elements it contains. The keys and + * values can be retrieved as arrays and the value for a key can be queried. + * + */ +public class OtpErlangMap extends OtpErlangObject implements Serializable, + Cloneable { + // don't change this! + private static final long serialVersionUID = -6410770117696198497L; + + private static final OtpErlangObject[] NO_ELEMENTS = new OtpErlangObject[0]; + + private OtpErlangObject[] keys = NO_ELEMENTS; + private OtpErlangObject[] values = NO_ELEMENTS; + + /** + * Create a map from an array of keys and an array of values. + * + * @param keys + * the array of terms to create the map keys from. + * @param values + * the array of terms to create the map values from. + * + * @exception java.lang.IllegalArgumentException + * if any array is empty (null) or contains null elements. + */ + public OtpErlangMap(final OtpErlangObject[] keys, + final OtpErlangObject[] values) { + this(keys, 0, keys.length, values, 0, values.length); + } + + /** + * Create a map from an array of terms. + * + * @param elems + * the array of terms to create the map from. + * @param start + * the offset of the first term to insert. + * @param vcount + * the number of terms to insert. + * + * @exception java.lang.IllegalArgumentException + * if any array is empty (null) or contains null elements. + */ + public OtpErlangMap(final OtpErlangObject[] keys, final int kstart, + final int kcount, final OtpErlangObject[] values, final int vstart, + final int vcount) { + if (keys == null || values == null) { + throw new java.lang.IllegalArgumentException( + "Map content can't be null"); + } else if (kcount != vcount) { + throw new java.lang.IllegalArgumentException( + "Map keys and values must have same arity"); + } else if (vcount < 1) { + this.keys = NO_ELEMENTS; + this.values = NO_ELEMENTS; + } else { + this.keys = new OtpErlangObject[vcount]; + for (int i = 0; i < vcount; i++) { + if (keys[kstart + i] != null) { + this.keys[i] = keys[kstart + i]; + } else { + throw new java.lang.IllegalArgumentException( + "Map key cannot be null (element" + (kstart + i) + + ")"); + } + } + this.values = new OtpErlangObject[vcount]; + for (int i = 0; i < vcount; i++) { + if (values[vstart + i] != null) { + this.values[i] = values[vstart + i]; + } else { + throw new java.lang.IllegalArgumentException( + "Map value cannot be null (element" + (vstart + i) + + ")"); + } + } + } + } + + /** + * Create a map from a stream containing a map encoded in Erlang external + * format. + * + * @param buf + * the stream containing the encoded map. + * + * @exception OtpErlangDecodeException + * if the buffer does not contain a valid external + * representation of an Erlang map. + */ + public OtpErlangMap(final OtpInputStream buf) + throws OtpErlangDecodeException { + final int arity = buf.read_map_head(); + + if (arity > 0) { + keys = new OtpErlangObject[arity]; + values = new OtpErlangObject[arity]; + + for (int i = 0; i < arity; i++) { + keys[i] = buf.read_any(); + } + for (int i = 0; i < arity; i++) { + values[i] = buf.read_any(); + } + } else { + keys = NO_ELEMENTS; + values = NO_ELEMENTS; + } + } + + /** + * Get the arity of the map. + * + * @return the number of elements contained in the map. + */ + public int arity() { + return keys.length; + } + + /** + * Get the specified value from the map. + * + * @param key + * the key of the requested value. + * + * @return the requested value, of null if key is not a valid key. + */ + public OtpErlangObject get(final OtpErlangObject key) { + if (key == null) { + return null; + } + for (int i = 0; i < keys.length; i++) { + if (key.equals(keys[i])) { + return values[i]; + } + } + return null; + } + + /** + * Get all the keys from the map as an array. + * + * @return an array containing all of the map's keys. + */ + public OtpErlangObject[] keys() { + final OtpErlangObject[] res = new OtpErlangObject[arity()]; + System.arraycopy(keys, 0, res, 0, res.length); + return res; + } + + /** + * Get all the values from the map as an array. + * + * @return an array containing all of the map's values. + */ + public OtpErlangObject[] values() { + final OtpErlangObject[] res = new OtpErlangObject[arity()]; + System.arraycopy(values, 0, res, 0, res.length); + return res; + } + + /** + * Get the string representation of the map. + * + * @return the string representation of the map. + */ + @Override + public String toString() { + int i; + final StringBuffer s = new StringBuffer(); + final int arity = values.length; + + s.append("#{"); + + for (i = 0; i < arity; i++) { + if (i > 0) { + s.append(","); + } + s.append(keys[i].toString()); + s.append(" => "); + s.append(values[i].toString()); + } + + s.append("}"); + + return s.toString(); + } + + /** + * Convert this map to the equivalent Erlang external representation. + * + * @param buf + * an output stream to which the encoded map should be written. + */ + @Override + public void encode(final OtpOutputStream buf) { + final int arity = values.length; + + buf.write_map_head(arity); + + for (int i = 0; i < arity; i++) { + buf.write_any(keys[i]); + } + for (int i = 0; i < arity; i++) { + buf.write_any(values[i]); + } + } + + /** + * Determine if two maps are equal. Maps are equal if they have the same + * arity and all of the elements are equal. + * + * @param o + * the map to compare to. + * + * @return true if the maps have the same arity and all the elements are + * equal. + */ + @Override + public boolean equals(final Object o) { + if (!(o instanceof OtpErlangMap)) { + return false; + } + + final OtpErlangMap t = (OtpErlangMap) o; + final int a = arity(); + + if (a != t.arity()) { + return false; + } + + for (int i = 0; i < a; i++) { + if (!keys[i].equals(t.keys[i])) { + return false; // early exit + } + } + for (int i = 0; i < a; i++) { + if (!values[i].equals(t.values[i])) { + return false; // early exit + } + } + + return true; + } + + @Override + protected int doHashCode() { + final OtpErlangObject.Hash hash = new OtpErlangObject.Hash(9); + final int a = arity(); + hash.combine(a); + for (int i = 0; i < a; i++) { + hash.combine(keys[i].hashCode()); + } + for (int i = 0; i < a; i++) { + hash.combine(values[i].hashCode()); + } + return hash.valueOf(); + } + + @Override + public Object clone() { + final OtpErlangMap newMap = (OtpErlangMap) super.clone(); + newMap.values = values.clone(); + return newMap; + } +} diff --git a/lib/jinterface/java_src/com/ericsson/otp/erlang/OtpExternal.java b/lib/jinterface/java_src/com/ericsson/otp/erlang/OtpExternal.java index 45a82d6c94..fa0fe18e95 100644 --- a/lib/jinterface/java_src/com/ericsson/otp/erlang/OtpExternal.java +++ b/lib/jinterface/java_src/com/ericsson/otp/erlang/OtpExternal.java @@ -85,6 +85,9 @@ public class OtpExternal { /** The tag used for new style references */ public static final int newRefTag = 114; + /** The tag used for maps */ + public static final int mapTag = 116; + /** The tag used for old Funs */ public static final int funTag = 117; diff --git a/lib/jinterface/java_src/com/ericsson/otp/erlang/OtpInputStream.java b/lib/jinterface/java_src/com/ericsson/otp/erlang/OtpInputStream.java index 9dc1728346..0d1342d796 100644 --- a/lib/jinterface/java_src/com/ericsson/otp/erlang/OtpInputStream.java +++ b/lib/jinterface/java_src/com/ericsson/otp/erlang/OtpInputStream.java @@ -1202,6 +1202,9 @@ public class OtpInputStream extends ByteArrayInputStream { case OtpExternal.newRefTag: return new OtpErlangRef(this); + case OtpExternal.mapTag: + return new OtpErlangMap(this); + case OtpExternal.portTag: return new OtpErlangPort(this); @@ -1244,4 +1247,21 @@ public class OtpInputStream extends ByteArrayInputStream { throw new OtpErlangDecodeException("Uknown data type: " + tag); } } + + public int read_map_head() throws OtpErlangDecodeException { + int arity = 0; + final int tag = read1skip_version(); + + // decode the map header and get arity + switch (tag) { + case OtpExternal.mapTag: + arity = read4BE(); + break; + + default: + throw new OtpErlangDecodeException("Not valid map tag: " + tag); + } + + return arity; + } } diff --git a/lib/jinterface/java_src/com/ericsson/otp/erlang/OtpOutputStream.java b/lib/jinterface/java_src/com/ericsson/otp/erlang/OtpOutputStream.java index 78f47aa32f..a78423db44 100644 --- a/lib/jinterface/java_src/com/ericsson/otp/erlang/OtpOutputStream.java +++ b/lib/jinterface/java_src/com/ericsson/otp/erlang/OtpOutputStream.java @@ -974,4 +974,9 @@ public class OtpOutputStream extends ByteArrayOutputStream { write_atom(function); write_long(arity); } + + public void write_map_head(final int arity) { + write1(OtpExternal.mapTag); + write4BE(arity); + } } diff --git a/lib/jinterface/java_src/com/ericsson/otp/erlang/java_files b/lib/jinterface/java_src/com/ericsson/otp/erlang/java_files index 1390542194..62fa7f990e 100644 --- a/lib/jinterface/java_src/com/ericsson/otp/erlang/java_files +++ b/lib/jinterface/java_src/com/ericsson/otp/erlang/java_files @@ -74,6 +74,7 @@ ERL = \ OtpErlangShort\ OtpErlangString\ OtpErlangTuple \ + OtpErlangMap \ OtpErlangUInt \ OtpErlangUShort diff --git a/lib/jinterface/test/jinterface_SUITE.erl b/lib/jinterface/test/jinterface_SUITE.erl index de8d611efc..4a6505c597 100644 --- a/lib/jinterface/test/jinterface_SUITE.erl +++ b/lib/jinterface/test/jinterface_SUITE.erl @@ -675,6 +675,17 @@ status_handler_connAttempt(Config) when is_list(Config) -> "NodeStatusHandler", [erlang:get_cookie(),node(),?status_handler_connAttempt]). +%%%----------------------------------------------------------------- +maps(doc) -> + ["Maps.java: " + "Tests OtpErlangMap encoding, decoding, toString, get"]; +maps(suite) -> + []; +maps(Config) when is_list(Config) -> + ok = jitu:java(?config(java, Config), + ?config(data_dir, Config), + "Maps", + []). %%%----------------------------------------------------------------- %%% INTERNAL FUNCTIONS diff --git a/lib/jinterface/test/jinterface_SUITE_data/Makefile.src b/lib/jinterface/test/jinterface_SUITE_data/Makefile.src index 2a3dca463b..a15ed1aa63 100644 --- a/lib/jinterface/test/jinterface_SUITE_data/Makefile.src +++ b/lib/jinterface/test/jinterface_SUITE_data/Makefile.src @@ -46,7 +46,8 @@ JAVA_FILES = \ MboxPing.java \ MboxSendReceive.java \ MboxLinkUnlink.java \ - NodeStatusHandler.java + NodeStatusHandler.java \ + Maps.java CLASS_FILES = $(JAVA_FILES:.java=.class) diff --git a/lib/jinterface/test/jinterface_SUITE_data/Maps.java b/lib/jinterface/test/jinterface_SUITE_data/Maps.java new file mode 100644 index 0000000000..136a665f23 --- /dev/null +++ b/lib/jinterface/test/jinterface_SUITE_data/Maps.java @@ -0,0 +1,108 @@ +import java.util.Arrays; + +import com.ericsson.otp.erlang.OtpErlangAtom; +import com.ericsson.otp.erlang.OtpErlangDecodeException; +import com.ericsson.otp.erlang.OtpErlangList; +import com.ericsson.otp.erlang.OtpErlangLong; +import com.ericsson.otp.erlang.OtpErlangMap; +import com.ericsson.otp.erlang.OtpInputStream; +import com.ericsson.otp.erlang.OtpOutputStream; + +/* + * %CopyrightBegin% + * + * Copyright Ericsson AB 2004-2010. All Rights Reserved. + * + * The contents of this file are subject to the Erlang Public License, + * Version 1.1, (the "License"); you may not use this file except in + * compliance with the License. You should have received a copy of the + * Erlang Public License along with this software. If not, it can be + * retrieved online at http://www.erlang.org/. + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * %CopyrightEnd% + */ + +class Maps { + + /* + * Implements test case jinterface_SUITE:maps/1 + * + * Test the class OtpErlangMap + */ + + @SuppressWarnings("resource") + public static void main(final String argv[]) { + + runTest(new byte[] { (byte) 131, 116, 0, 0, 0, 0 }, "#{}", 1); + runTest(new byte[] { (byte) 131, 116, 0, 0, 0, 1, 100, 0, 1, 97, 100, + 0, 1, 98 }, "#{a => b}", 2); + // make sure keys are sorted here, jinterface doesn't reorder them + runTest(new byte[] { (byte) 131, 116, 0, 0, 0, 2, 97, 2, 100, 0, 1, 97, + 106, 97, 1 }, "#{2 => [],a => 1}", 3); + runTest(new byte[] { (byte) 131, 116, 0, 0, 0, 1, 104, 1, 97, 3, 108, + 0, 0, 0, 1, 100, 0, 1, 114, 106 }, "#{{3} => [r]}", 4); + + try { + // #{2 => [],a => 1} + final OtpErlangMap map = new OtpErlangMap(new OtpInputStream( + new byte[] { (byte) 131, 116, 0, 0, 0, 2, 97, 2, 100, 0, 1, + 97, 106, 97, 1 })); + + if (map.arity() != 2) { + fail(5); + } + if (!new OtpErlangLong(1).equals(map.get(new OtpErlangAtom("a")))) { + fail(6); + } + if (!new OtpErlangList().equals(map.get(new OtpErlangLong(2)))) { + fail(7); + } + if (map.get(new OtpErlangLong(1)) != null) { + fail(8); + } + } catch (final OtpErlangDecodeException e) { + fail(99); + } + + } + + @SuppressWarnings("resource") + private static void runTest(final byte[] in, final String out, final int err) { + try { + final OtpInputStream is = new OtpInputStream(in); + + final OtpErlangMap map = new OtpErlangMap(is); + final String output = map.toString(); + if (!output.equals(out)) { + fail("toString mismatch " + output + " <> " + out, err); + } + + final OtpOutputStream os = new OtpOutputStream(map); + final byte[] outArray0 = os.toByteArray(); + final byte[] outArray = new byte[outArray0.length + 1]; + System.arraycopy(outArray0, 0, outArray, 1, outArray0.length); + outArray[0] = (byte) 131; + if (!Arrays.equals(in, outArray)) { + fail("encode error " + Arrays.toString(outArray), err); + } + } catch (final OtpErlangDecodeException e) { + fail("decode error " + e.getMessage(), err); + } catch (final Exception e) { + fail("error " + e.getMessage(), err); + } + } + + private static void fail(final int reason) { + System.exit(reason); + } + + private static void fail(final String str, final int reason) { + System.out.println(str); + System.exit(reason); + } +} |