From 4fbaf8865a330341b4e856e26e00a2f761d835a7 Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Wed, 18 Jun 2014 16:43:18 +0930 Subject: [PATCH 1/6] Improve test framework: create_file create_file '-' sends to standard output create_file --label=LABEL create_file passes other options to tfw_createfile --- testframework.sh | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/testframework.sh b/testframework.sh index 6d4e2287..f3567360 100644 --- a/testframework.sh +++ b/testframework.sh @@ -2099,19 +2099,37 @@ tfw_multicolumn() { } # Create a file with the given size (default 0). -# Usage: create_file [--append] [] -# where: is of the form Nu +# Usage: create_file [--append] [create_file opts] [--] [] +# where: if is - then writes to standard output +# is of the form Nu # N is decimal integer # u is one of kKmMgG (k=10^3, K=2^10, m=10^6, M=2^20, g=10^9, G=2^30) create_file() { local args=("$@") - case "$1" in - --append) shift;; - *) rm -f "$1";; - esac - local path="$1" + local opt_append=false + local opt_label= + local opts=() + while [ $# -ne 0 ]; do + case "$1" in + --) shift; break;; + --append) opt_append=true; shift;; + --label=*) opt_label="${1#*=}"; shift;; + --*) opts+=("$1"); shift;; + *) break;; + esac + done + local path="${1?}" local size="$2" - tfw_createfile --label="$path" ${size:+--size=$size} >>"$path" || error "failed command: create_file ${args[*]}" + case "$path" in + -) + tfw_createfile ${opt_label:+--label="$opt_label"} "${opts[@]}" ${size:+--size=$size} + ;; + *) + [ -z "$opt_label" ] && opt_label="$path" + tfw_createfile ${opt_label:+--label="$opt_label"} "${opts[@]}" ${size:+--size=$size} >>"$path" + ;; + esac + [ $? -eq 0 ] || error "failed command: create_file ${args[*]}" } # Add quotations to the given arguments to allow them to be expanded intact From f2772b0ce8ac92e8f45b0110a2b7fed38400cae8 Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Wed, 11 Jun 2014 17:20:47 +0930 Subject: [PATCH 2/6] Refactor tests: testdefs_java.sh, testdefs_meshms.sh meshms_add_messages() now puts message texts into TEXT[$n] array instead of "text$n" files --- testdefs.sh | 10 +----- testdefs_java.sh | 58 +++++++++++++++++++++++++++++++ testdefs_meshms.sh | 71 +++++++++++++++++++++++++++++++++++++ testdefs_rhizome.sh | 4 +-- tests/jni | 40 ++++++++------------- tests/rhizomehttp | 85 +++++++++++++-------------------------------- 6 files changed, 171 insertions(+), 97 deletions(-) create mode 100644 testdefs_java.sh create mode 100644 testdefs_meshms.sh diff --git a/testdefs.sh b/testdefs.sh index 4a47787c..1924b41b 100644 --- a/testdefs.sh +++ b/testdefs.sh @@ -1,5 +1,5 @@ # Common definitions for all test suites. -# Copyright 2012 The Serval Project, Inc. +# Copyright 2012 Serval Project Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -321,14 +321,6 @@ foreach_instance_with_pidfile() { foreach_instance "${instances[@]}" "$@" } -# Utility function for setting up servald JNI fixtures: -# - check that libserval.so is present -# - set LD_LIBRARY_PATH so that libserval.so can be found -setup_servald_so() { - assert [ -r "$servald_build_root/libserval.so" ] - export LD_LIBRARY_PATH="$servald_build_root" -} - # Utility function for setting up a fixture with a servald server process: # - start a servald server process # - assert that the pidfile is created and correct diff --git a/testdefs_java.sh b/testdefs_java.sh new file mode 100644 index 00000000..8db580cb --- /dev/null +++ b/testdefs_java.sh @@ -0,0 +1,58 @@ +# Definitions for test suites using Java. +# Copyright 2014 Serval Project Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +source "${0%/*}/../testconfig.sh" + +# Utility function for setting up servald JNI fixtures: +# - check that libserval.so is present +# - set LD_LIBRARY_PATH so that libserval.so can be found +setup_servald_so() { + assert [ -r "$servald_build_root/libserval.so" ] + export LD_LIBRARY_PATH="$servald_build_root" +} + +compile_java_classes() { + assert --message='Java compiler was detected by ./configure' [ "$JAVAC" ] + mkdir classes + assert find "$servald_source_root"/java/ -name *.java | xargs $JAVAC -Xlint:unchecked -d classes + assert [ -r classes/org/servalproject/servaldna/ServalDCommand.class ] + assert [ -r classes/org/servalproject/servaldna/IJniResults.class ] + assert [ -r classes/org/servalproject/test/ServalDTests.class ] +} + +_executeJava() { + local func="${1?}" + shift + local opts=() + while [ $# -ne 0 ]; do + case "$1" in + --) shift; break;; + --*) opts+=("$1"); shift;; + *) break;; + esac + done + "$func" "${opts[@]}" --core-backtrace java "-Djava.library.path=$LD_LIBRARY_PATH" -classpath "$PWD/classes" "$@" +} + +executeJava() { + _executeJava execute "$@" +} + +executeJavaOk() { + _executeJava executeOk "$@" +} + diff --git a/testdefs_meshms.sh b/testdefs_meshms.sh new file mode 100644 index 00000000..a4298bb6 --- /dev/null +++ b/testdefs_meshms.sh @@ -0,0 +1,71 @@ +# Common definitions for MeshMS test suites. +# Copyright 2014 Serval Project Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +# Create a file that contains no blank lines. +meshms_create_message() { + create_file --label="$1" - $2 | sed -e '/^$/d' +} + +# Add a sequence of messages of varying sizes up to 1 KiB. +meshms_add_messages() { + local sid1="${1?}" + local sid2="${2?}" + local symbols="${3?}" + shift 3 + local texts=("$@") + local sent_since_ack=0 + local i n size msize + local size=0 + for ((i = 0; i < ${#symbols}; ++i)); do + local sym="${symbols:$i:1}" + let size+=379 + let msize=size%1021 + let n=NMESSAGE++ + local text="${texts[$i]}" + case $sym in + '>'|'<') + if [ -n "$text" ]; then + TEXT[$n]="$text" + else + TEXT[$n]="$(meshms_create_message "message$n" $msize)" + fi + ;; + esac + case $sym in + '>') + MESSAGE[$n]=">" + executeOk_servald meshms send message $sid1 $sid2 "${TEXT[$n]}" + let ++sent_since_ack + let ++NSENT + ;; + '<') + MESSAGE[$n]="<" + executeOk_servald meshms send message $sid2 $sid1 "${TEXT[$n]}" + let ++NRECV + ;; + 'A') + MESSAGE[$n]=ACK + [ $i -ne 0 -a $sent_since_ack -eq 0 ] && error "two ACKs in a row (at position $i)" + executeOk_servald meshms list messages $sid2 $sid1 + let ++NACK + ;; + *) + error "invalid message symbol '$sym' (at position $i)" + ;; + esac + done +} diff --git a/testdefs_rhizome.sh b/testdefs_rhizome.sh index 04939d28..0a8baf6e 100644 --- a/testdefs_rhizome.sh +++ b/testdefs_rhizome.sh @@ -1,5 +1,5 @@ -# Common definitions for rhizome test suites. -# Copyright 2012 The Serval Project, Inc. +# Common definitions for Rhizome test suites. +# Copyright 2012 Serval Project Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff --git a/tests/jni b/tests/jni index 2454b655..3f7b6516 100755 --- a/tests/jni +++ b/tests/jni @@ -2,7 +2,7 @@ # Tests for Serval DNA JNI entry points. # -# Copyright 2012 Serval Project, Inc. +# Copyright 2012-2014 Serval Project Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -20,7 +20,7 @@ source "${0%/*}/../testframework.sh" source "${0%/*}/../testdefs.sh" -source "${0%/*}/../testconfig.sh" +source "${0%/*}/../testdefs_java.sh" setup() { setup_servald @@ -32,15 +32,6 @@ setup() { setup_servald_so } -compile_java_classes() { - assert --message='Java compiler was detected by ./configure' [ "$JAVAC" ] - mkdir classes - assert find "$servald_source_root"/java/ -name *.java | xargs $JAVAC -Xlint:unchecked -d classes - assert [ -r classes/org/servalproject/servaldna/ServalDCommand.class ] - assert [ -r classes/org/servalproject/servaldna/IJniResults.class ] - assert [ -r classes/org/servalproject/test/ServalDTests.class ] -} - # Make sure that the normal echo command-line works, without JNI. assert_echo_works() { executeOk $servald echo -e 'Hello,\ttab' 'world\0!' @@ -49,7 +40,7 @@ assert_echo_works() { doc_Echo="Serval JNI echo Hello world" test_Echo() { - executeOk java "-Djava.library.path=$LD_LIBRARY_PATH" -classpath "$PWD/classes" org.servalproject.test.ServalDTests 'echo' '-e' 'Hello,\ttab' 'world\0!' + executeJavaOk org.servalproject.test.ServalDTests 'echo' '-e' 'Hello,\ttab' 'world\0!' assertStdoutIs -e 'Hello,\ttab world\0! \n' } @@ -66,14 +57,14 @@ test_Delim() { doc_Repeat="Serval JNI repeated calls in same process" test_Repeat() { - executeOk --core-backtrace java "-Djava.library.path=$LD_LIBRARY_PATH" -classpath "$PWD/classes" org.servalproject.test.ServalDTests repeat 50 'echo' 'Hello,' 'world!' + executeJavaOk org.servalproject.test.ServalDTests repeat 50 'echo' 'Hello,' 'world!' assertStdoutLineCount '==' 50 assertStdoutGrep --matches=50 '^Hello, world! $' } doc_NullArg="Serval JNI null arguments throw exception" test_NullArg() { - execute --core-backtrace java "-Djava.library.path=$LD_LIBRARY_PATH" -classpath "$PWD/classes" org.servalproject.test.ServalDTests 'echo' '(null)' + executeJava org.servalproject.test.ServalDTests 'echo' '(null)' tfw_cat --stdout --stderr assertExitStatus '!=' 0 assertStderrGrep 'NullPointerException: null element in argv' @@ -81,13 +72,11 @@ test_NullArg() { doc_help="Serval JNI returns help text" test_help() { - execute --core-backtrace java "-Djava.library.path=$LD_LIBRARY_PATH" -classpath "$PWD/classes" org.servalproject.test.ServalDTests 'help' - tfw_cat --stdout --stderr - assertExitStatus '==' 0 + executeJavaOk org.servalproject.test.ServalDTests 'help' assertStdoutGrep 'Serval DNA version ' } -doc_PeerList="Get peer details via JNI" +doc_PeerList="Serval JNI get peer details" setup_PeerList() { configure_servald_server() { add_servald_interface @@ -98,7 +87,7 @@ setup_PeerList() { set_instance +A } test_PeerList() { - execute --core-backtrace java "-Djava.library.path=$LD_LIBRARY_PATH" -classpath "$PWD/classes" org.servalproject.test.CommandLine 'peers' + executeJavaOk org.servalproject.test.CommandLine 'peers' assertStdoutGrep "$SIDB" tfw_cat --stdout } @@ -109,7 +98,7 @@ teardown_PeerList() { report_all_servald_servers } -doc_DnaLookup="DNA Lookup via JNI MDP API" +doc_DnaLookup="Serval JNI DNA Lookup" setup_DnaLookup() { configure_servald_server() { add_servald_interface @@ -123,9 +112,9 @@ setup_DnaLookup() { set_instance +A } test_DnaLookup() { - execute --timeout=10 --core-backtrace java "-Djava.library.path=$LD_LIBRARY_PATH" -classpath "$PWD/classes" org.servalproject.test.CommandLine 'lookup' - assertStdoutGrep "$SIDB" + executeJavaOk --timeout=10 org.servalproject.test.CommandLine 'lookup' tfw_cat --stdout --stderr + assertStdoutGrep "$SIDB" } teardown_DnaLookup() { stop_all_servald_servers @@ -134,7 +123,7 @@ teardown_DnaLookup() { report_all_servald_servers } -doc_serviceDiscovery="Discover network services by name" +doc_serviceDiscovery="Serval JNI discover network services by name" listen_service() { executeOk_servald --timeout=20 msp listen --service=test_name 512 <" tfw_cat --stdout --stderr } teardown_serviceDiscovery() { @@ -169,4 +158,5 @@ teardown_serviceDiscovery() { assert_no_servald_processes report_all_servald_servers } + runTests "$@" diff --git a/tests/rhizomehttp b/tests/rhizomehttp index eeb9b3e2..37e02b41 100755 --- a/tests/rhizomehttp +++ b/tests/rhizomehttp @@ -21,6 +21,7 @@ source "${0%/*}/../testframework.sh" source "${0%/*}/../testdefs.sh" source "${0%/*}/../testdefs_rhizome.sh" +source "${0%/*}/../testdefs_meshms.sh" shopt -s extglob @@ -47,6 +48,23 @@ assertJqCmp() { assert --dump-on-fail="$TFWTMP/jqcmp.tmp" --dump-on-fail="$file" "${opts[@]}" cmp "$TFWTMP/jqcmp.tmp" "$file" } +assertJqIs() { + local opts=() + while [ $# -gt 0 ]; do + case "$1" in + --) shift; break;; + --*) opts+=("$1"); shift;; + *) break;; + esac + done + [ $# -eq 3 ] || error "invalid arguments" + local json="$1" + local jqscript="$2" + local text="$3" + local jqout="$(jq --raw-output "$jqscript" "$json")" + assert "${opts[@]}" [ "$jqout" = "$text" ] +} + assertJqGrep() { local opts=() while [ $# -gt 0 ]; do @@ -989,66 +1007,11 @@ test_MeshmsListConversations() { ])" } -# Create a file that contains no blank lines. -create_message_file() { - create_file "$1" $2 - sed -i -e '/^$/d' "$1" -} - -# Add a sequence of messages of varying sizes up to 1 KiB. -add_messages() { - local symbols="$1" - shift - local texts=("$@") - local sent_since_ack=0 - local i n size msize - local size=0 - for ((i = 0; i < ${#symbols}; ++i)); do - local sym="${symbols:$i:1}" - let size+=379 - let msize=size%1021 - let n=NMESSAGE++ - local text="${texts[$i]}" - case $sym in - '>'|'<') - if [ -n "$text" ]; then - echo "$text" >text$n - else - create_message_file text$n $msize - text="$(') - MESSAGE[$n]=">" - executeOk_servald meshms send message $SIDA1 $SIDA2 "$text" - let ++sent_since_ack - let ++NSENT - ;; - '<') - MESSAGE[$n]="<" - executeOk_servald meshms send message $SIDA2 $SIDA1 "$text" - let ++NRECV - ;; - 'A') - MESSAGE[$n]=ACK - [ $i -ne 0 -a $sent_since_ack -eq 0 ] && error "two ACKs in a row (at position $i)" - executeOk_servald meshms list messages $SIDA2 $SIDA1 - let ++NACK - ;; - *) - error "invalid message symbol '$sym' (at position $i)" - ;; - esac - done -} - doc_MeshmsListMessages="HTTP RESTful list MeshMS messages in one conversation as JSON" setup_MeshmsListMessages() { IDENTITY_COUNT=2 setup - add_messages '><>>A>A<>><><><>>>A>A><<<><>>A<<>' + meshms_add_messages $SIDA1 $SIDA2 '><>>A>A<>><><><>>>A>A><<<><>>A<<>' let NROWS=NSENT+NRECV+(NACK?1:0) executeOk_servald meshms list messages $SIDA1 $SIDA2 delivered_offset=$(sed -n -e '/^[0-9]\+:[0-9]\+:ACK:delivered$/{n;s/^[0-9]\+:\([0-9]\+\):>:.*/\1/p;q}' "$TFWSTDOUT") @@ -1080,13 +1043,13 @@ test_MeshmsListMessages() { case ${MESSAGE[$j]} in '>') assertJq messages.json '.['$i'].type == ">"' - assertJqCmp messages.json '.['$i'].text' text$j + assertJqIs messages.json '.['$i'].text' "${TEXT[$j]}" assertJq messages.json '.['$i'].delivered == (.['$i'].offset <= '$delivered_offset')' let ++i ;; '<') assertJq messages.json '.['$i'].type == "<"' - assertJqCmp messages.json '.['$i'].text' text$j + assertJqIs messages.json '.['$i'].text' "${TEXT[$j]}" assertJq messages.json '.['$i'].read == (.['$i'].offset <= '$read_offset')' let ++i ;; @@ -1129,7 +1092,7 @@ setup_MeshmsListMessagesNewSince() { set rhizome.api.restful.newsince_poll_ms 500 } setup - add_messages '><>>A>A<>><><><>>>A>A><<<><>>A<<>' + meshms_add_messages $SIDA1 $SIDA2 '><>>A>A<>><><><>>>A>A><<<><>>A<<>' let NROWS=NSENT+NRECV+(NACK?1:0) executeOk curl \ --silent --fail --show-error \ @@ -1181,7 +1144,7 @@ setup_MeshmsListMessagesNewSinceArrival() { set rhizome.api.restful.newsince_poll_ms 500 } setup - add_messages '><>A>' + meshms_add_messages $SIDA1 $SIDA2 '><>A>' let NROWS=NSENT+NRECV+(NACK?1:0) executeOk curl \ --silent --fail --show-error \ @@ -1206,7 +1169,7 @@ test_MeshmsListMessagesNewSinceArrival() { done wait_until [ -e newsince1.json -a -e newsince2.json -a -e newsince3.json ] for message in '>Rumplestiltskin' 'A' 'Eulenspiegel'; do - add_messages "${message:0:1}" "${message:1}" + meshms_add_messages $SIDA1 $SIDA2 "${message:0:1}" "${message:1}" wait_until --timeout=60 grepall "${message:1}" newsince{1,2,3}.json done fork_terminate_all From d879189299b2978b5cb419f7eb91f42a89479407 Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Mon, 16 Jun 2014 14:48:07 +0930 Subject: [PATCH 3/6] Fix HTTP Authorization header parsing Was returning status 500 if Authorization: Basic token was long, because _reserve() did not have enough free space at start of buffer. The solution was to _commit(r) before attempting the _reserve_str() calls for the decoded username and password. As a separate issue, the status 500 also triggered an assertion failure because http_request_parse_header() was returning 400 but response.result_code had already been set to 500. --- http_server.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/http_server.c b/http_server.c index 0ea1ae10..4ec854fb 100644 --- a/http_server.c +++ b/http_server.c @@ -667,6 +667,7 @@ static int _parse_authorization(struct http_request *r, struct http_client_autho char buf[bufsz]; if (_parse_authorization_credentials_basic(r, &auth->credentials.basic, buf, bufsz)) { auth->scheme = BASIC; + _commit(r); // make room for following reservations if ( !_reserve_str(r, &auth->credentials.basic.user, auth->credentials.basic.user) || !_reserve_str(r, &auth->credentials.basic.password, auth->credentials.basic.password) ) @@ -974,6 +975,8 @@ static int http_request_parse_header(struct http_request *r) _commit(r); return 0; } + if (r->response.result_code) + return r->response.result_code; goto malformed; } _rewind(r); From 9cbd7c365cc1a9c1f94ff4503d06b520a4e06f49 Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Wed, 11 Jun 2014 17:21:08 +0930 Subject: [PATCH 4/6] MeshMS Java API: list conversations --- java/org/servalproject/codec/Base64.java | 70 +++ .../servaldna/ServalDClient.java | 122 +++++ .../servaldna/ServalDFailureException.java | 2 +- .../ServalDHttpConnectionFactory.java | 30 ++ .../servaldna/ServalDInterfaceException.java | 44 ++ .../servaldna/meshms/MeshMSConversation.java | 46 ++ .../meshms/MeshMSConversationList.java | 438 ++++++++++++++++++ java/org/servalproject/test/Meshms.java | 72 +++ tests/meshmsjava | 72 +++ 9 files changed, 895 insertions(+), 1 deletion(-) create mode 100644 java/org/servalproject/codec/Base64.java create mode 100644 java/org/servalproject/servaldna/ServalDClient.java create mode 100644 java/org/servalproject/servaldna/ServalDHttpConnectionFactory.java create mode 100644 java/org/servalproject/servaldna/ServalDInterfaceException.java create mode 100644 java/org/servalproject/servaldna/meshms/MeshMSConversation.java create mode 100644 java/org/servalproject/servaldna/meshms/MeshMSConversationList.java create mode 100644 java/org/servalproject/test/Meshms.java create mode 100755 tests/meshmsjava diff --git a/java/org/servalproject/codec/Base64.java b/java/org/servalproject/codec/Base64.java new file mode 100644 index 00000000..56ff6ac4 --- /dev/null +++ b/java/org/servalproject/codec/Base64.java @@ -0,0 +1,70 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.codec; + +import java.lang.StringBuilder; + +public class Base64 { + + public static final char[] SYMBOLS = { + 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', + 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', + 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', + 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/', + '=' + }; + + public static String encode(byte[] binary) + { + StringBuilder sb = new StringBuilder(); + int place = 0; + byte buf = 0; + for (byte b: binary) { + switch (place) { + case 0: + sb.append(SYMBOLS[b >>> 2]); + buf = (byte)((b << 4) & 0x3f); + place = 1; + break; + case 1: + sb.append(SYMBOLS[(b >>> 4) | buf]); + buf = (byte)((b << 2) & 0x3f); + place = 2; + break; + case 2: + sb.append(SYMBOLS[(b >>> 6) | buf]); + sb.append(SYMBOLS[b & 0x3f]); + place = 0; + break; + } + } + if (place != 0) + sb.append(SYMBOLS[buf]); + switch (place) { + case 1: + sb.append(SYMBOLS[64]); + case 2: + sb.append(SYMBOLS[64]); + } + return sb.toString(); + } + +} diff --git a/java/org/servalproject/servaldna/ServalDClient.java b/java/org/servalproject/servaldna/ServalDClient.java new file mode 100644 index 00000000..21c71dba --- /dev/null +++ b/java/org/servalproject/servaldna/ServalDClient.java @@ -0,0 +1,122 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLConnection; +import java.net.HttpURLConnection; +import org.servalproject.codec.Base64; +import org.servalproject.servaldna.SubscriberId; +import org.servalproject.servaldna.ServalDCommand; +import org.servalproject.servaldna.ServalDInterfaceException; +import org.servalproject.servaldna.meshms.MeshMSConversationList; + +public class ServalDClient implements ServalDHttpConnectionFactory +{ + + private static final String restfulUsername = "ServalDClient"; + private static final String restfulPasswordDefault = "u6ng^ues%@@SabLEEEE8"; + private static String restfulPassword; + protected boolean connected; + int httpPort; + + public static ServalDClient newServalDClient() + { + return new ServalDClient(); + } + + protected ServalDClient() + { + restfulPassword = null; + connected = false; + httpPort = 0; + } + + private void connect() throws ServalDInterfaceException + { + ensureServerRunning(); + if (!connected) { + if (!fetchRestfulAuthorization()) + createRestfulAuthorization(); + connected = true; + } + } + + private void ensureServerRunning() throws ServalDInterfaceException + { + ServalDCommand.Status s = ServalDCommand.serverStatus(); + if (!s.status.equals("running")) + throw new ServalDInterfaceException("server is not running"); + if (s.httpPort < 1 || s.httpPort > 65535) + throw new ServalDInterfaceException("invalid HTTP port number: " + s.httpPort); + httpPort = s.httpPort; + } + + private boolean fetchRestfulAuthorization() throws ServalDInterfaceException + { + restfulPassword = ServalDCommand.getConfigItem("rhizome.api.restful.users." + restfulUsername + ".password"); + return restfulPassword != null; + } + + private void createRestfulAuthorization() throws ServalDInterfaceException + { + ServalDCommand.setConfigItem("rhizome.api.restful.users." + restfulUsername + ".password", restfulPasswordDefault); + ServalDCommand.configSync(); + if (!fetchRestfulAuthorization()) + throw new ServalDInterfaceException("restful password not set"); + } + + public MeshMSConversationList meshmsListConversations(SubscriberId sid) throws ServalDInterfaceException, IOException + { + MeshMSConversationList list = new MeshMSConversationList(this, sid); + list.connect(); + return list; + } + + // interface ServalDHttpConnectionFactory + public HttpURLConnection newServalDHttpConnection(String path) throws ServalDInterfaceException, IOException + { + connect(); + assert restfulPassword != null; + assert httpPort != 0; + URL url = new URL("http", "localhost", httpPort, path); + URLConnection uconn = url.openConnection(); + HttpURLConnection conn; + try { + conn = (HttpURLConnection) uconn; + } + catch (ClassCastException e) { + throw new ServalDInterfaceException("URL.openConnection() returned a " + uconn.getClass().getName() + ", expecting a HttpURLConnection", e); + } + int status = 0; + conn.setAllowUserInteraction(false); + try { + conn.addRequestProperty("Authorization", "Basic " + Base64.encode((restfulUsername + ":" + restfulPassword).getBytes("US-ASCII"))); + } + catch (UnsupportedEncodingException e) { + throw new ServalDInterfaceException("invalid RESTful password", e); + } + return conn; + } + +} diff --git a/java/org/servalproject/servaldna/ServalDFailureException.java b/java/org/servalproject/servaldna/ServalDFailureException.java index b2211e7d..90296a86 100644 --- a/java/org/servalproject/servaldna/ServalDFailureException.java +++ b/java/org/servalproject/servaldna/ServalDFailureException.java @@ -26,7 +26,7 @@ package org.servalproject.servaldna; * * @author Andrew Bettison */ -public class ServalDFailureException extends Exception +public class ServalDFailureException extends ServalDInterfaceException { private static final long serialVersionUID = 1L; diff --git a/java/org/servalproject/servaldna/ServalDHttpConnectionFactory.java b/java/org/servalproject/servaldna/ServalDHttpConnectionFactory.java new file mode 100644 index 00000000..2ac1def5 --- /dev/null +++ b/java/org/servalproject/servaldna/ServalDHttpConnectionFactory.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna; + +import java.io.IOException; +import java.net.HttpURLConnection; + +public interface ServalDHttpConnectionFactory { + + public HttpURLConnection newServalDHttpConnection(String path) throws ServalDInterfaceException, IOException; + +} diff --git a/java/org/servalproject/servaldna/ServalDInterfaceException.java b/java/org/servalproject/servaldna/ServalDInterfaceException.java new file mode 100644 index 00000000..ec2ff597 --- /dev/null +++ b/java/org/servalproject/servaldna/ServalDInterfaceException.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna; + +/** + * Thrown when the Serval DNA interface has not behaved as expected. This is a general class of + * errors, and is specialised by subclasses that represent an error returned by a server command, + * MDP protocol non-compliance, etc. + * + * @author Andrew Bettison + */ +public class ServalDInterfaceException extends Exception +{ + public ServalDInterfaceException(String message) { + super(message); + } + + public ServalDInterfaceException(Throwable cause) { + super(cause); + } + + public ServalDInterfaceException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/java/org/servalproject/servaldna/meshms/MeshMSConversation.java b/java/org/servalproject/servaldna/meshms/MeshMSConversation.java new file mode 100644 index 00000000..c944c1f0 --- /dev/null +++ b/java/org/servalproject/servaldna/meshms/MeshMSConversation.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.meshms; + +import org.servalproject.servaldna.SubscriberId; + +public class MeshMSConversation { + + public final int _rowNumber; + public final int _id; + public final SubscriberId mySid; + public final SubscriberId theirSid; + public final boolean isRead; + public final int lastMessageOffset; + public final int readOffset; + + protected MeshMSConversation(int rowNumber, int _id, SubscriberId my_sid, SubscriberId their_sid, boolean read, int last_message_offset, int read_offset) + { + this._rowNumber = rowNumber; + this._id = _id; + this.mySid = my_sid; + this.theirSid = their_sid; + this.isRead = read; + this.lastMessageOffset = last_message_offset; + this.readOffset = read_offset; + } + +} diff --git a/java/org/servalproject/servaldna/meshms/MeshMSConversationList.java b/java/org/servalproject/servaldna/meshms/MeshMSConversationList.java new file mode 100644 index 00000000..9bd8ff27 --- /dev/null +++ b/java/org/servalproject/servaldna/meshms/MeshMSConversationList.java @@ -0,0 +1,438 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.meshms; + +import java.io.IOException; +import java.lang.StringBuilder; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.PushbackReader; +import java.util.Collection; +import java.util.Vector; +import java.net.HttpURLConnection; +import org.servalproject.servaldna.ServalDHttpConnectionFactory; +import org.servalproject.servaldna.ServalDInterfaceException; +import org.servalproject.servaldna.SubscriberId; + +public class MeshMSConversationList { + + private ServalDHttpConnectionFactory httpConnector; + private SubscriberId sid; + private HttpURLConnection httpConnection; + private PushbackReader reader; + private Vector headers; + int columnIndex__id; + int columnIndex_my_sid; + int columnIndex_their_sid; + int columnIndex_read; + int columnIndex_last_message; + int columnIndex_read_offset; + int rowCount; + + public MeshMSConversationList(ServalDHttpConnectionFactory connector, SubscriberId sid) + { + this.httpConnector = connector; + this.sid = sid; + } + + public void connect() throws ServalDInterfaceException, IOException + { + columnIndex__id = -1; + columnIndex_my_sid = -1; + columnIndex_their_sid = -1; + columnIndex_read = -1; + columnIndex_last_message = -1; + columnIndex_read_offset = -1; + rowCount = 0; + httpConnection = httpConnector.newServalDHttpConnection("/restful/meshms/" + sid.toHex() + "/conversationlist.json"); + httpConnection.connect(); + reader = new PushbackReader(new InputStreamReader(httpConnection.getInputStream(), "US-ASCII")); + consume(reader, JsonToken.START_OBJECT); + consume(reader, "header"); + consume(reader, JsonToken.COLON); + headers = new Vector(); + consumeArray(reader, headers, String.class); + if (headers.size() < 1) + throw new ServalDInterfaceException("empty JSON headers array"); + for (int i = 0; i < headers.size(); ++i) { + String header = headers.get(i); + if (header.equals("_id")) + columnIndex__id = i; + else if (header.equals("my_sid")) + columnIndex_my_sid = i; + else if (header.equals("their_sid")) + columnIndex_their_sid = i; + else if (header.equals("read")) + columnIndex_read = i; + else if (header.equals("last_message")) + columnIndex_last_message = i; + else if (header.equals("read_offset")) + columnIndex_read_offset = i; + } + if (columnIndex__id == -1) + throw new ServalDInterfaceException("missing JSON column: _id"); + if (columnIndex_my_sid == -1) + throw new ServalDInterfaceException("missing JSON column: my_sid"); + if (columnIndex_their_sid == -1) + throw new ServalDInterfaceException("missing JSON column: their_sid"); + if (columnIndex_read == -1) + throw new ServalDInterfaceException("missing JSON column: read"); + if (columnIndex_last_message == -1) + throw new ServalDInterfaceException("missing JSON column: last_message"); + if (columnIndex_read_offset == -1) + throw new ServalDInterfaceException("missing JSON column: read_offset"); + consume(reader, JsonToken.COMMA); + consume(reader, "rows"); + consume(reader, JsonToken.COLON); + consume(reader, JsonToken.START_ARRAY); + } + + public MeshMSConversation nextConversation() throws ServalDInterfaceException, IOException + { + Object tok = nextJsonToken(reader); + if (tok == JsonToken.END_ARRAY) { + consume(reader, JsonToken.END_OBJECT); + consume(reader, JsonToken.EOF); + return null; + } + if (rowCount != 0) { + match(tok, JsonToken.COMMA); + tok = nextJsonToken(reader); + } + match(tok, JsonToken.START_ARRAY); + Object[] row = new Object[headers.size()]; + for (int i = 0; i < headers.size(); ++i) { + if (i != 0) + consume(reader, JsonToken.COMMA); + row[i] = consume(reader); + } + consume(reader, JsonToken.END_ARRAY); + int _id = narrow(row[columnIndex__id], Integer.class); + SubscriberId my_sid; + try { + my_sid = new SubscriberId(narrow(row[columnIndex_my_sid], String.class)); + } + catch (SubscriberId.InvalidHexException e) { + throw new ServalDInterfaceException("invalid JSON column value: my_sid", e); + } + SubscriberId their_sid; + try { + their_sid = new SubscriberId(narrow(row[columnIndex_their_sid], String.class)); + } + catch (SubscriberId.InvalidHexException e) { + throw new ServalDInterfaceException("invalid JSON column value: their_sid", e); + } + boolean is_read = narrow(row[columnIndex_read], Boolean.class); + int last_message = narrow(row[columnIndex_last_message], Integer.class); + int read_offset = narrow(row[columnIndex_read_offset], Integer.class); + return new MeshMSConversation(rowCount++, _id, my_sid, their_sid, is_read, last_message, read_offset); + } + + public void close() throws IOException + { + if (reader != null) { + reader.close(); + reader = null; + } + } + + static void match(Object tok, JsonToken exactly) throws ServalDInterfaceException + { + if (tok != exactly) + throw new ServalDInterfaceException("unexpected JSON token " + exactly + ", got: " + jsonTokenDescription(tok)); + } + + static void consume(PushbackReader rd, JsonToken exactly) throws ServalDInterfaceException, IOException + { + match(nextJsonToken(rd), exactly); + } + + @SuppressWarnings("unchecked") + static T narrow(Object tok, Class cls) throws ServalDInterfaceException + { + assert !cls.isAssignableFrom(JsonToken.class); // can only narrow to values + if (tok == JsonToken.EOF) + throw new ServalDInterfaceException("unexpected EOF"); + if (tok instanceof JsonToken) + throw new ServalDInterfaceException("expecting JSON " + cls.getName() + ", got: " + tok); + // Convert: + // Integer --> Float or Double + // Float --> Double + // Double --> Float + if (cls == Double.class && (tok instanceof Float || tok instanceof Integer)) + tok = new Double(((Number)tok).doubleValue()); + else if (cls == Float.class && (tok instanceof Double || tok instanceof Integer)) + tok = new Float(((Number)tok).floatValue()); + if (cls.isInstance(tok)) + return (T)tok; + throw new ServalDInterfaceException("expecting JSON " + cls.getName() + ", got: " + jsonTokenDescription(tok)); + } + + static T consume(PushbackReader rd, Class cls) throws ServalDInterfaceException, IOException + { + return narrow(nextJsonToken(rd), cls); + } + + static Object consume(PushbackReader rd) throws ServalDInterfaceException, IOException + { + return consume(rd, Object.class); + } + + static String consume(PushbackReader rd, String exactly) throws ServalDInterfaceException, IOException + { + String tok = consume(rd, String.class); + if (tok.equals(exactly)) + return tok; + throw new ServalDInterfaceException("unexpected JSON String \"" + exactly + "\", got: " + jsonTokenDescription(tok)); + } + + static int consumeArray(PushbackReader rd, Collection collection, Class cls) throws ServalDInterfaceException, IOException + { + int added = 0; + consume(rd, JsonToken.START_ARRAY); + Object tok = nextJsonToken(rd); + if (tok != JsonToken.END_ARRAY) { + while (true) { + try { + collection.add(narrow(tok, cls)); + ++added; + } + catch (ClassCastException e) { + throw new ServalDInterfaceException("unexpected JSON token: " + jsonTokenDescription(tok)); + } + tok = nextJsonToken(rd); + if (tok == JsonToken.END_ARRAY) + break; + match(tok, JsonToken.COMMA); + tok = nextJsonToken(rd); + } + } + return added; + } + + enum JsonToken { + START_OBJECT, + END_OBJECT, + START_ARRAY, + END_ARRAY, + COMMA, + COLON, + NULL, + EOF + }; + + static boolean jsonIsToken(Object tok) + { + return tok instanceof JsonToken || tok instanceof String || tok instanceof Double || tok instanceof Integer || tok instanceof Boolean; + } + + static String jsonTokenDescription(Object tok) + { + if (tok instanceof String) + return "\"" + tok + "\""; + if (tok instanceof Number) + return "" + tok; + if (tok instanceof Boolean) + return "" + tok; + assert tok instanceof JsonToken; + return tok.toString(); + } + + static void readAll(Reader rd, char[] word) throws ServalDInterfaceException, IOException + { + int len = 0; + while (len < word.length) { + int n = rd.read(word, len, word.length - len); + if (n == -1) + throw new ServalDInterfaceException("unexpected EOF"); + len += n; + } + } + + static Object nextJsonToken(PushbackReader rd) throws ServalDInterfaceException, IOException + { + while (true) { + int c = rd.read(); + switch (c) { + case -1: + return JsonToken.EOF; + case '\t': + case '\r': + case '\n': + case ' ': + break; + case '{': + return JsonToken.START_OBJECT; + case '}': + return JsonToken.END_OBJECT; + case '[': + return JsonToken.START_ARRAY; + case ']': + return JsonToken.END_ARRAY; + case ',': + return JsonToken.COMMA; + case ':': + return JsonToken.COLON; + case 't': { + char[] word = new char[3]; + readAll(rd, word); + if (word[0] == 'r' && word[1] == 'u' && word[2] == 'e') + return Boolean.TRUE; + } + throw new ServalDInterfaceException("malformed JSON"); + case 'f': { + char[] word = new char[4]; + readAll(rd, word); + if (word[0] == 'a' && word[1] == 'l' && word[2] == 's' && word[3] == 'e') + return Boolean.FALSE; + } + throw new ServalDInterfaceException("malformed JSON"); + case 'n': { + char[] word = new char[3]; + readAll(rd, word); + if (word[0] == 'u' && word[1] == 'l' && word[2] == 'l') + return JsonToken.NULL; + } + throw new ServalDInterfaceException("malformed JSON"); + case '"': { + StringBuilder sb = new StringBuilder(); + boolean slosh = false; + while (true) { + c = rd.read(); + if (c == -1) + throw new ServalDInterfaceException("unexpected EOF in JSON string"); + if (slosh) { + switch (c) { + case '"': case '/': case '\\': sb.append('"'); break; + case 'b': sb.append('\b'); break; + case 'f': sb.append('\f'); break; + case 'n': sb.append('\n'); break; + case 'r': sb.append('\r'); break; + case 't': sb.append('\t'); break; + case 'u': + char[] hex = new char[4]; + readAll(rd, hex); + int code = Integer.valueOf(new String(hex), 16); + if (code >= 0 && code <= 0xffff) { + sb.append((char)code); + break; + } + // fall through + default: + throw new ServalDInterfaceException("malformed JSON string"); + } + } + else { + switch (c) { + case '"': + return sb.toString(); + case '\\': + slosh = true; + break; + default: + sb.append((char)c); + break; + } + } + } + } + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': { + StringBuilder sb = new StringBuilder(); + if (c == '-') { + sb.append((char)c); + c = rd.read(); + } + if (c == '0') { + sb.append((char)c); + c = rd.read(); + } + else if (Character.isDigit(c)) { + do { + sb.append((char)c); + c = rd.read(); + } + while (Character.isDigit(c)); + } + else + throw new ServalDInterfaceException("malformed JSON number"); + boolean isfloat = false; + if (c == '.') { + isfloat = true; + sb.append((char)c); + c = rd.read(); + if (c == -1) + throw new ServalDInterfaceException("unexpected EOF in JSON number"); + if (!Character.isDigit(c)) + throw new ServalDInterfaceException("malformed JSON number"); + do { + sb.append((char)c); + c = rd.read(); + } + while (Character.isDigit(c)); + } + if (c == 'e' || c == 'E') { + isfloat = true; + sb.append((char)c); + c = rd.read(); + if (c == '+' || c == '-') { + sb.append((char)c); + c = rd.read(); + } + if (c == -1) + throw new ServalDInterfaceException("unexpected EOF in JSON number"); + if (!Character.isDigit(c)) + throw new ServalDInterfaceException("malformed JSON number"); + do { + sb.append((char)c); + c = rd.read(); + } + while (Character.isDigit(c)); + } + rd.unread(c); + String number = sb.toString(); + try { + if (isfloat) + return Double.parseDouble(number); + else + return Integer.parseInt(number); + } + catch (NumberFormatException e) { + throw new ServalDInterfaceException("malformed JSON number: " + number); + } + } + default: + throw new ServalDInterfaceException("malformed JSON: '" + (char)c + "'"); + } + } + } + +} diff --git a/java/org/servalproject/test/Meshms.java b/java/org/servalproject/test/Meshms.java new file mode 100644 index 00000000..ee786328 --- /dev/null +++ b/java/org/servalproject/test/Meshms.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.test; + +import java.lang.System; +import java.io.IOException; +import org.servalproject.servaldna.SubscriberId; + +import org.servalproject.servaldna.ServalDClient; +import org.servalproject.servaldna.ServalDInterfaceException; +import org.servalproject.servaldna.meshms.MeshMSConversationList; +import org.servalproject.servaldna.meshms.MeshMSConversation; + +public class Meshms { + + static void meshms_list_conversations(SubscriberId sid, String offset, String count) throws ServalDInterfaceException, IOException, InterruptedException + { + ServalDClient client = ServalDClient.newServalDClient(); + MeshMSConversationList list = client.meshmsListConversations(sid); + try { + MeshMSConversation conv; + while ((conv = list.nextConversation()) != null) { + System.out.println( + "_id=" + conv._id + + ", my_sid=" + conv.mySid + + ", their_sid=" + conv.theirSid + + ", read=" + conv.isRead + + ", last_message=" + conv.lastMessageOffset + + ", read_offset=" + conv.readOffset + ); + } + } + finally { + list.close(); + } + System.exit(0); + } + + public static void main(String... args) + { + if (args.length < 1) + return; + String methodName = args[0]; + try { + if (methodName.equals("meshms-list-conversations")) + meshms_list_conversations(new SubscriberId(args[1]), args.length > 2 ? args[2] : null, args.length > 3 ? args[3] : null); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + System.err.println("No such command: " + methodName); + System.exit(1); + } +} diff --git a/tests/meshmsjava b/tests/meshmsjava new file mode 100755 index 00000000..57ffda50 --- /dev/null +++ b/tests/meshmsjava @@ -0,0 +1,72 @@ +#!/bin/bash + +# Tests for MeshMS Java API. +# +# Copyright 2014 Serval Project Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +source "${0%/*}/../testframework.sh" +source "${0%/*}/../testdefs.sh" +source "${0%/*}/../testdefs_java.sh" + +setup() { + setup_servald + setup_servald_so + compile_java_classes + set_instance +A + executeOk_servald config \ + set log.console.level debug \ + set debug.httpd on + create_identities 5 + configure_servald_server() { + add_servald_interface + executeOk_servald config \ + set rhizome.api.restful.users.joe.password bloggs + } + start_servald_server +} + +teardown() { + stop_all_servald_servers + kill_all_servald_processes + assert_no_servald_processes + report_all_servald_servers +} + +doc_MeshmsListConversations="Java API list MeshMS conversations" +setup_MeshmsListConversations() { + setup + # create 3 threads, with all permutations of incoming and outgoing messages + executeOk_servald meshms send message $SIDA1 $SIDA2 "Message One" + executeOk_servald meshms send message $SIDA3 $SIDA1 "Message Two" + executeOk_servald meshms send message $SIDA1 $SIDA4 "Message Three" + executeOk_servald meshms send message $SIDA4 $SIDA1 "Message Four" +} +test_MeshmsListConversations() { + executeJavaOk org.servalproject.test.Meshms meshms-list-conversations $SIDA1 + assertStdoutLineCount '==' 3 + assertStdoutGrep "my_sid=$SIDA1, their_sid=$SIDA2, read=true, last_message=0, read_offset=0" + assertStdoutGrep "my_sid=$SIDA1, their_sid=$SIDA3, read=false, last_message=14, read_offset=0" + assertStdoutGrep "my_sid=$SIDA1, their_sid=$SIDA4, read=false, last_message=18, read_offset=0" + executeOk_servald meshms read messages $SIDA1 + executeJavaOk org.servalproject.test.Meshms meshms-list-conversations $SIDA1 + assertStdoutLineCount '==' 3 + assertStdoutGrep "my_sid=$SIDA1, their_sid=$SIDA2, read=true, last_message=0, read_offset=0" + assertStdoutGrep "my_sid=$SIDA1, their_sid=$SIDA3, read=true, last_message=14, read_offset=14" + assertStdoutGrep "my_sid=$SIDA1, their_sid=$SIDA4, read=true, last_message=18, read_offset=18" +} + +runTests "$@" From 0a5441474482d0cc5cdda1546136b10adbc21e9b Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Tue, 17 Jun 2014 13:26:03 +0930 Subject: [PATCH 5/6] Refactor Java JSON parsing into JSONTokeniser --- .../json/JSONInputException.java | 43 ++ .../org/servalproject/json/JSONTokeniser.java | 377 ++++++++++++++ .../meshms/MeshMSConversationList.java | 479 ++++-------------- 3 files changed, 529 insertions(+), 370 deletions(-) create mode 100644 java/org/servalproject/json/JSONInputException.java create mode 100644 java/org/servalproject/json/JSONTokeniser.java diff --git a/java/org/servalproject/json/JSONInputException.java b/java/org/servalproject/json/JSONInputException.java new file mode 100644 index 00000000..b64840b6 --- /dev/null +++ b/java/org/servalproject/json/JSONInputException.java @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.json; + +/** + * Thrown when there is any problem with JSON input. This exception class is subclassed to + * specialise it to specific causes, such as JSON syntax error, unexpected JSON token, etc. + * + * @author Andrew Bettison + */ +public class JSONInputException extends Exception +{ + public JSONInputException(String message) { + super(message); + } + + public JSONInputException(Throwable cause) { + super(cause); + } + + public JSONInputException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/java/org/servalproject/json/JSONTokeniser.java b/java/org/servalproject/json/JSONTokeniser.java new file mode 100644 index 00000000..144ff817 --- /dev/null +++ b/java/org/servalproject/json/JSONTokeniser.java @@ -0,0 +1,377 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.json; + +import java.lang.StringBuilder; +import java.lang.NumberFormatException; +import java.io.IOException; +import java.io.Reader; +import java.io.PushbackReader; +import java.util.Collection; + +public class JSONTokeniser { + + PushbackReader reader; + + public enum Token { + START_OBJECT, + END_OBJECT, + START_ARRAY, + END_ARRAY, + COMMA, + COLON, + NULL, + EOF + }; + + public static class SyntaxException extends JSONInputException + { + public SyntaxException(String message) { + super(message); + } + } + + public static class UnexpectedException extends JSONInputException + { + public UnexpectedException(String got, Class expecting) { + super("unexpected " + got + ", expecting " + expecting.getName()); + } + + public UnexpectedException(String got, Object expecting) { + super("unexpected " + got + ", expecting " + jsonTokenDescription(expecting)); + } + + } + + public static class UnexpectedEOFException extends UnexpectedException + { + public UnexpectedEOFException(Class expecting) { + super("EOF", expecting); + } + + public UnexpectedEOFException(Object expecting) { + super("EOF", expecting); + } + + } + + public static class UnexpectedTokenException extends UnexpectedException + { + public UnexpectedTokenException(Object got, Class expecting) { + super(jsonTokenDescription(got), expecting); + } + + public UnexpectedTokenException(Object got, Object expecting) { + super(jsonTokenDescription(got), expecting); + } + + } + + // Can accept any PushbackReader, because we only need one character of unread(). + public JSONTokeniser(PushbackReader pbrd) + { + reader = pbrd; + } + + public JSONTokeniser(Reader rd) + { + reader = new PushbackReader(rd); + } + + public static void match(Object tok, Token exactly) throws SyntaxException + { + if (tok != exactly) + throw new SyntaxException("JSON syntax error: expecting " + exactly + ", got " + jsonTokenDescription(tok)); + } + + public void consume(Token exactly) throws SyntaxException, UnexpectedException, IOException + { + match(nextToken(), exactly); + } + + @SuppressWarnings("unchecked") + public static T narrow(Object tok, Class cls) throws UnexpectedException + { + assert !cls.isAssignableFrom(Token.class); // can only narrow to values + if (tok == Token.EOF) + throw new UnexpectedEOFException(cls); + if (tok instanceof Token) + throw new UnexpectedTokenException(tok, cls); + // Convert: + // Integer --> Float or Double + // Float --> Double + // Double --> Float + if (cls == Double.class && (tok instanceof Float || tok instanceof Integer)) + tok = new Double(((Number)tok).doubleValue()); + else if (cls == Float.class && (tok instanceof Double || tok instanceof Integer)) + tok = new Float(((Number)tok).floatValue()); + if (cls.isInstance(tok)) + return (T)tok; + throw new UnexpectedTokenException(tok, cls); + } + + public T consume(Class cls) throws SyntaxException, UnexpectedException, IOException + { + return narrow(nextToken(), cls); + } + + public Object consume() throws SyntaxException, UnexpectedException, IOException + { + return consume(Object.class); + } + + public String consume(String exactly) throws SyntaxException, UnexpectedException, IOException + { + String tok = consume(String.class); + if (tok.equals(exactly)) + return tok; + throw new UnexpectedTokenException(tok, exactly); + } + + public int consumeArray(Collection collection, Class cls) throws SyntaxException, UnexpectedException, IOException + { + int added = 0; + consume(Token.START_ARRAY); + Object tok = nextToken(); + if (tok != Token.END_ARRAY) { + while (true) { + collection.add(narrow(tok, cls)); + ++added; + tok = nextToken(); + if (tok == Token.END_ARRAY) + break; + match(tok, Token.COMMA); + tok = nextToken(); + } + } + return added; + } + + public static boolean jsonIsToken(Object tok) + { + return tok instanceof Token || tok instanceof String || tok instanceof Double || tok instanceof Integer || tok instanceof Boolean; + } + + public static String jsonTokenDescription(Object tok) + { + if (tok instanceof String) + return "\"" + tok + "\""; + if (tok instanceof Number) + return "" + tok; + if (tok instanceof Boolean) + return "" + tok; + assert tok instanceof Token; + return tok.toString(); + } + + private void readWord(String word) throws SyntaxException, IOException + { + int len = 0; + while (len < word.length()) { + char[] buf = new char[word.length() - len]; + int n = this.reader.read(buf, 0, buf.length); + if (n == -1) + throw new SyntaxException("EOF in middle of \"" + word + "\""); + for (int i = 0; i < n; ++i) + if (buf[i] != word.charAt(len++)) + throw new SyntaxException("expecting \"" + word + "\""); + } + } + + private int readHex(int digits) throws SyntaxException, IOException + { + char[] buf = new char[digits]; + int len = 0; + while (len < buf.length) { + int n = this.reader.read(buf, len, buf.length - len); + if (n == -1) + throw new SyntaxException("EOF in middle of " + digits + " hex digits"); + len += n; + } + String hex = new String(buf); + try { + return Integer.valueOf(hex, 16); + } + catch (NumberFormatException e) { + throw new SyntaxException("expecting " + digits + " hex digits, got \"" + hex + "\""); + } + } + + public Object nextToken() throws SyntaxException, IOException + { + while (true) { + int c = this.reader.read(); + switch (c) { + case -1: + return Token.EOF; + case '\t': + case '\r': + case '\n': + case ' ': + break; + case '{': + return Token.START_OBJECT; + case '}': + return Token.END_OBJECT; + case '[': + return Token.START_ARRAY; + case ']': + return Token.END_ARRAY; + case ',': + return Token.COMMA; + case ':': + return Token.COLON; + case 't': + this.reader.unread(c); + readWord("true"); + return Boolean.TRUE; + case 'f': + this.reader.unread(c); + readWord("false"); + return Boolean.FALSE; + case 'n': + this.reader.unread(c); + readWord("null"); + return Token.NULL; + case '"': { + StringBuilder sb = new StringBuilder(); + boolean slosh = false; + while (true) { + c = this.reader.read(); + if (c == -1) + throw new SyntaxException("unexpected EOF in JSON string"); + if (slosh) { + switch (c) { + case '"': case '/': case '\\': sb.append('"'); break; + case 'b': sb.append('\b'); break; + case 'f': sb.append('\f'); break; + case 'n': sb.append('\n'); break; + case 'r': sb.append('\r'); break; + case 't': sb.append('\t'); break; + case 'u': + + int code = readHex(4); + sb.append((char)code); + // fall through + default: + throw new SyntaxException("malformed JSON string"); + } + } + else { + switch (c) { + case '"': + return sb.toString(); + case '\\': + slosh = true; + break; + default: + sb.append((char)c); + break; + } + } + } + } + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': { + StringBuilder sb = new StringBuilder(); + if (c == '-') { + sb.append((char)c); + c = this.reader.read(); + } + if (c == '0') { + sb.append((char)c); + c = this.reader.read(); + } + else if (Character.isDigit(c)) { + do { + sb.append((char)c); + c = this.reader.read(); + } + while (Character.isDigit(c)); + } + else + throw new SyntaxException("malformed JSON number"); + boolean isfloat = false; + if (c == '.') { + isfloat = true; + sb.append((char)c); + c = this.reader.read(); + if (c == -1) + throw new SyntaxException("unexpected EOF in JSON number"); + if (!Character.isDigit(c)) + throw new SyntaxException("malformed JSON number"); + do { + sb.append((char)c); + c = this.reader.read(); + } + while (Character.isDigit(c)); + } + if (c == 'e' || c == 'E') { + isfloat = true; + sb.append((char)c); + c = this.reader.read(); + if (c == '+' || c == '-') { + sb.append((char)c); + c = this.reader.read(); + } + if (c == -1) + throw new SyntaxException("unexpected EOF in JSON number"); + if (!Character.isDigit(c)) + throw new SyntaxException("malformed JSON number"); + do { + sb.append((char)c); + c = this.reader.read(); + } + while (Character.isDigit(c)); + } + this.reader.unread(c); + String number = sb.toString(); + try { + if (isfloat) + return Double.parseDouble(number); + else + return Integer.parseInt(number); + } + catch (NumberFormatException e) { + throw new SyntaxException("malformed JSON number: " + number); + } + } + default: + throw new SyntaxException("malformed JSON: '" + (char)c + "'"); + } + } + } + + public void close() throws IOException + { + this.reader.close(); + } + +} diff --git a/java/org/servalproject/servaldna/meshms/MeshMSConversationList.java b/java/org/servalproject/servaldna/meshms/MeshMSConversationList.java index 9bd8ff27..2abc63f9 100644 --- a/java/org/servalproject/servaldna/meshms/MeshMSConversationList.java +++ b/java/org/servalproject/servaldna/meshms/MeshMSConversationList.java @@ -21,24 +21,21 @@ package org.servalproject.servaldna.meshms; import java.io.IOException; -import java.lang.StringBuilder; -import java.io.InputStream; import java.io.InputStreamReader; -import java.io.Reader; -import java.io.PushbackReader; -import java.util.Collection; import java.util.Vector; import java.net.HttpURLConnection; import org.servalproject.servaldna.ServalDHttpConnectionFactory; import org.servalproject.servaldna.ServalDInterfaceException; import org.servalproject.servaldna.SubscriberId; +import org.servalproject.json.JSONTokeniser; +import org.servalproject.json.JSONInputException; public class MeshMSConversationList { private ServalDHttpConnectionFactory httpConnector; private SubscriberId sid; private HttpURLConnection httpConnection; - private PushbackReader reader; + private JSONTokeniser json; private Vector headers; int columnIndex__id; int columnIndex_my_sid; @@ -54,385 +51,127 @@ public class MeshMSConversationList { this.sid = sid; } + public boolean isConnected() + { + return this.json != null; + } + public void connect() throws ServalDInterfaceException, IOException { - columnIndex__id = -1; - columnIndex_my_sid = -1; - columnIndex_their_sid = -1; - columnIndex_read = -1; - columnIndex_last_message = -1; - columnIndex_read_offset = -1; - rowCount = 0; - httpConnection = httpConnector.newServalDHttpConnection("/restful/meshms/" + sid.toHex() + "/conversationlist.json"); - httpConnection.connect(); - reader = new PushbackReader(new InputStreamReader(httpConnection.getInputStream(), "US-ASCII")); - consume(reader, JsonToken.START_OBJECT); - consume(reader, "header"); - consume(reader, JsonToken.COLON); - headers = new Vector(); - consumeArray(reader, headers, String.class); - if (headers.size() < 1) - throw new ServalDInterfaceException("empty JSON headers array"); - for (int i = 0; i < headers.size(); ++i) { - String header = headers.get(i); - if (header.equals("_id")) - columnIndex__id = i; - else if (header.equals("my_sid")) - columnIndex_my_sid = i; - else if (header.equals("their_sid")) - columnIndex_their_sid = i; - else if (header.equals("read")) - columnIndex_read = i; - else if (header.equals("last_message")) - columnIndex_last_message = i; - else if (header.equals("read_offset")) - columnIndex_read_offset = i; + try { + columnIndex__id = -1; + columnIndex_my_sid = -1; + columnIndex_their_sid = -1; + columnIndex_read = -1; + columnIndex_last_message = -1; + columnIndex_read_offset = -1; + rowCount = 0; + httpConnection = httpConnector.newServalDHttpConnection("/restful/meshms/" + sid.toHex() + "/conversationlist.json"); + httpConnection.connect(); + if (httpConnection.getResponseCode() != HttpURLConnection.HTTP_OK) + throw new ServalDInterfaceException("unexpected HTTP response code: " + httpConnection.getResponseCode()); + if (!httpConnection.getContentType().equals("application/json")) + throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + httpConnection.getContentType()); + json = new JSONTokeniser(new InputStreamReader(httpConnection.getInputStream(), "US-ASCII")); + json = new JSONTokeniser(new InputStreamReader(httpConnection.getInputStream(), "US-ASCII")); + json.consume(JSONTokeniser.Token.START_OBJECT); + json.consume("header"); + json.consume(JSONTokeniser.Token.COLON); + headers = new Vector(); + json.consumeArray(headers, String.class); + if (headers.size() < 1) + throw new ServalDInterfaceException("empty JSON headers array"); + for (int i = 0; i < headers.size(); ++i) { + String header = headers.get(i); + if (header.equals("_id")) + columnIndex__id = i; + else if (header.equals("my_sid")) + columnIndex_my_sid = i; + else if (header.equals("their_sid")) + columnIndex_their_sid = i; + else if (header.equals("read")) + columnIndex_read = i; + else if (header.equals("last_message")) + columnIndex_last_message = i; + else if (header.equals("read_offset")) + columnIndex_read_offset = i; + } + if (columnIndex__id == -1) + throw new ServalDInterfaceException("missing JSON column: _id"); + if (columnIndex_my_sid == -1) + throw new ServalDInterfaceException("missing JSON column: my_sid"); + if (columnIndex_their_sid == -1) + throw new ServalDInterfaceException("missing JSON column: their_sid"); + if (columnIndex_read == -1) + throw new ServalDInterfaceException("missing JSON column: read"); + if (columnIndex_last_message == -1) + throw new ServalDInterfaceException("missing JSON column: last_message"); + if (columnIndex_read_offset == -1) + throw new ServalDInterfaceException("missing JSON column: read_offset"); + json.consume(JSONTokeniser.Token.COMMA); + json.consume("rows"); + json.consume(JSONTokeniser.Token.COLON); + json.consume(JSONTokeniser.Token.START_ARRAY); + } + catch (JSONInputException e) { + throw new ServalDInterfaceException(e); } - if (columnIndex__id == -1) - throw new ServalDInterfaceException("missing JSON column: _id"); - if (columnIndex_my_sid == -1) - throw new ServalDInterfaceException("missing JSON column: my_sid"); - if (columnIndex_their_sid == -1) - throw new ServalDInterfaceException("missing JSON column: their_sid"); - if (columnIndex_read == -1) - throw new ServalDInterfaceException("missing JSON column: read"); - if (columnIndex_last_message == -1) - throw new ServalDInterfaceException("missing JSON column: last_message"); - if (columnIndex_read_offset == -1) - throw new ServalDInterfaceException("missing JSON column: read_offset"); - consume(reader, JsonToken.COMMA); - consume(reader, "rows"); - consume(reader, JsonToken.COLON); - consume(reader, JsonToken.START_ARRAY); } public MeshMSConversation nextConversation() throws ServalDInterfaceException, IOException { - Object tok = nextJsonToken(reader); - if (tok == JsonToken.END_ARRAY) { - consume(reader, JsonToken.END_OBJECT); - consume(reader, JsonToken.EOF); - return null; - } - if (rowCount != 0) { - match(tok, JsonToken.COMMA); - tok = nextJsonToken(reader); - } - match(tok, JsonToken.START_ARRAY); - Object[] row = new Object[headers.size()]; - for (int i = 0; i < headers.size(); ++i) { - if (i != 0) - consume(reader, JsonToken.COMMA); - row[i] = consume(reader); - } - consume(reader, JsonToken.END_ARRAY); - int _id = narrow(row[columnIndex__id], Integer.class); - SubscriberId my_sid; try { - my_sid = new SubscriberId(narrow(row[columnIndex_my_sid], String.class)); + Object tok = json.nextToken(); + if (tok == JSONTokeniser.Token.END_ARRAY) { + json.consume(JSONTokeniser.Token.END_OBJECT); + json.consume(JSONTokeniser.Token.EOF); + return null; + } + if (rowCount != 0) { + JSONTokeniser.match(tok, JSONTokeniser.Token.COMMA); + tok = json.nextToken(); + } + JSONTokeniser.match(tok, JSONTokeniser.Token.START_ARRAY); + Object[] row = new Object[headers.size()]; + for (int i = 0; i < headers.size(); ++i) { + if (i != 0) + json.consume(JSONTokeniser.Token.COMMA); + row[i] = json.consume(); + } + json.consume(JSONTokeniser.Token.END_ARRAY); + int _id = JSONTokeniser.narrow(row[columnIndex__id], Integer.class); + SubscriberId my_sid; + try { + my_sid = new SubscriberId(JSONTokeniser.narrow(row[columnIndex_my_sid], String.class)); + } + catch (SubscriberId.InvalidHexException e) { + throw new ServalDInterfaceException("invalid column value: my_sid", e); + } + SubscriberId their_sid; + try { + their_sid = new SubscriberId(JSONTokeniser.narrow(row[columnIndex_their_sid], String.class)); + } + catch (SubscriberId.InvalidHexException e) { + throw new ServalDInterfaceException("invalid column value: their_sid", e); + } + boolean is_read = JSONTokeniser.narrow(row[columnIndex_read], Boolean.class); + int last_message = JSONTokeniser.narrow(row[columnIndex_last_message], Integer.class); + int read_offset = JSONTokeniser.narrow(row[columnIndex_read_offset], Integer.class); + return new MeshMSConversation(rowCount++, _id, my_sid, their_sid, is_read, last_message, read_offset); } - catch (SubscriberId.InvalidHexException e) { - throw new ServalDInterfaceException("invalid JSON column value: my_sid", e); + catch (JSONInputException e) { + throw new ServalDInterfaceException(e); } - SubscriberId their_sid; - try { - their_sid = new SubscriberId(narrow(row[columnIndex_their_sid], String.class)); - } - catch (SubscriberId.InvalidHexException e) { - throw new ServalDInterfaceException("invalid JSON column value: their_sid", e); - } - boolean is_read = narrow(row[columnIndex_read], Boolean.class); - int last_message = narrow(row[columnIndex_last_message], Integer.class); - int read_offset = narrow(row[columnIndex_read_offset], Integer.class); - return new MeshMSConversation(rowCount++, _id, my_sid, their_sid, is_read, last_message, read_offset); } public void close() throws IOException { - if (reader != null) { - reader.close(); - reader = null; - } - } - - static void match(Object tok, JsonToken exactly) throws ServalDInterfaceException - { - if (tok != exactly) - throw new ServalDInterfaceException("unexpected JSON token " + exactly + ", got: " + jsonTokenDescription(tok)); - } - - static void consume(PushbackReader rd, JsonToken exactly) throws ServalDInterfaceException, IOException - { - match(nextJsonToken(rd), exactly); - } - - @SuppressWarnings("unchecked") - static T narrow(Object tok, Class cls) throws ServalDInterfaceException - { - assert !cls.isAssignableFrom(JsonToken.class); // can only narrow to values - if (tok == JsonToken.EOF) - throw new ServalDInterfaceException("unexpected EOF"); - if (tok instanceof JsonToken) - throw new ServalDInterfaceException("expecting JSON " + cls.getName() + ", got: " + tok); - // Convert: - // Integer --> Float or Double - // Float --> Double - // Double --> Float - if (cls == Double.class && (tok instanceof Float || tok instanceof Integer)) - tok = new Double(((Number)tok).doubleValue()); - else if (cls == Float.class && (tok instanceof Double || tok instanceof Integer)) - tok = new Float(((Number)tok).floatValue()); - if (cls.isInstance(tok)) - return (T)tok; - throw new ServalDInterfaceException("expecting JSON " + cls.getName() + ", got: " + jsonTokenDescription(tok)); - } - - static T consume(PushbackReader rd, Class cls) throws ServalDInterfaceException, IOException - { - return narrow(nextJsonToken(rd), cls); - } - - static Object consume(PushbackReader rd) throws ServalDInterfaceException, IOException - { - return consume(rd, Object.class); - } - - static String consume(PushbackReader rd, String exactly) throws ServalDInterfaceException, IOException - { - String tok = consume(rd, String.class); - if (tok.equals(exactly)) - return tok; - throw new ServalDInterfaceException("unexpected JSON String \"" + exactly + "\", got: " + jsonTokenDescription(tok)); - } - - static int consumeArray(PushbackReader rd, Collection collection, Class cls) throws ServalDInterfaceException, IOException - { - int added = 0; - consume(rd, JsonToken.START_ARRAY); - Object tok = nextJsonToken(rd); - if (tok != JsonToken.END_ARRAY) { - while (true) { - try { - collection.add(narrow(tok, cls)); - ++added; - } - catch (ClassCastException e) { - throw new ServalDInterfaceException("unexpected JSON token: " + jsonTokenDescription(tok)); - } - tok = nextJsonToken(rd); - if (tok == JsonToken.END_ARRAY) - break; - match(tok, JsonToken.COMMA); - tok = nextJsonToken(rd); - } - } - return added; - } - - enum JsonToken { - START_OBJECT, - END_OBJECT, - START_ARRAY, - END_ARRAY, - COMMA, - COLON, - NULL, - EOF - }; - - static boolean jsonIsToken(Object tok) - { - return tok instanceof JsonToken || tok instanceof String || tok instanceof Double || tok instanceof Integer || tok instanceof Boolean; - } - - static String jsonTokenDescription(Object tok) - { - if (tok instanceof String) - return "\"" + tok + "\""; - if (tok instanceof Number) - return "" + tok; - if (tok instanceof Boolean) - return "" + tok; - assert tok instanceof JsonToken; - return tok.toString(); - } - - static void readAll(Reader rd, char[] word) throws ServalDInterfaceException, IOException - { - int len = 0; - while (len < word.length) { - int n = rd.read(word, len, word.length - len); - if (n == -1) - throw new ServalDInterfaceException("unexpected EOF"); - len += n; - } - } - - static Object nextJsonToken(PushbackReader rd) throws ServalDInterfaceException, IOException - { - while (true) { - int c = rd.read(); - switch (c) { - case -1: - return JsonToken.EOF; - case '\t': - case '\r': - case '\n': - case ' ': - break; - case '{': - return JsonToken.START_OBJECT; - case '}': - return JsonToken.END_OBJECT; - case '[': - return JsonToken.START_ARRAY; - case ']': - return JsonToken.END_ARRAY; - case ',': - return JsonToken.COMMA; - case ':': - return JsonToken.COLON; - case 't': { - char[] word = new char[3]; - readAll(rd, word); - if (word[0] == 'r' && word[1] == 'u' && word[2] == 'e') - return Boolean.TRUE; - } - throw new ServalDInterfaceException("malformed JSON"); - case 'f': { - char[] word = new char[4]; - readAll(rd, word); - if (word[0] == 'a' && word[1] == 'l' && word[2] == 's' && word[3] == 'e') - return Boolean.FALSE; - } - throw new ServalDInterfaceException("malformed JSON"); - case 'n': { - char[] word = new char[3]; - readAll(rd, word); - if (word[0] == 'u' && word[1] == 'l' && word[2] == 'l') - return JsonToken.NULL; - } - throw new ServalDInterfaceException("malformed JSON"); - case '"': { - StringBuilder sb = new StringBuilder(); - boolean slosh = false; - while (true) { - c = rd.read(); - if (c == -1) - throw new ServalDInterfaceException("unexpected EOF in JSON string"); - if (slosh) { - switch (c) { - case '"': case '/': case '\\': sb.append('"'); break; - case 'b': sb.append('\b'); break; - case 'f': sb.append('\f'); break; - case 'n': sb.append('\n'); break; - case 'r': sb.append('\r'); break; - case 't': sb.append('\t'); break; - case 'u': - char[] hex = new char[4]; - readAll(rd, hex); - int code = Integer.valueOf(new String(hex), 16); - if (code >= 0 && code <= 0xffff) { - sb.append((char)code); - break; - } - // fall through - default: - throw new ServalDInterfaceException("malformed JSON string"); - } - } - else { - switch (c) { - case '"': - return sb.toString(); - case '\\': - slosh = true; - break; - default: - sb.append((char)c); - break; - } - } - } - } - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - case '-': { - StringBuilder sb = new StringBuilder(); - if (c == '-') { - sb.append((char)c); - c = rd.read(); - } - if (c == '0') { - sb.append((char)c); - c = rd.read(); - } - else if (Character.isDigit(c)) { - do { - sb.append((char)c); - c = rd.read(); - } - while (Character.isDigit(c)); - } - else - throw new ServalDInterfaceException("malformed JSON number"); - boolean isfloat = false; - if (c == '.') { - isfloat = true; - sb.append((char)c); - c = rd.read(); - if (c == -1) - throw new ServalDInterfaceException("unexpected EOF in JSON number"); - if (!Character.isDigit(c)) - throw new ServalDInterfaceException("malformed JSON number"); - do { - sb.append((char)c); - c = rd.read(); - } - while (Character.isDigit(c)); - } - if (c == 'e' || c == 'E') { - isfloat = true; - sb.append((char)c); - c = rd.read(); - if (c == '+' || c == '-') { - sb.append((char)c); - c = rd.read(); - } - if (c == -1) - throw new ServalDInterfaceException("unexpected EOF in JSON number"); - if (!Character.isDigit(c)) - throw new ServalDInterfaceException("malformed JSON number"); - do { - sb.append((char)c); - c = rd.read(); - } - while (Character.isDigit(c)); - } - rd.unread(c); - String number = sb.toString(); - try { - if (isfloat) - return Double.parseDouble(number); - else - return Integer.parseInt(number); - } - catch (NumberFormatException e) { - throw new ServalDInterfaceException("malformed JSON number: " + number); - } - } - default: - throw new ServalDInterfaceException("malformed JSON: '" + (char)c + "'"); - } + httpConnection = null; + if (json != null) { + json.close(); + json = null; } + headers = null; } } From e9437e9a61ad6248c6ae0084521ffafc51c09b68 Mon Sep 17 00:00:00 2001 From: Andrew Bettison Date: Tue, 17 Jun 2014 16:04:30 +0930 Subject: [PATCH 6/6] MeshMS Java API: list messages --- .../org/servalproject/json/JSONTokeniser.java | 132 +++++++++-- .../servaldna/ServalDClient.java | 11 +- .../servaldna/meshms/MeshMSCommon.java | 72 ++++++ .../meshms/MeshMSConversationList.java | 24 +- .../servaldna/meshms/MeshMSException.java | 40 ++++ .../servaldna/meshms/MeshMSMessage.java | 78 +++++++ .../servaldna/meshms/MeshMSMessageList.java | 220 ++++++++++++++++++ .../meshms/MeshMSProtocolFaultException.java | 39 ++++ .../MeshMSUnknownIdentityException.java | 39 ++++ java/org/servalproject/test/Meshms.java | 50 +++- tests/meshmsjava | 85 ++++++- 11 files changed, 741 insertions(+), 49 deletions(-) create mode 100644 java/org/servalproject/servaldna/meshms/MeshMSCommon.java create mode 100644 java/org/servalproject/servaldna/meshms/MeshMSException.java create mode 100644 java/org/servalproject/servaldna/meshms/MeshMSMessage.java create mode 100644 java/org/servalproject/servaldna/meshms/MeshMSMessageList.java create mode 100644 java/org/servalproject/servaldna/meshms/MeshMSProtocolFaultException.java create mode 100644 java/org/servalproject/servaldna/meshms/MeshMSUnknownIdentityException.java diff --git a/java/org/servalproject/json/JSONTokeniser.java b/java/org/servalproject/json/JSONTokeniser.java index 144ff817..d639e50e 100644 --- a/java/org/servalproject/json/JSONTokeniser.java +++ b/java/org/servalproject/json/JSONTokeniser.java @@ -30,6 +30,9 @@ import java.util.Collection; public class JSONTokeniser { PushbackReader reader; + Object pushedToken; + + private static final boolean DUMP_JSON_TO_STDERR = false; public enum Token { START_OBJECT, @@ -96,6 +99,22 @@ public class JSONTokeniser { reader = new PushbackReader(rd); } + private int _read() throws IOException + { + int n = this.reader.read(); + if (DUMP_JSON_TO_STDERR && n != -1) + System.err.print((char)n); + return n; + } + + private int _read(char[] buf, int offset, int length) throws IOException + { + int n = this.reader.read(buf, offset, length); + if (DUMP_JSON_TO_STDERR && n != -1) + System.err.print(new String(buf, offset, n)); + return n; + } + public static void match(Object tok, Token exactly) throws SyntaxException { if (tok != exactly) @@ -107,12 +126,24 @@ public class JSONTokeniser { match(nextToken(), exactly); } - @SuppressWarnings("unchecked") + public enum Narrow { + NO_NULL, + ALLOW_NULL + }; + public static T narrow(Object tok, Class cls) throws UnexpectedException + { + return narrow(tok, cls, Narrow.NO_NULL); + } + + @SuppressWarnings("unchecked") + public static T narrow(Object tok, Class cls, Narrow opts) throws UnexpectedException { assert !cls.isAssignableFrom(Token.class); // can only narrow to values if (tok == Token.EOF) throw new UnexpectedEOFException(cls); + if (opts == Narrow.ALLOW_NULL && (tok == null || tok == Token.NULL)) + return null; if (tok instanceof Token) throw new UnexpectedTokenException(tok, cls); // Convert: @@ -124,18 +155,28 @@ public class JSONTokeniser { else if (cls == Float.class && (tok instanceof Double || tok instanceof Integer)) tok = new Float(((Number)tok).floatValue()); if (cls.isInstance(tok)) - return (T)tok; + return (T)tok; // unchecked cast throw new UnexpectedTokenException(tok, cls); } public T consume(Class cls) throws SyntaxException, UnexpectedException, IOException { - return narrow(nextToken(), cls); + return consume(cls, Narrow.NO_NULL); + } + + public T consume(Class cls, Narrow opts) throws SyntaxException, UnexpectedException, IOException + { + return narrow(nextToken(), cls, opts); } public Object consume() throws SyntaxException, UnexpectedException, IOException { - return consume(Object.class); + return consume(Object.class, Narrow.NO_NULL); + } + + public Object consume(Narrow opts) throws SyntaxException, UnexpectedException, IOException + { + return consume(Object.class, opts); } public String consume(String exactly) throws SyntaxException, UnexpectedException, IOException @@ -146,14 +187,24 @@ public class JSONTokeniser { throw new UnexpectedTokenException(tok, exactly); } + public int consumeArray(Collection collection, Narrow opts) throws SyntaxException, UnexpectedException, IOException + { + return consumeArray(collection, Object.class, opts); + } + public int consumeArray(Collection collection, Class cls) throws SyntaxException, UnexpectedException, IOException + { + return consumeArray(collection, cls, Narrow.NO_NULL); + } + + public int consumeArray(Collection collection, Class cls, Narrow opts) throws SyntaxException, UnexpectedException, IOException { int added = 0; consume(Token.START_ARRAY); Object tok = nextToken(); if (tok != Token.END_ARRAY) { while (true) { - collection.add(narrow(tok, cls)); + collection.add(narrow(tok, cls, opts)); ++added; tok = nextToken(); if (tok == Token.END_ARRAY) @@ -165,6 +216,27 @@ public class JSONTokeniser { return added; } + public void consumeArray(Object[] array) throws SyntaxException, UnexpectedException, IOException + { + consumeArray(array, Object.class, Narrow.NO_NULL); + } + + public void consumeArray(Object[] array, Narrow opts) throws SyntaxException, UnexpectedException, IOException + { + consumeArray(array, Object.class, opts); + } + + public void consumeArray(T[] array, Class cls, Narrow opts) throws SyntaxException, UnexpectedException, IOException + { + consume(Token.START_ARRAY); + for (int i = 0; i < array.length; ++i) { + if (i != 0) + consume(Token.COMMA); + array[i] = consume(cls, opts); + } + consume(Token.END_ARRAY); + } + public static boolean jsonIsToken(Object tok) { return tok instanceof Token || tok instanceof String || tok instanceof Double || tok instanceof Integer || tok instanceof Boolean; @@ -172,6 +244,8 @@ public class JSONTokeniser { public static String jsonTokenDescription(Object tok) { + if (tok == null) + return "null"; if (tok instanceof String) return "\"" + tok + "\""; if (tok instanceof Number) @@ -187,7 +261,7 @@ public class JSONTokeniser { int len = 0; while (len < word.length()) { char[] buf = new char[word.length() - len]; - int n = this.reader.read(buf, 0, buf.length); + int n = _read(buf, 0, buf.length); if (n == -1) throw new SyntaxException("EOF in middle of \"" + word + "\""); for (int i = 0; i < n; ++i) @@ -201,7 +275,7 @@ public class JSONTokeniser { char[] buf = new char[digits]; int len = 0; while (len < buf.length) { - int n = this.reader.read(buf, len, buf.length - len); + int n = _read(buf, len, buf.length - len); if (n == -1) throw new SyntaxException("EOF in middle of " + digits + " hex digits"); len += n; @@ -215,10 +289,22 @@ public class JSONTokeniser { } } + public void pushToken(Object tok) + { + assert jsonIsToken(tok); + assert pushedToken == null; + pushedToken = tok; + } + public Object nextToken() throws SyntaxException, IOException { + if (pushedToken != null) { + Object tok = pushedToken; + pushedToken = null; + return tok; + } while (true) { - int c = this.reader.read(); + int c = _read(); switch (c) { case -1: return Token.EOF; @@ -255,25 +341,21 @@ public class JSONTokeniser { StringBuilder sb = new StringBuilder(); boolean slosh = false; while (true) { - c = this.reader.read(); + c = _read(); if (c == -1) throw new SyntaxException("unexpected EOF in JSON string"); if (slosh) { switch (c) { - case '"': case '/': case '\\': sb.append('"'); break; + case '"': case '/': case '\\': sb.append((char)c); break; case 'b': sb.append('\b'); break; case 'f': sb.append('\f'); break; case 'n': sb.append('\n'); break; case 'r': sb.append('\r'); break; case 't': sb.append('\t'); break; - case 'u': - - int code = readHex(4); - sb.append((char)code); - // fall through - default: - throw new SyntaxException("malformed JSON string"); + case 'u': sb.append((char)readHex(4)); break; + default: throw new SyntaxException("malformed JSON string"); } + slosh = false; } else { switch (c) { @@ -303,16 +385,16 @@ public class JSONTokeniser { StringBuilder sb = new StringBuilder(); if (c == '-') { sb.append((char)c); - c = this.reader.read(); + c = _read(); } if (c == '0') { sb.append((char)c); - c = this.reader.read(); + c = _read(); } else if (Character.isDigit(c)) { do { sb.append((char)c); - c = this.reader.read(); + c = _read(); } while (Character.isDigit(c)); } @@ -322,24 +404,24 @@ public class JSONTokeniser { if (c == '.') { isfloat = true; sb.append((char)c); - c = this.reader.read(); + c = _read(); if (c == -1) throw new SyntaxException("unexpected EOF in JSON number"); if (!Character.isDigit(c)) throw new SyntaxException("malformed JSON number"); do { sb.append((char)c); - c = this.reader.read(); + c = _read(); } while (Character.isDigit(c)); } if (c == 'e' || c == 'E') { isfloat = true; sb.append((char)c); - c = this.reader.read(); + c = _read(); if (c == '+' || c == '-') { sb.append((char)c); - c = this.reader.read(); + c = _read(); } if (c == -1) throw new SyntaxException("unexpected EOF in JSON number"); @@ -347,7 +429,7 @@ public class JSONTokeniser { throw new SyntaxException("malformed JSON number"); do { sb.append((char)c); - c = this.reader.read(); + c = _read(); } while (Character.isDigit(c)); } diff --git a/java/org/servalproject/servaldna/ServalDClient.java b/java/org/servalproject/servaldna/ServalDClient.java index 21c71dba..b6c22baa 100644 --- a/java/org/servalproject/servaldna/ServalDClient.java +++ b/java/org/servalproject/servaldna/ServalDClient.java @@ -30,6 +30,8 @@ import org.servalproject.servaldna.SubscriberId; import org.servalproject.servaldna.ServalDCommand; import org.servalproject.servaldna.ServalDInterfaceException; import org.servalproject.servaldna.meshms.MeshMSConversationList; +import org.servalproject.servaldna.meshms.MeshMSMessageList; +import org.servalproject.servaldna.meshms.MeshMSException; public class ServalDClient implements ServalDHttpConnectionFactory { @@ -86,13 +88,20 @@ public class ServalDClient implements ServalDHttpConnectionFactory throw new ServalDInterfaceException("restful password not set"); } - public MeshMSConversationList meshmsListConversations(SubscriberId sid) throws ServalDInterfaceException, IOException + public MeshMSConversationList meshmsListConversations(SubscriberId sid) throws ServalDInterfaceException, IOException, MeshMSException { MeshMSConversationList list = new MeshMSConversationList(this, sid); list.connect(); return list; } + public MeshMSMessageList meshmsListMessages(SubscriberId sid1, SubscriberId sid2) throws IOException, ServalDInterfaceException, MeshMSException + { + MeshMSMessageList list = new MeshMSMessageList(this, sid1, sid2); + list.connect(); + return list; + } + // interface ServalDHttpConnectionFactory public HttpURLConnection newServalDHttpConnection(String path) throws ServalDInterfaceException, IOException { diff --git a/java/org/servalproject/servaldna/meshms/MeshMSCommon.java b/java/org/servalproject/servaldna/meshms/MeshMSCommon.java new file mode 100644 index 00000000..dbe89667 --- /dev/null +++ b/java/org/servalproject/servaldna/meshms/MeshMSCommon.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.meshms; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import org.servalproject.servaldna.ServalDInterfaceException; +import org.servalproject.json.JSONTokeniser; +import org.servalproject.json.JSONInputException; + +class MeshMSCommon +{ + protected static JSONTokeniser connectMeshMSRestful(HttpURLConnection conn) throws IOException, ServalDInterfaceException, MeshMSException + { + conn.connect(); + if (!conn.getContentType().equals("application/json")) + throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType()); + if (conn.getResponseCode() == HttpURLConnection.HTTP_FORBIDDEN) { + JSONTokeniser json = new JSONTokeniser(new InputStreamReader(conn.getErrorStream(), "US-ASCII")); + try { + json.consume(JSONTokeniser.Token.START_OBJECT); + json.consume("http_status_code"); + json.consume(JSONTokeniser.Token.COLON); + json.consume(Integer.class); + json.consume(JSONTokeniser.Token.COMMA); + json.consume("http_status_message"); + json.consume(JSONTokeniser.Token.COLON); + String message = json.consume(String.class); + json.consume(JSONTokeniser.Token.COMMA); + json.consume("meshms_status_code"); + json.consume(JSONTokeniser.Token.COLON); + int meshms_status = json.consume(Integer.class); + json.consume(JSONTokeniser.Token.END_OBJECT); + json.consume(JSONTokeniser.Token.EOF); + switch (meshms_status) { + case 2: + throw new MeshMSUnknownIdentityException(conn.getURL()); + case 3: + throw new MeshMSProtocolFaultException(conn.getURL()); + } + throw new ServalDInterfaceException("unexpected MeshMS status = " + meshms_status + ", \"" + message + "\""); + } + catch (JSONInputException e) { + throw new ServalDInterfaceException("malformed response body for HTTP status code " + conn.getResponseCode(), e); + } + } + if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) + throw new ServalDInterfaceException("unexpected HTTP response code: " + conn.getResponseCode()); + JSONTokeniser json = new JSONTokeniser(new InputStreamReader(conn.getInputStream(), "US-ASCII")); + return json; + } + +} diff --git a/java/org/servalproject/servaldna/meshms/MeshMSConversationList.java b/java/org/servalproject/servaldna/meshms/MeshMSConversationList.java index 2abc63f9..05173b1f 100644 --- a/java/org/servalproject/servaldna/meshms/MeshMSConversationList.java +++ b/java/org/servalproject/servaldna/meshms/MeshMSConversationList.java @@ -56,7 +56,7 @@ public class MeshMSConversationList { return this.json != null; } - public void connect() throws ServalDInterfaceException, IOException + public void connect() throws IOException, ServalDInterfaceException, MeshMSException { try { columnIndex__id = -1; @@ -67,13 +67,7 @@ public class MeshMSConversationList { columnIndex_read_offset = -1; rowCount = 0; httpConnection = httpConnector.newServalDHttpConnection("/restful/meshms/" + sid.toHex() + "/conversationlist.json"); - httpConnection.connect(); - if (httpConnection.getResponseCode() != HttpURLConnection.HTTP_OK) - throw new ServalDInterfaceException("unexpected HTTP response code: " + httpConnection.getResponseCode()); - if (!httpConnection.getContentType().equals("application/json")) - throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + httpConnection.getContentType()); - json = new JSONTokeniser(new InputStreamReader(httpConnection.getInputStream(), "US-ASCII")); - json = new JSONTokeniser(new InputStreamReader(httpConnection.getInputStream(), "US-ASCII")); + json = MeshMSCommon.connectMeshMSRestful(httpConnection); json.consume(JSONTokeniser.Token.START_OBJECT); json.consume("header"); json.consume(JSONTokeniser.Token.COLON); @@ -127,18 +121,12 @@ public class MeshMSConversationList { json.consume(JSONTokeniser.Token.EOF); return null; } - if (rowCount != 0) { + if (rowCount != 0) JSONTokeniser.match(tok, JSONTokeniser.Token.COMMA); - tok = json.nextToken(); - } - JSONTokeniser.match(tok, JSONTokeniser.Token.START_ARRAY); + else + json.pushToken(tok); Object[] row = new Object[headers.size()]; - for (int i = 0; i < headers.size(); ++i) { - if (i != 0) - json.consume(JSONTokeniser.Token.COMMA); - row[i] = json.consume(); - } - json.consume(JSONTokeniser.Token.END_ARRAY); + json.consumeArray(row); int _id = JSONTokeniser.narrow(row[columnIndex__id], Integer.class); SubscriberId my_sid; try { diff --git a/java/org/servalproject/servaldna/meshms/MeshMSException.java b/java/org/servalproject/servaldna/meshms/MeshMSException.java new file mode 100644 index 00000000..33865764 --- /dev/null +++ b/java/org/servalproject/servaldna/meshms/MeshMSException.java @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.meshms; + +import java.net.URL; + +/** + * Thrown when a MeshMS API encounters an exceptional condition. This exception is subclassed for + * specific causes. + * + * @author Andrew Bettison + */ +public abstract class MeshMSException extends Exception +{ + public final URL url; + + public MeshMSException(String message, URL url) { + super(message + "; " + url); + this.url = url; + } + +} diff --git a/java/org/servalproject/servaldna/meshms/MeshMSMessage.java b/java/org/servalproject/servaldna/meshms/MeshMSMessage.java new file mode 100644 index 00000000..f104b621 --- /dev/null +++ b/java/org/servalproject/servaldna/meshms/MeshMSMessage.java @@ -0,0 +1,78 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.meshms; + +import org.servalproject.servaldna.SubscriberId; +import org.servalproject.servaldna.ServalDInterfaceException; + +public class MeshMSMessage { + + public enum Type { + MESSAGE_SENT, + MESSAGE_RECEIVED, + ACK_RECEIVED + }; + + public final int _rowNumber; + public final Type type; + public final SubscriberId mySid; + public final SubscriberId theirSid; + public final int offset; + public final String token; + public final String text; + public final boolean isDelivered; + public final boolean isRead; + public final Integer ackOffset; + + protected MeshMSMessage(int rowNumber, + Type type, + SubscriberId my_sid, + SubscriberId their_sid, + int offset, + String token, + String text, + boolean delivered, + boolean read, + Integer ack_offset) throws ServalDInterfaceException + { + if (my_sid == null) + throw new ServalDInterfaceException("my_sid is null"); + if (their_sid == null) + throw new ServalDInterfaceException("their_sid is null"); + if (type != Type.ACK_RECEIVED && text == null) + throw new ServalDInterfaceException("text is null"); + if (token == null) + throw new ServalDInterfaceException("token is null"); + if (type == Type.ACK_RECEIVED && ack_offset == null) + throw new ServalDInterfaceException("ack_offset is null"); + this._rowNumber = rowNumber; + this.type = type; + this.mySid = my_sid; + this.theirSid = their_sid; + this.offset = offset; + this.token = token; + this.text = text; + this.isDelivered = delivered; + this.isRead = read; + this.ackOffset = ack_offset; + } + +} diff --git a/java/org/servalproject/servaldna/meshms/MeshMSMessageList.java b/java/org/servalproject/servaldna/meshms/MeshMSMessageList.java new file mode 100644 index 00000000..fdb2a6dc --- /dev/null +++ b/java/org/servalproject/servaldna/meshms/MeshMSMessageList.java @@ -0,0 +1,220 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.meshms; + +import java.io.IOException; +import java.util.Vector; +import java.net.HttpURLConnection; +import org.servalproject.servaldna.ServalDHttpConnectionFactory; +import org.servalproject.servaldna.ServalDInterfaceException; +import org.servalproject.servaldna.SubscriberId; +import org.servalproject.json.JSONTokeniser; +import org.servalproject.json.JSONInputException; + +public class MeshMSMessageList { + + private ServalDHttpConnectionFactory httpConnector; + private SubscriberId my_sid; + private SubscriberId their_sid; + private HttpURLConnection httpConnection; + private JSONTokeniser json; + private int readOffset; + private int latestAckOffset; + private Vector headers; + private int columnIndex_type; + private int columnIndex_my_sid; + private int columnIndex_their_sid; + private int columnIndex_offset; + private int columnIndex_token; + private int columnIndex_text; + private int columnIndex_delivered; + private int columnIndex_read; + private int columnIndex_ack_offset; + private int rowCount; + + public MeshMSMessageList(ServalDHttpConnectionFactory connector, SubscriberId my_sid, SubscriberId their_sid) + { + this.httpConnector = connector; + this.my_sid = my_sid; + this.their_sid = their_sid; + } + + public boolean isConnected() + { + return this.json != null; + } + + public void connect() throws MeshMSException, ServalDInterfaceException, IOException + { + assert json == null; + try { + columnIndex_type = -1; + columnIndex_my_sid = -1; + columnIndex_their_sid = -1; + columnIndex_offset = -1; + columnIndex_token = -1; + columnIndex_text = -1; + columnIndex_delivered = -1; + columnIndex_read = -1; + columnIndex_ack_offset = -1; + rowCount = 0; + httpConnection = httpConnector.newServalDHttpConnection("/restful/meshms/" + my_sid.toHex() + "/" + their_sid.toHex() + "/messagelist.json"); + json = MeshMSCommon.connectMeshMSRestful(httpConnection); + json.consume(JSONTokeniser.Token.START_OBJECT); + json.consume("read_offset"); + json.consume(JSONTokeniser.Token.COLON); + readOffset = json.consume(Integer.class); + json.consume(JSONTokeniser.Token.COMMA); + json.consume("latest_ack_offset"); + json.consume(JSONTokeniser.Token.COLON); + latestAckOffset = json.consume(Integer.class); + json.consume(JSONTokeniser.Token.COMMA); + json.consume("header"); + json.consume(JSONTokeniser.Token.COLON); + headers = new Vector(); + json.consumeArray(headers, String.class); + if (headers.size() < 1) + throw new ServalDInterfaceException("empty JSON headers array"); + for (int i = 0; i < headers.size(); ++i) { + String header = headers.get(i); + if (header.equals("type")) + columnIndex_type = i; + else if (header.equals("my_sid")) + columnIndex_my_sid = i; + else if (header.equals("their_sid")) + columnIndex_their_sid = i; + else if (header.equals("offset")) + columnIndex_offset = i; + else if (header.equals("token")) + columnIndex_token = i; + else if (header.equals("text")) + columnIndex_text = i; + else if (header.equals("delivered")) + columnIndex_delivered = i; + else if (header.equals("read")) + columnIndex_read = i; + else if (header.equals("ack_offset")) + columnIndex_ack_offset = i; + } + if (columnIndex_type == -1) + throw new ServalDInterfaceException("missing JSON column: type"); + if (columnIndex_my_sid == -1) + throw new ServalDInterfaceException("missing JSON column: my_sid"); + if (columnIndex_their_sid == -1) + throw new ServalDInterfaceException("missing JSON column: their_sid"); + if (columnIndex_offset == -1) + throw new ServalDInterfaceException("missing JSON column: offset"); + if (columnIndex_token == -1) + throw new ServalDInterfaceException("missing JSON column: token"); + if (columnIndex_text == -1) + throw new ServalDInterfaceException("missing JSON column: text"); + if (columnIndex_delivered == -1) + throw new ServalDInterfaceException("missing JSON column: delivered"); + if (columnIndex_read == -1) + throw new ServalDInterfaceException("missing JSON column: read"); + if (columnIndex_ack_offset == -1) + throw new ServalDInterfaceException("missing JSON column: ack_offset"); + json.consume(JSONTokeniser.Token.COMMA); + json.consume("rows"); + json.consume(JSONTokeniser.Token.COLON); + json.consume(JSONTokeniser.Token.START_ARRAY); + } + catch (JSONInputException e) { + throw new ServalDInterfaceException(e); + } + } + + public int getReadOffset() + { + assert json != null; + return readOffset; + } + + public int getLatestAckOffset() + { + assert json != null; + return latestAckOffset; + } + + public MeshMSMessage nextMessage() throws ServalDInterfaceException, IOException + { + assert json != null; + try { + Object tok = json.nextToken(); + if (tok == JSONTokeniser.Token.END_ARRAY) { + json.consume(JSONTokeniser.Token.END_OBJECT); + json.consume(JSONTokeniser.Token.EOF); + return null; + } + if (rowCount != 0) + JSONTokeniser.match(tok, JSONTokeniser.Token.COMMA); + else + json.pushToken(tok); + Object[] row = new Object[headers.size()]; + json.consumeArray(row, JSONTokeniser.Narrow.ALLOW_NULL); + String typesym = JSONTokeniser.narrow(row[columnIndex_type], String.class); + SubscriberId my_sid; + try { + my_sid = new SubscriberId(JSONTokeniser.narrow(row[columnIndex_my_sid], String.class)); + } + catch (SubscriberId.InvalidHexException e) { + throw new ServalDInterfaceException("invalid column value: my_sid", e); + } + SubscriberId their_sid; + try { + their_sid = new SubscriberId(JSONTokeniser.narrow(row[columnIndex_their_sid], String.class)); + } + catch (SubscriberId.InvalidHexException e) { + throw new ServalDInterfaceException("invalid column value: their_sid", e); + } + int offset = JSONTokeniser.narrow(row[columnIndex_offset], Integer.class); + String token = JSONTokeniser.narrow(row[columnIndex_token], String.class); + String text = JSONTokeniser.narrow(row[columnIndex_text], String.class, JSONTokeniser.Narrow.ALLOW_NULL); + boolean is_delivered = JSONTokeniser.narrow(row[columnIndex_delivered], Boolean.class); + boolean is_read = JSONTokeniser.narrow(row[columnIndex_read], Boolean.class); + Integer ack_offset = JSONTokeniser.narrow(row[columnIndex_ack_offset], Integer.class, JSONTokeniser.Narrow.ALLOW_NULL); + MeshMSMessage.Type type; + if (typesym.equals(">")) + type = MeshMSMessage.Type.MESSAGE_SENT; + else if (typesym.equals("<")) + type = MeshMSMessage.Type.MESSAGE_RECEIVED; + else if (typesym.equals("ACK")) + type = MeshMSMessage.Type.ACK_RECEIVED; + else + throw new ServalDInterfaceException("invalid column value: type=" + typesym); + return new MeshMSMessage(rowCount++, type, my_sid, their_sid, offset, token, text, is_delivered, is_read, ack_offset); + } + catch (JSONInputException e) { + throw new ServalDInterfaceException(e); + } + } + + public void close() throws IOException + { + httpConnection = null; + if (json != null) { + json.close(); + json = null; + } + headers = null; + } + +} diff --git a/java/org/servalproject/servaldna/meshms/MeshMSProtocolFaultException.java b/java/org/servalproject/servaldna/meshms/MeshMSProtocolFaultException.java new file mode 100644 index 00000000..99853e08 --- /dev/null +++ b/java/org/servalproject/servaldna/meshms/MeshMSProtocolFaultException.java @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.meshms; + +import java.net.URL; + +/** + * Thrown when a MeshMS API method is used to request a message for an unknown identity. + * This is not an error in the Serval DNA interface, so it is not a subclass of + * ServalDInterfaceException, so the programmer must explicitly deal with it instead of just + * absorbing it as an interface malfunction. + * + * @author Andrew Bettison + */ +public class MeshMSProtocolFaultException extends MeshMSException +{ + public MeshMSProtocolFaultException(URL url) { + super("MeshMS protocol fault", url); + } + +} diff --git a/java/org/servalproject/servaldna/meshms/MeshMSUnknownIdentityException.java b/java/org/servalproject/servaldna/meshms/MeshMSUnknownIdentityException.java new file mode 100644 index 00000000..67976382 --- /dev/null +++ b/java/org/servalproject/servaldna/meshms/MeshMSUnknownIdentityException.java @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2014 Serval Project Inc. + * + * This file is part of Serval Software (http://www.servalproject.org) + * + * Serval Software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.servalproject.servaldna.meshms; + +import java.net.URL; + +/** + * Thrown when a MeshMS API method is used to request a message for an unknown identity. + * This is not an error in the Serval DNA interface, so it is not a subclass of + * ServalDInterfaceException, so the programmer must explicitly deal with it instead of just + * absorbing it as an interface malfunction. + * + * @author Andrew Bettison + */ +public class MeshMSUnknownIdentityException extends MeshMSException +{ + public MeshMSUnknownIdentityException(URL url) { + super("unknown identity", url); + } + +} diff --git a/java/org/servalproject/test/Meshms.java b/java/org/servalproject/test/Meshms.java index ee786328..6731b4e7 100644 --- a/java/org/servalproject/test/Meshms.java +++ b/java/org/servalproject/test/Meshms.java @@ -28,14 +28,18 @@ import org.servalproject.servaldna.ServalDClient; import org.servalproject.servaldna.ServalDInterfaceException; import org.servalproject.servaldna.meshms.MeshMSConversationList; import org.servalproject.servaldna.meshms.MeshMSConversation; +import org.servalproject.servaldna.meshms.MeshMSMessageList; +import org.servalproject.servaldna.meshms.MeshMSMessage; +import org.servalproject.servaldna.meshms.MeshMSException; public class Meshms { - static void meshms_list_conversations(SubscriberId sid, String offset, String count) throws ServalDInterfaceException, IOException, InterruptedException + static void meshms_list_conversations(SubscriberId sid) throws ServalDInterfaceException, IOException, InterruptedException { ServalDClient client = ServalDClient.newServalDClient(); - MeshMSConversationList list = client.meshmsListConversations(sid); + MeshMSConversationList list = null; try { + list = client.meshmsListConversations(sid); MeshMSConversation conv; while ((conv = list.nextConversation()) != null) { System.out.println( @@ -48,8 +52,44 @@ public class Meshms { ); } } + catch (MeshMSException e) { + System.out.println(e.toString()); + } finally { - list.close(); + if (list != null) + list.close(); + } + System.exit(0); + } + + static void meshms_list_messages(SubscriberId sid1, SubscriberId sid2) throws ServalDInterfaceException, IOException, InterruptedException + { + ServalDClient client = ServalDClient.newServalDClient(); + MeshMSMessageList list = null; + try { + list = client.meshmsListMessages(sid1, sid2); + System.out.println("read_offset=" + list.getReadOffset()); + System.out.println("latest_ack_offset=" + list.getLatestAckOffset()); + MeshMSMessage msg; + while ((msg = list.nextMessage()) != null) { + System.out.println("type=" + msg.type + + ", my_sid=" + msg.mySid + + ", their_sid=" + msg.theirSid + + ", offset=" + msg.offset + + ", token=" + msg.token + + ", text=" + (msg.text == null ? null : msg.text.replace('\n', '.').replace(' ', '.')) + + ", delivered=" + msg.isDelivered + + ", read=" + msg.isRead + + ", ack_offset=" + msg.ackOffset + ); + } + } + catch (MeshMSException e) { + System.out.println(e.toString()); + } + finally { + if (list != null) + list.close(); } System.exit(0); } @@ -61,7 +101,9 @@ public class Meshms { String methodName = args[0]; try { if (methodName.equals("meshms-list-conversations")) - meshms_list_conversations(new SubscriberId(args[1]), args.length > 2 ? args[2] : null, args.length > 3 ? args[3] : null); + meshms_list_conversations(new SubscriberId(args[1])); + else if (methodName.equals("meshms-list-messages")) + meshms_list_messages(new SubscriberId(args[1]), new SubscriberId(args[2])); } catch (Exception e) { e.printStackTrace(); System.exit(1); diff --git a/tests/meshmsjava b/tests/meshmsjava index 57ffda50..248591b0 100755 --- a/tests/meshmsjava +++ b/tests/meshmsjava @@ -21,6 +21,7 @@ source "${0%/*}/../testframework.sh" source "${0%/*}/../testdefs.sh" source "${0%/*}/../testdefs_java.sh" +source "${0%/*}/../testdefs_meshms.sh" setup() { setup_servald @@ -30,7 +31,7 @@ setup() { executeOk_servald config \ set log.console.level debug \ set debug.httpd on - create_identities 5 + create_identities 4 configure_servald_server() { add_servald_interface executeOk_servald config \ @@ -57,6 +58,7 @@ setup_MeshmsListConversations() { } test_MeshmsListConversations() { executeJavaOk org.servalproject.test.Meshms meshms-list-conversations $SIDA1 + tfw_cat --stderr assertStdoutLineCount '==' 3 assertStdoutGrep "my_sid=$SIDA1, their_sid=$SIDA2, read=true, last_message=0, read_offset=0" assertStdoutGrep "my_sid=$SIDA1, their_sid=$SIDA3, read=false, last_message=14, read_offset=0" @@ -69,4 +71,85 @@ test_MeshmsListConversations() { assertStdoutGrep "my_sid=$SIDA1, their_sid=$SIDA4, read=true, last_message=18, read_offset=18" } +doc_MeshmsListMessages="Java API list MeshMS messages in one conversation" +setup_MeshmsListMessages() { + setup + meshms_add_messages $SIDA1 $SIDA2 '><>>A>A<>><><><>>>A>A><<<><>>A<<>' + let NROWS=NSENT+NRECV+(NACK?1:0) + executeOk_servald meshms list messages $SIDA1 $SIDA2 + delivered_offset=$(sed -n -e '/^[0-9]\+:[0-9]\+:ACK:delivered$/{n;s/^[0-9]\+:\([0-9]\+\):>:.*/\1/p;q}' "$TFWSTDOUT") + [ -z "$delivered_offset" ] && delivered_offset=0 + read_offset=$(sed -n -e 's/^[0-9]\+:\([0-9]\+\):MARK:read$/\1/p' "$TFWSTDOUT") + [ -z "$read_offset" ] && read_offset=0 + tfw_log delivered_offset="$delivered_offset" read_offset="$read_offset" +} +test_MeshmsListMessages() { + executeJavaOk org.servalproject.test.Meshms meshms-list-messages $SIDA1 $SIDA2 + assertStdoutLineCount '==' $(($NROWS + 2)) + assertStdoutIs --line=1 -e "read_offset=$read_offset\n" + assertStdoutIs --line=2 -e "latest_ack_offset=$delivered_offset\n" + seen_ack=false + let lnum=3 + for ((j = NMESSAGE-1; j >= 0; --j)); do + case ${MESSAGE[$j]} in + 'ACK') $seen_ack && continue + esac + assertStdoutGrep --line=$lnum 'token=[-_A-Za-z0-9=]\+,' + assertStdoutGrep --line=$lnum "my_sid=$SIDA1," + assertStdoutGrep --line=$lnum "their_sid=$SIDA2," + text="$(sed -n -e $lnum's/.*\'|'<') + echo -n "${TEXT[$j]}" | tr ' \n' . >text_fixture + echo -n "$text" >text_list + assert --dump-on-fail=text_fixture --dump-on-fail=text_list cmp text_fixture text_list + assert [ "$ack_offset" = null ] + ;; + esac + case ${MESSAGE[$j]} in + '>') + assertStdoutGrep --line=$lnum 'type=MESSAGE_SENT,' + if [ "$offset" -le "$delivered_offset" ]; then + assert [ "$is_delivered" = true ] + else + assert [ "$is_delivered" = false ] + fi + let ++lnum + ;; + '<') + assertStdoutGrep --line=$lnum 'type=MESSAGE_RECEIVED,' + if [ "$offset" -le "$read_offset" ]; then + assert [ "$is_read" = true ] + else + assert [ "$is_read" = false ] + fi + let ++lnum + ;; + 'ACK') + assertStdoutGrep --line=$lnum 'type=ACK_RECEIVED,' + assert [ "$text" = null ] + assert [ "$ack_offset" = "$delivered_offset" ] + seen_ack=true + let ++lnum + ;; + esac + done +} + +doc_MeshmsListMessagesNoIdentity="Java API list MeshMS messages from unknown identity" +setup_MeshmsListMessagesNoIdentity() { + setup + SIDX=0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF +} +test_MeshmsListMessagesNoIdentity() { + executeJavaOk org.servalproject.test.Meshms meshms-list-messages $SIDX $SIDA2 + assertStdoutGrep 'MeshMSUnknownIdentityException' + tfw_cat --stdout --stderr +} + runTests "$@"