diff --git a/java/org/servalproject/json/JSONTableScanner.java b/java/org/servalproject/json/JSONTableScanner.java new file mode 100644 index 00000000..3442b5e0 --- /dev/null +++ b/java/org/servalproject/json/JSONTableScanner.java @@ -0,0 +1,113 @@ +/** + * 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.reflect.InvocationTargetException; +import java.io.IOException; +import java.util.Vector; +import java.util.Map; +import java.util.HashMap; +import java.util.HashSet; + +public class JSONTableScanner { + + private static class Column { + public String label; + public Class type; + public JSONTokeniser.Narrow opts; + } + + HashMap columnMap; + Column[] columns; + + public JSONTableScanner() + { + columnMap = new HashMap(); + } + + public JSONTableScanner addColumn(String label, Class type) + { + return addColumn(label, type, JSONTokeniser.Narrow.NO_NULL); + } + + public JSONTableScanner addColumn(String label, Class type, JSONTokeniser.Narrow opts) + { + assert !columnMap.containsKey(label); + Column col = new Column(); + col.label = label; + col.type = type; + col.opts = opts; + columnMap.put(label, col); + return this; + } + + public void consumeHeaderArray(JSONTokeniser json) throws IOException, JSONInputException + { + Vector headers = new Vector(); + json.consumeArray(headers, String.class); + if (headers.size() < 1) + throw new JSONInputException("malformed JSON table, empty headers array"); + columns = new Column[headers.size()]; + HashSet headerSet = new HashSet(columnMap.size()); + for (int i = 0; i < headers.size(); ++i) { + String header = headers.get(i); + if (columnMap.containsKey(header)) { + if (headerSet.contains(header)) + throw new JSONInputException("malformed JSON table, duplicate column header: \"" + header + "\""); + headerSet.add(header); + columns[i] = columnMap.get(header); + } + } + for (String header: columnMap.keySet()) + if (!headerSet.contains(header)) + throw new JSONInputException("malformed JSON table, missing column header: \"" + header + "\""); + } + + @SuppressWarnings("unchecked") + public Map consumeRowArray(JSONTokeniser json) throws IOException, JSONInputException + { + Object[] row = new Object[columns.length]; + json.consumeArray(row, JSONTokeniser.Narrow.ALLOW_NULL); + HashMap rowMap = new HashMap(row.length); + for (int i = 0; i < row.length; ++i) { + Column col = columns[i]; + if (col != null) { + Object value; + if (JSONTokeniser.supportsNarrowTo(col.type)) + value = JSONTokeniser.narrow(row[i], col.type, col.opts); + else { + value = JSONTokeniser.narrow(row[i], col.opts); + try { + value = value == null ? null : col.type.getConstructor(value.getClass()).newInstance(value); + } + catch (InvocationTargetException e) { + throw new JSONInputException("invalid column value: " + col.label + "=\"" + value + "\"", e.getTargetException()); + } + catch (Exception e) { + throw new JSONInputException("invalid column value: " + col.label + "=\"" + value + "\"", e); + } + } + rowMap.put(col.label, value); + } + } + return rowMap; + } +} diff --git a/java/org/servalproject/json/JSONTokeniser.java b/java/org/servalproject/json/JSONTokeniser.java index 681a1ccf..9ea14c7a 100644 --- a/java/org/servalproject/json/JSONTokeniser.java +++ b/java/org/servalproject/json/JSONTokeniser.java @@ -131,6 +131,20 @@ public class JSONTokeniser { ALLOW_NULL }; + public static boolean supportsNarrowTo(Class cls) { + return cls == Boolean.class + || cls == Integer.class + || cls == Long.class + || cls == Float.class + || cls == Double.class + || cls == String.class; + } + + public static Object narrow(Object tok, Narrow opts) throws UnexpectedException + { + return narrow(tok, Object.class, opts); + } + public static T narrow(Object tok, Class cls) throws UnexpectedException { return narrow(tok, cls, Narrow.NO_NULL); diff --git a/java/org/servalproject/servaldna/ServalDClient.java b/java/org/servalproject/servaldna/ServalDClient.java index 27f74171..1b3562ad 100644 --- a/java/org/servalproject/servaldna/ServalDClient.java +++ b/java/org/servalproject/servaldna/ServalDClient.java @@ -34,6 +34,8 @@ import org.servalproject.codec.Base64; import org.servalproject.servaldna.SubscriberId; import org.servalproject.servaldna.ServalDCommand; import org.servalproject.servaldna.ServalDInterfaceException; +import org.servalproject.servaldna.rhizome.RhizomeCommon; +import org.servalproject.servaldna.rhizome.RhizomeBundleList; import org.servalproject.servaldna.meshms.MeshMSCommon; import org.servalproject.servaldna.meshms.MeshMSConversationList; import org.servalproject.servaldna.meshms.MeshMSMessageList; @@ -58,6 +60,13 @@ public class ServalDClient implements ServalDHttpConnectionFactory this.restfulPassword = restfulPassword; } + public RhizomeBundleList rhizomeListBundles() throws ServalDInterfaceException, IOException + { + RhizomeBundleList list = new RhizomeBundleList(this); + list.connect(); + return list; + } + public MeshMSConversationList meshmsListConversations(SubscriberId sid) throws ServalDInterfaceException, IOException, MeshMSException { MeshMSConversationList list = new MeshMSConversationList(this, sid); diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeBundle.java b/java/org/servalproject/servaldna/rhizome/RhizomeBundle.java new file mode 100644 index 00000000..50acadb6 --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeBundle.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.rhizome; + +import org.servalproject.servaldna.BundleId; +import org.servalproject.servaldna.SubscriberId; +import org.servalproject.servaldna.FileHash; + +public class RhizomeBundle { + + public final int _rowNumber; + public final int _id; + public final String _token; + public final String service; + public final BundleId id; + public final long version; + public final long date; + public final long _inserttime; + public final SubscriberId _author; + public final int _fromhere; + public final long filesize; + public final FileHash filehash; + public final SubscriberId sender; + public final SubscriberId recipient; + public final String name; + + protected RhizomeBundle(int rowNumber, + int _id, + String _token, + String service, + BundleId id, + long version, + long date, + long _inserttime, + SubscriberId _author, + int _fromhere, + long filesize, + FileHash filehash, + SubscriberId sender, + SubscriberId recipient, + String name) + { + this._rowNumber = rowNumber; + this._id = _id; + this._token = _token; + this.service = service; + this.id = id; + this.version = version; + this.date = date; + this._inserttime = _inserttime; + this._author = _author; + this._fromhere = _fromhere; + this.filesize = filesize; + this.filehash = filehash; + this.sender = sender; + this.recipient = recipient; + this.name = name; + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeBundleList.java b/java/org/servalproject/servaldna/rhizome/RhizomeBundleList.java new file mode 100644 index 00000000..9ef446d5 --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeBundleList.java @@ -0,0 +1,137 @@ +/** + * 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.rhizome; + +import org.servalproject.json.JSONInputException; +import org.servalproject.json.JSONTokeniser; +import org.servalproject.json.JSONTableScanner; +import org.servalproject.servaldna.ServalDHttpConnectionFactory; +import org.servalproject.servaldna.ServalDInterfaceException; +import org.servalproject.servaldna.BundleId; +import org.servalproject.servaldna.SubscriberId; +import org.servalproject.servaldna.FileHash; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class RhizomeBundleList { + + private ServalDHttpConnectionFactory httpConnector; + private HttpURLConnection httpConnection; + private JSONTokeniser json; + private JSONTableScanner table; + int rowCount; + + public RhizomeBundleList(ServalDHttpConnectionFactory connector) + { + this.httpConnector = connector; + this.table = new JSONTableScanner() + .addColumn("_id", Integer.class) + .addColumn(".token", String.class, JSONTokeniser.Narrow.ALLOW_NULL) + .addColumn("service", String.class) + .addColumn("id", BundleId.class) + .addColumn("version", Long.class) + .addColumn("date", Long.class) + .addColumn(".inserttime", Long.class) + .addColumn(".author", SubscriberId.class, JSONTokeniser.Narrow.ALLOW_NULL) + .addColumn(".fromhere", Integer.class) + .addColumn("filesize", Long.class) + .addColumn("filehash", FileHash.class, JSONTokeniser.Narrow.ALLOW_NULL) + .addColumn("sender", SubscriberId.class, JSONTokeniser.Narrow.ALLOW_NULL) + .addColumn("recipient", SubscriberId.class, JSONTokeniser.Narrow.ALLOW_NULL) + .addColumn("name", String.class); + } + + public boolean isConnected() + { + return this.json != null; + } + + public void connect() throws IOException, ServalDInterfaceException + { + try { + rowCount = 0; + httpConnection = httpConnector.newServalDHttpConnection("/restful/rhizome/bundlelist.json"); + httpConnection.connect(); + json = RhizomeCommon.receiveRestfulResponse(httpConnection, HttpURLConnection.HTTP_OK); + json.consume(JSONTokeniser.Token.START_OBJECT); + json.consume("header"); + json.consume(JSONTokeniser.Token.COLON); + table.consumeHeaderArray(json); + 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 RhizomeBundle nextBundle() throws ServalDInterfaceException, IOException + { + 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); + Map row = table.consumeRowArray(json); + return new RhizomeBundle( + rowCount++, + (int)row.get("_id"), + (String)row.get(".token"), + (String)row.get("service"), + (BundleId)row.get("id"), + (long)row.get("version"), + (long)row.get("date"), + (long)row.get(".inserttime"), + (SubscriberId)row.get(".author"), + (int)row.get(".fromhere"), + (long)row.get("filesize"), + (FileHash)row.get("filehash"), + (SubscriberId)row.get("sender"), + (SubscriberId)row.get("recipient"), + (String)row.get("name")); + } + catch (JSONInputException e) { + throw new ServalDInterfaceException(e); + } + } + + public void close() throws IOException + { + httpConnection = null; + if (json != null) { + json.close(); + json = null; + } + } + +} diff --git a/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java new file mode 100644 index 00000000..7f881e02 --- /dev/null +++ b/java/org/servalproject/servaldna/rhizome/RhizomeCommon.java @@ -0,0 +1,81 @@ +/** + * 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.rhizome; + +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; + +public class RhizomeCommon +{ + protected static JSONTokeniser receiveRestfulResponse(HttpURLConnection conn, int expected_response_code) throws IOException, ServalDInterfaceException + { + int[] expected_response_codes = { expected_response_code }; + return receiveRestfulResponse(conn, expected_response_codes); + } + + protected static JSONTokeniser receiveRestfulResponse(HttpURLConnection conn, int[] expected_response_codes) throws IOException, ServalDInterfaceException + { + 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")); + Status status = decodeRestfulStatus(json); + throw new ServalDInterfaceException("unexpected Rhizome failure, \"" + status.message + "\""); + } + for (int code: expected_response_codes) { + if (conn.getResponseCode() == code) { + JSONTokeniser json = new JSONTokeniser(new InputStreamReader(conn.getInputStream(), "US-ASCII")); + return json; + } + } + throw new ServalDInterfaceException("unexpected HTTP response code: " + conn.getResponseCode()); + } + + private static class Status { + public String message; + } + + protected static Status decodeRestfulStatus(JSONTokeniser json) throws IOException, ServalDInterfaceException + { + try { + Status status = new Status(); + 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); + status.message = json.consume("http_status_message"); + json.consume(JSONTokeniser.Token.COLON); + String message = json.consume(String.class); + json.consume(JSONTokeniser.Token.END_OBJECT); + json.consume(JSONTokeniser.Token.EOF); + return status; + } + catch (JSONInputException e) { + throw new ServalDInterfaceException("malformed JSON status response", e); + } + } + +} diff --git a/java/org/servalproject/test/Rhizome.java b/java/org/servalproject/test/Rhizome.java new file mode 100644 index 00000000..c54fabca --- /dev/null +++ b/java/org/servalproject/test/Rhizome.java @@ -0,0 +1,81 @@ +/** + * 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.io.IOException; +import org.servalproject.servaldna.ServalDClient; +import org.servalproject.servaldna.ServalDInterfaceException; +import org.servalproject.servaldna.ServerControl; +import org.servalproject.servaldna.SubscriberId; +import org.servalproject.servaldna.rhizome.RhizomeBundle; +import org.servalproject.servaldna.rhizome.RhizomeBundleList; + +public class Rhizome { + + static void rhizome_list() throws ServalDInterfaceException, IOException, InterruptedException + { + ServalDClient client = new ServerControl().getRestfulClient(); + RhizomeBundleList list = null; + try { + list = client.rhizomeListBundles(); + RhizomeBundle bundle; + while ((bundle = list.nextBundle()) != null) { + System.out.println( + "_id=" + bundle._id + + ", .token=" + bundle._token + + ", service=" + bundle.service + + ", id=" + bundle.id + + ", version=" + bundle.version + + ", date=" + bundle.date + + ", .inserttime=" + bundle._inserttime + + ", .author=" + bundle._author + + ", .fromhere=" + bundle._fromhere + + ", filesize=" + bundle.filesize + + ", filehash=" + bundle.filehash + + ", sender=" + bundle.sender + + ", recipient=" + bundle.recipient + + ", name=" + bundle.name + ); + } + } + finally { + if (list != null) + list.close(); + } + System.exit(0); + } + + public static void main(String... args) + { + if (args.length < 1) + return; + String methodName = args[0]; + try { + if (methodName.equals("rhizome-list")) + rhizome_list(); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + System.err.println("No such command: " + methodName); + System.exit(1); + } +} diff --git a/tests/all b/tests/all index f7e7732d..f3ded7cc 100755 --- a/tests/all +++ b/tests/all @@ -2,7 +2,7 @@ # Aggregation of all tests except high-load stress tests. # -# 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 @@ -39,6 +39,7 @@ includeTests directory_service includeTests vomp if type -p "$JAVAC" >/dev/null; then includeTests jni + includeTests rhizomejava includeTests meshmsjava fi diff --git a/tests/rhizomejava b/tests/rhizomejava new file mode 100755 index 00000000..54fa7abf --- /dev/null +++ b/tests/rhizomejava @@ -0,0 +1,141 @@ +#!/bin/bash + +# Tests for Rhizome 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" +source "${0%/*}/../testdefs_rhizome.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 4 + start_servald_server +} + +teardown() { + stop_all_servald_servers + kill_all_servald_processes + assert_no_servald_processes + report_all_servald_servers +} + +# Utility function: +# +# unset_vars_with_prefix PREFIX +# +# Unsets all shell variables whose names starting with the given PREFIX +unset_vars_with_prefix() { + local __prefix="${1?}" + local __varname + for __varname in $(declare -p | sed -n -e "s/^declare -[^ ]* \($__prefix[A-Za-z0-9_]\+\)=.*/\1/p"); do + unset $__varname + done +} + +# Utility function: +# +# unpack_vars PREFIX TEXT +# +# parses the given TEXT which must have the form: +# +# ident1=..., ident2=...., ... identN=... +# +# into shell variables: +# +# PREFIXident1=... +# PREFIXident2=... +# ... +# PREFIXidentN=... +# +# Sets the UNPACKED_VAR_NAMES[] array variable to a list of the names of the +# variables that were set (names include the PREFIX). +# +# Warning: overwrites existing shell variables. Names of overwritten shell +# variables are derived directly from the output of the command, so cannot be +# controlled. PREFIX should be used to ensure that special variables cannot +# be clobbered by accident. +unpack_vars() { + local __prefix="${1?}" + local __text="${2?}" + local __oo + tfw_shopt __oo -s extglob + UNPACKED_VAR_NAMES=() + while [ -n "$__text" ]; do + case "$__text" in + [A-Za-z_.]+([A-Za-z_.0-9])=*) + local __ident="${__text%%=*}" + __ident="${__ident//./__}" + __text="${__text#*=}" + local __value="${__text%%, [A-Za-z_.]+([A-Za-z_.0-9])=*}" + __text="${__text:${#__value}}" + __text="${__text#, }" + UNPACKED_VAR_NAMES+=("$__ident") + eval ${__prefix}${__ident}=\"\$__value\" + ;; + *) + fail "cannot unpack variable from '$__text'" + ;; + esac + done + tfw_shopt_restore __oo +} + +doc_RhizomeList="Java API Rhizome list 100 bundles" +setup_RhizomeList() { + setup + NBUNDLES=100 + rhizome_add_bundles $SIDA1 0 $((NBUNDLES-1)) + assert [ "$ROWID_MAX" -ge "$NBUNDLES" ] +} +test_RhizomeList() { + executeJavaOk org.servalproject.test.Rhizome rhizome-list + tfw_cat --stdout --stderr + assertStdoutLineCount == $NBUNDLES + let lnum=NBUNDLES + for ((n = 0; n != NBUNDLES; ++n)); do + line="$(sed -n -e ${lnum}p "$TFWSTDOUT")" + unset_vars_with_prefix X__ + unpack_vars X__ "$line" + if [ "${ROWID[$n]}" -eq "$ROWID_MAX" ]; then + # The first row must contain a non-null token string. + assert [ -n "$X____token" ] + assert [ "$lnum" -eq 1 ] + fi + assert [ "$X__name" = "file$n" ] + assert [ "$X__service" = "file" ] + assert [ "$X__id" = "${BID[$n]}" ] + assert [ "$X__version" = "${VERSION[$n]}" ] + assert [ "$X__filesize" = "${SIZE[$n]}" ] + assert [ "$X__filehash" = "${HASH[$n]}" ] + assert [ "$X__date" = "${DATE[$n]}" ] + assert [ "$X___id" = "${ROWID[$n]}" ] + assert [ "$X____fromhere" = "1" ] + assert [ "$X____author" = "$SIDA1" ] + let --lnum + done +} + +runTests "$@"