These are the OpenSRF sources exactly as I got them, checked into the Evergreen app...
authorkenstir <kenstir@gmail.com>
Wed, 18 Nov 2015 17:12:48 +0000 (12:12 -0500)
committerkenstir <kenstir@gmail.com>
Wed, 18 Nov 2015 17:12:48 +0000 (12:12 -0500)
I don't know the exact history of this copy but it is clear that the registry was made serializeable.  For now I want these sources inside the app build so I can debug them.

50 files changed:
Open-ILS/src/Android/opensrf/libs/java_memcached-release_2.0.1.jar [new file with mode: 0644]
Open-ILS/src/Android/opensrf/libs/json-20090211.jar [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/ClientSession.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/Message.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/Method.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/MethodException.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/MultiSession.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/Request.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/Result.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/ServerSession.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/Session.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/SessionException.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/Stack.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/Status.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/Sys.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/net/http/GatewayRequest.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpConnection.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpRequest.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpRequestHandler.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPException.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPMessage.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPReader.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPSession.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/test/MathBench.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/test/TestCache.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/test/TestClient.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/test/TestConfig.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/test/TestJSON.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/test/TestLog.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/test/TestMultiSession.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/test/TestSettings.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/test/TestThread.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMLFlattener.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMLTransformer.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMPP.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/util/Cache.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/util/Config.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/util/ConfigException.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/util/FileLogger.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/util/JSONException.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/util/JSONReader.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/util/JSONWriter.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/util/Logger.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFObject.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFRegistry.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFSerializable.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/util/SettingsClient.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/util/Utils.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/util/XMLFlattener.java [new file with mode: 0644]
Open-ILS/src/Android/opensrf/org/opensrf/util/XMLTransformer.java [new file with mode: 0644]

diff --git a/Open-ILS/src/Android/opensrf/libs/java_memcached-release_2.0.1.jar b/Open-ILS/src/Android/opensrf/libs/java_memcached-release_2.0.1.jar
new file mode 100644 (file)
index 0000000..9f62013
Binary files /dev/null and b/Open-ILS/src/Android/opensrf/libs/java_memcached-release_2.0.1.jar differ
diff --git a/Open-ILS/src/Android/opensrf/libs/json-20090211.jar b/Open-ILS/src/Android/opensrf/libs/json-20090211.jar
new file mode 100644 (file)
index 0000000..ef29094
Binary files /dev/null and b/Open-ILS/src/Android/opensrf/libs/json-20090211.jar differ
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/ClientSession.java b/Open-ILS/src/Android/opensrf/org/opensrf/ClientSession.java
new file mode 100644 (file)
index 0000000..3ed4908
--- /dev/null
@@ -0,0 +1,175 @@
+package org.opensrf;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.Random;
+import java.util.Arrays;
+
+import org.opensrf.util.*;
+import org.opensrf.net.xmpp.*;
+
+
+/**
+ * Models an OpenSRF client session.
+ */
+public class ClientSession extends Session {
+
+    /** The remote service to communicate with */
+    private String service;
+    /** OpenSRF domain */
+    private String domain;
+    /** Router name */
+    private String router;
+
+    /** 
+     * original remote node.  The current remote node will change based 
+     * on server responses.  This is used to reset the remote node to 
+     * its original state.
+     */
+    private String origRemoteNode;
+    /** The next request id */
+    private int nextId;
+    /** The requests this session has sent */
+    private Map<Integer, Request> requests;
+
+    /**
+     * Creates a new client session.  Initializes the 
+     * @param service The remote service.
+     */
+    public ClientSession(String service) throws ConfigException {
+        this(service, null);
+    }
+
+    /**
+     * Creates a new client session.  Initializes the 
+     * @param service The remote service.
+     * @param locale The locale for this session.
+     */
+    public ClientSession(String service, String locale) throws ConfigException {
+        this.service = service;
+        if(locale != null) 
+            setLocale(locale);
+
+        /** generate the remote node string */
+        domain = (String) Config.global().getFirst("/domain");
+        router = Config.global().getString("/router_name");
+        setRemoteNode(router + "@" + domain + "/" + service);
+        origRemoteNode = getRemoteNode();
+
+
+        /** create a random thread */
+        long time = new Date().getTime();
+        Random rand = new Random(time);
+        setThread(rand.nextInt()+""+rand.nextInt()+""+time+Thread.currentThread().getId());
+
+        nextId = 0;
+        requests = new HashMap<Integer, Request>();
+        cacheSession();
+    }
+
+    /**
+     * Creates a new request to send to our remote service.
+     * @param method The method API name
+     * @param params The list of method parameters
+     * @return The request object.
+     */
+    public Request request(String method, List<Object> params) throws SessionException {
+        return request(new Request(this, nextId++, method, params));
+    }
+
+    /**
+     * Creates a new request to send to our remote service.
+     * @param method The method API name
+     * @param params The list of method parameters
+     * @return The request object.
+     */
+    public Request request(String method, Object[] params) throws SessionException {
+        return request(new Request(this, nextId++, method, Arrays.asList(params)));
+    }
+
+
+    /**
+     * Creates a new request to send to our remote service.
+     * @param method The method API name
+     * @return The request object.
+     */
+    public Request request(String method) throws SessionException {
+        return request(new Request(this, nextId++, method));
+    }
+
+
+    private Request request(Request req) throws SessionException {
+        if(getConnectState() != ConnectState.CONNECTED)
+            resetRemoteId();
+        requests.put(new Integer(req.getId()), req);
+        req.send();
+        return req;
+    }
+
+
+    /**
+     * Resets the remoteNode to its original state.
+     */
+    public void resetRemoteId() {
+        setRemoteNode(origRemoteNode);
+    }
+
+
+    /**
+     * Pushes a response onto the result queue of the appropriate request.
+     * @param msg The received RESULT Message
+     */
+    public void pushResponse(Message msg) {
+
+        Request req = findRequest(msg.getId());
+        if(req == null) {
+            /** LOG that we've received a result to a non-existant request */
+            System.err.println(msg.getId() +" has no corresponding request");
+            return;
+        }
+        OSRFObject payload = (OSRFObject) msg.get("payload");
+
+        /** build a result and push it onto the request's result queue */
+        req.pushResponse(
+            new Result( 
+                payload.getString("status"), 
+                payload.getInt("statusCode"),
+                payload.get("content")
+            )
+        );
+    }
+
+    public Request findRequest(int reqId) {
+        return requests.get(new Integer(reqId));
+    }
+
+    /**
+     * Removes a request for this session's request set
+     */
+    public void cleanupRequest(int reqId) {
+        requests.remove(new Integer(reqId));
+    }
+
+     public void setRequestComplete(int reqId) {
+        Request req = findRequest(reqId);
+        if(req == null) return;
+        req.setComplete();
+    }
+
+    public static Object atomicRequest(String service, String method, Object[] params) throws MethodException {
+        try {
+            ClientSession session = new ClientSession(service);
+            Request osrfRequest = session.request(method, params);
+            Result result = osrfRequest.recv(600000);
+            if(result.getStatusCode() != 200) 
+                throw new MethodException( 
+                    "Request "+service+":"+method+":"+" failed with status code " + result.getStatusCode());
+            return result.getContent();
+        } catch(Exception e) {
+            throw new MethodException(e);
+        }
+    }
+}
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/Message.java b/Open-ILS/src/Android/opensrf/org/opensrf/Message.java
new file mode 100644 (file)
index 0000000..6bfe1ea
--- /dev/null
@@ -0,0 +1,110 @@
+package org.opensrf;
+import org.opensrf.util.*;
+
+
+public class Message implements OSRFSerializable {
+
+    /** Message types */
+    public static final String REQUEST = "REQUEST";
+    public static final String STATUS = "STATUS";
+    public static final String RESULT = "RESULT";
+    public static final String CONNECT = "CONNECT";
+    public static final String DISCONNECT = "DISCONNECT";
+
+    /** Message ID.  This number is used to relate requests to responses */
+    private int id;
+    /** type of message. */
+    private String type;
+    /** message payload */
+    private Object payload;
+    /** message locale */
+    private String locale;
+
+    /** Create a registry for the osrfMessage object */
+    private static OSRFRegistry registry = 
+        OSRFRegistry.registerObject(
+            "osrfMessage", 
+            OSRFRegistry.WireProtocol.HASH, 
+            new String[] {"threadTrace", "type", "payload", "locale"});
+
+    /**
+     * @param id This message's ID
+     * @param type The type of message
+     */
+    public Message(int id, String type) {
+        setId(id);
+        setString(type);
+    }
+
+    /**
+     * @param id This message's ID
+     * @param type The type of message
+     * @param payload The message payload
+     */
+    public Message(int id, String type, Object payload) {
+        this(id, type);
+        setPayload(payload);
+    }
+
+    /**
+     * @param id This message's ID
+     * @param type The type of message
+     * @param payload The message payload
+     * @param locale The message locale
+     */
+    public Message(int id, String type, Object payload, String locale) {
+        this(id, type, payload);
+        setPayload(payload);
+        setLocale(locale);
+    }
+
+
+    public int getId() {
+        return id;
+    }   
+    public String getType() {
+        return type;
+    }
+    public Object getPayload() {
+        return payload;
+    }
+    public String getLocale() {
+        return locale;
+    }
+    public void setId(int id) {
+        this.id = id;
+    }
+    public void setString(String type) {
+        this.type = type;
+    }
+    public void setPayload(Object p) {
+        payload = p;
+    }
+    public void setLocale(String l) {
+        locale = l;
+    }
+
+    /**
+     * Implements the generic get() API required by OSRFSerializable
+     */
+    public Object get(String field) {
+        if("threadTrace".equals(field))
+            return getId();
+        if("type".equals(field))
+            return getType().toString();
+        if("payload".equals(field))
+            return getPayload();
+        if("locale".equals(field))
+            return getLocale();
+        return null;
+    }
+
+    /**
+     * @return The osrfMessage registry.
+     */
+    public OSRFRegistry getRegistry() {
+        return registry;
+    }
+}
+
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/Method.java b/Open-ILS/src/Android/opensrf/org/opensrf/Method.java
new file mode 100644 (file)
index 0000000..b708d4f
--- /dev/null
@@ -0,0 +1,78 @@
+package org.opensrf;
+import java.util.List;
+import java.util.ArrayList;
+import org.opensrf.util.*;
+
+
+public class Method extends OSRFObject {
+
+    /** The method API name */
+    private String name;
+    /** The ordered list of method params */
+    private List<Object> params;
+
+    /** Create a registry for the osrfMethod object */
+    private static OSRFRegistry registry = 
+        OSRFRegistry.registerObject(
+            "osrfMethod", 
+            OSRFRegistry.WireProtocol.HASH, 
+            new String[] {"method", "params"});
+
+    /**
+     * @param name The method API name 
+     */
+    public Method(String name) {
+        this.name = name;
+        this.params = new ArrayList<Object>(8);
+    }
+
+    /**
+     * @param name The method API name
+     * @param params The ordered list of params
+     */
+    public Method(String name, List<Object> params) {
+        this.name = name;
+        this.params = params;
+    }
+
+    /**
+     * @return The method API name
+     */
+    public String getName() {
+        return name;
+    }
+    /**
+     * @return The ordered list of params
+     */
+    public List<Object> getParams() {
+       return params; 
+    }
+
+    /**
+     * Pushes a new param object onto the set of params 
+     * @param p The new param to add to the method.
+     */
+    public void addParam(Object p) {
+        this.params.add(p);
+    }
+
+    /**
+     * Implements the generic get() API required by OSRFSerializable
+     */
+    public Object get(String field) {
+        if("method".equals(field))
+            return getName();
+        if("params".equals(field))
+            return getParams();
+        return null;
+    }
+
+    /**
+     * @return The osrfMethod registry.
+     */
+    public OSRFRegistry getRegistry() {
+        return registry;
+    }
+
+}
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/MethodException.java b/Open-ILS/src/Android/opensrf/org/opensrf/MethodException.java
new file mode 100644 (file)
index 0000000..f87e638
--- /dev/null
@@ -0,0 +1,14 @@
+package org.opensrf;
+
+/**
+ * Thrown when the server responds with a method exception.
+ */
+public class MethodException extends Exception {
+    public MethodException(String info) {
+        super(info);
+    }
+    public MethodException(Throwable cause) {
+        super(cause);
+    }
+}
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/MultiSession.java b/Open-ILS/src/Android/opensrf/org/opensrf/MultiSession.java
new file mode 100644 (file)
index 0000000..a312aff
--- /dev/null
@@ -0,0 +1,123 @@
+package org.opensrf;
+import java.util.List;
+import java.util.ArrayList;
+import org.opensrf.util.ConfigException;
+
+public class MultiSession {
+
+    class RequestContainer {
+        Request request;
+        int id;
+        RequestContainer(Request r) {
+            request = r; 
+        }
+    }
+
+    private boolean complete;
+    private List<RequestContainer> requests;
+    private int lastId;
+
+    public MultiSession() {
+        requests = new ArrayList<RequestContainer>();
+    }
+
+    public boolean isComplete() {
+        return complete;
+    }
+
+    public int lastId() {
+        return lastId;
+    }
+
+    /**
+     * Adds a new request to the set of requests.
+     * @param service The OpenSRF service
+     * @param method The OpenSRF method
+     * @param params The array of method params
+     * @return The request ID, which is used to map results from recv() to the original request.
+     */
+    public int request(String service, String method, Object[] params) throws SessionException, ConfigException {
+        ClientSession ses = new ClientSession(service);
+        return request(ses.request(method, params));
+    }
+
+
+    public int request(String service, String method) throws SessionException, ConfigException {
+        ClientSession ses = new ClientSession(service);
+        return request(ses.request(method));
+    }
+
+    private int request(Request req) {
+        RequestContainer c = new RequestContainer(req);
+        c.id = requests.size();
+        requests.add(c);
+        return c.id;
+    }
+
+
+    /**
+     * Calls recv on all pending requests until there is data to return.  The ID which
+     * maps the received object to the request can be retrieved by calling lastId().
+     * @param millis Number of milliseconds to wait for some data to arrive.
+     * @return The object result or null if all requests are complete
+     * @throws MethodException Thrown if no response is received within 
+     * the given timeout or the method fails.
+     */
+    public Object recv(int millis) throws MethodException {
+        if(complete) return null;
+
+        Request req = null;
+        Result res = null;
+        RequestContainer cont = null;
+
+        long duration = 0;
+        long blockTime = 100;
+
+        /* if there is only 1 outstanding request, don't poll */
+        if(requests.size() == 1)
+            blockTime = millis;
+
+        while(true) {
+            for(int i = 0; i < requests.size(); i++) {
+
+                cont = requests.get(i);
+                req = cont.request;
+
+                try {
+                    if(i == 0) {
+                        res = req.recv(blockTime);
+                    } else {
+                        res = req.recv(0);
+                    }
+                } catch(SessionException e) {
+                    throw new MethodException(e);
+                }
+
+                if(res != null) break;
+            }
+
+            if(res != null) break;
+            duration += blockTime;
+
+            if(duration >= millis) {
+                System.out.println("duration = " + duration + " millis = " + millis);
+                throw new MethodException("No request received within " + millis + " milliseconds");
+            }
+        }
+
+        if(res.getStatusCode() != 200) {
+            throw new MethodException("Request " + cont.id + " failed  with status code " + 
+                res.getStatusCode() + " and status message " + res.getStatus());
+        }
+
+        if(req.isComplete())
+            requests.remove(requests.indexOf(cont));
+
+        if(requests.size() == 0)
+            complete = true;
+
+        lastId = cont.id;
+        return res.getContent();
+    }
+}
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/Request.java b/Open-ILS/src/Android/opensrf/org/opensrf/Request.java
new file mode 100644 (file)
index 0000000..2d72e2d
--- /dev/null
@@ -0,0 +1,138 @@
+package org.opensrf;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.List;
+import java.util.Date;
+import org.opensrf.net.xmpp.XMPPException;
+import org.opensrf.util.Logger;
+
+public class Request {
+    
+    /** This request's controlling session */
+    private ClientSession session;
+    /** The method */
+    private Method method;
+    /** The ID of this request */
+    private int id;
+    /** Queue of Results */
+    private Queue<Result> resultQueue;
+    /** If true, the receive timeout for this request should be reset */
+    private boolean resetTimeout;
+
+    /** If true, the server has indicated that this request is complete. */
+    private boolean complete;
+
+    /**
+     * @param ses The controlling session for this request.
+     * @param id This request's ID.
+     * @param method The requested method.
+     */
+    public Request(ClientSession ses, int id, Method method) {
+        this.session = ses;
+        this.id = id;
+        this.method = method;
+        resultQueue = new ConcurrentLinkedQueue<Result>();
+        complete = false;
+        resetTimeout = false;
+    }
+
+    /**
+     * @param ses The controlling session for this request.
+     * @param id This request's ID.
+     * @param methodName The requested method's API name.
+     */
+    public Request(ClientSession ses, int id, String methodName) {
+        this(ses, id, new Method(methodName));
+    }
+
+    /**
+     * @param ses The controlling session for this request.
+     * @param id This request's ID.
+     * @param methodName The requested method's API name.
+     * @param params The list of request params
+     */
+    public Request(ClientSession ses, int id, String methodName, List<Object> params) {
+        this(ses, id, new Method(methodName, params));
+    }
+
+    /**
+     * Sends the request to the server.
+     */
+    public void send() throws SessionException {
+        session.send(new Message(id, Message.REQUEST, method, session.getLocale()));
+    }
+
+    /**
+     * Receives the next result for this request.  This method
+     * will wait up to the specified number of milliseconds for 
+     * a response. 
+     * @param millis Number of milliseconds to wait for a result.  If
+     * negative, this method will wait indefinitely.
+     * @return The result or null if none arrives in time
+     */
+    public Result recv(long millis) throws SessionException, MethodException {
+
+        Result result = null;
+
+        if((result = resultQueue.poll()) != null)
+            return result;
+
+        if(millis < 0 && !complete) {
+            /** wait potentially forever for a result to arrive */
+            while(!complete) {
+                session.waitForMessage(millis);
+                if((result = resultQueue.poll()) != null)
+                    return result;
+            }
+
+        } else {
+
+            while(millis >= 0 && !complete) {
+
+                /** wait up to millis milliseconds for a result.  waitForMessage() 
+                 * will return if a response to any request arrives, so we keep track
+                 * of how long we've been waiting in total for a response to 
+                 * this request */
+
+                long start = new Date().getTime();
+                session.waitForMessage(millis);
+                millis -= new Date().getTime() - start;
+                if((result = resultQueue.poll()) != null)
+                    return result;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Pushes a result onto the result queue 
+     * @param result The result to push
+     */
+    public void pushResponse(Result result) {
+        resultQueue.offer(result);
+    }
+
+    /**
+     * @return This request's ID
+     */
+    public int getId() {
+        return id;
+    }
+
+    /**
+     * Removes this request from the controlling session's request set
+     */
+    public void cleanup() {
+        session.cleanupRequest(id);
+    }
+
+    /** Sets this request as complete */
+    public void setComplete() {
+        complete = true;
+    }
+
+    public boolean isComplete() {
+        return complete;
+    }
+}
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/Result.java b/Open-ILS/src/Android/opensrf/org/opensrf/Result.java
new file mode 100644 (file)
index 0000000..80b71dd
--- /dev/null
@@ -0,0 +1,106 @@
+package org.opensrf;
+import org.opensrf.util.*;
+
+
+/**
+ * Models a single result from a method request.
+ */
+public class Result implements OSRFSerializable {
+
+    /** Method result content */
+    private Object content;
+    /** Name of the status */
+    private String status;
+    /** Status code number */
+    private int statusCode;
+
+
+    /** Register this object */
+    private static OSRFRegistry registry = 
+        OSRFRegistry.registerObject(
+            "osrfResult", 
+            OSRFRegistry.WireProtocol.HASH, 
+            new String[] {"status", "statusCode", "content"});
+
+
+    /**
+     * @param status The status message for this result
+     * @param statusCode The status code
+     * @param content The content of the result
+     */
+    public Result(String status, int statusCode, Object content) {
+        this.status = status;
+        this.statusCode = statusCode;
+        this.content = content;
+    }
+    
+    /**
+     * Get status.
+     * @return status as String.
+     */
+    public String getStatus() {
+        return status;
+    }
+    
+    /**
+     * Set status.
+     * @param status the value to set.
+     */
+    public void setStatus(String status) {
+        this.status = status;
+    }
+    
+    /**
+     * Get statusCode.
+     * @return statusCode as int.
+     */
+    public int getStatusCode() {
+        return statusCode;
+    }
+    
+    /**
+     * Set statusCode.
+     * @param statusCode the value to set.
+     */
+    public void setStatusCode(int statusCode) {
+        this.statusCode = statusCode;
+    }
+    
+    /**
+     * Get content.
+     * @return content as Object.
+     */
+    public Object getContent() {
+        return content;
+    }
+    
+    /**
+     * Set content.
+     * @param content the value to set.
+     */
+    public void setContent(Object content) {
+        this.content = content;
+    }
+
+    /**
+     * Implements the generic get() API required by OSRFSerializable
+     */
+    public Object get(String field) {
+        if("status".equals(field))
+            return getStatus();
+        if("statusCode".equals(field))
+            return getStatusCode();
+        if("content".equals(field))
+            return getContent();
+        return null;
+    }
+
+    /**
+     * @return The osrfMethod registry.
+     */
+    public OSRFRegistry getRegistry() {
+        return registry;
+    }
+
+}
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/ServerSession.java b/Open-ILS/src/Android/opensrf/org/opensrf/ServerSession.java
new file mode 100644 (file)
index 0000000..62e5133
--- /dev/null
@@ -0,0 +1,8 @@
+package org.opensrf;
+
+/**
+ * Models an OpenSRF server session.
+ */
+public class ServerSession extends Session {
+}
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/Session.java b/Open-ILS/src/Android/opensrf/org/opensrf/Session.java
new file mode 100644 (file)
index 0000000..15fd352
--- /dev/null
@@ -0,0 +1,180 @@
+package org.opensrf;
+import org.opensrf.util.JSONWriter;
+import org.opensrf.net.xmpp.*;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Arrays;
+
+public abstract class Session {
+
+    /** Represents the different connection states for a session */
+    public enum ConnectState {
+        DISCONNECTED,
+        CONNECTING,
+        CONNECTED
+    };
+
+    /** local cache of existing sessions */
+    private static Map<String, Session> 
+        sessionCache = new HashMap<String, Session>();
+
+    /** the current connection state */
+    private ConnectState connectState;
+
+    /** The address of the remote party we are communicating with */
+    private String remoteNode;
+
+    /** Session locale */
+    protected String locale;
+    /** Default session locale */
+    protected static String defaultLocale = "en-US";
+
+    /** 
+     * The thread is used to link messages to a given session. 
+     * In other words, each session has a unique thread, and all messages 
+     * in that session will carry this thread around as an indicator.
+     */
+    private String thread;
+
+    public Session() {
+        connectState = ConnectState.DISCONNECTED;
+    }
+    
+    /**
+     * Sends a Message to our remoteNode.
+     */
+    public void send(Message omsg) throws SessionException {
+
+        /** construct the XMPP message */
+        XMPPMessage xmsg = new XMPPMessage();
+        xmsg.setTo(remoteNode);
+        xmsg.setThread(thread);
+        xmsg.setBody(new JSONWriter(Arrays.asList(new Message[] {omsg})).write());
+
+        try {
+            XMPPSession.getThreadSession().send(xmsg);
+        } catch(XMPPException e) {
+            connectState = ConnectState.DISCONNECTED;
+            throw new SessionException("Error sending message to " + remoteNode, e);
+        }
+    }
+
+    /**
+     * Waits for a message to arrive over the network and passes
+     * all received messages to the stack for processing
+     * @param millis The number of milliseconds to wait for a message to arrive
+     */
+    public static void waitForMessage(long millis) throws SessionException, MethodException {
+        try {
+            Stack.processXMPPMessage(
+                XMPPSession.getThreadSession().recv(millis));
+        } catch(XMPPException e) {
+            throw new SessionException("Error waiting for message", e);
+        }
+    }
+
+    /**
+     * Removes this session from the session cache.
+     */
+    public void cleanup() {
+        sessionCache.remove(thread);
+    }
+
+    /**
+     * Searches for the cached session with the given thread.
+     * @param thread The session thread.
+     * @return The found session or null.
+     */
+    public static Session findCachedSession(String thread) {
+        return sessionCache.get(thread);
+    }
+
+    /**
+     * Puts this session into session cache.
+     */
+    protected void cacheSession() {
+        sessionCache.put(thread, this);
+    }
+
+    /**
+     * Sets the remote address
+     * @param nodeName The name of the remote node.
+     */
+    public void setRemoteNode(String nodeName) {
+        remoteNode = nodeName;
+    }
+    /**
+     * @return The remote node
+     */
+    public String getRemoteNode() {
+        return remoteNode;
+    }
+
+
+    /**
+     * Get thread.
+     * @return thread as String.
+     */
+    public String getThread() {
+        return thread;
+    }
+    
+    /**
+     * Set thread.
+     * @param thread the value to set.
+     */
+    public void setThread(String thread) {
+        this.thread = thread;
+    }
+
+    /**
+     * Get locale.
+     * @return locale as String.
+     */
+    public String getLocale() {
+        if(locale == null)
+            return defaultLocale;
+        return locale;
+    }
+    
+    /**
+     * Set locale.
+     * @param locale the value to set.
+     */
+    public void setLocale(String locale) {
+        this.locale = locale;
+    }
+
+    /**
+     * Get defaultLocale.
+     * @return defaultLocale as String.
+     */
+    public String getDefaultLocale() {
+        return defaultLocale;
+    }
+    
+    /**
+     * Set defaultLocale.
+     * @param defaultLocale the value to set.
+     */
+    public void setDefaultLocale(String defaultLocale) {
+        this.defaultLocale = defaultLocale;
+    }
+
+    
+    /**
+     * Get connectState.
+     * @return connectState as ConnectState.
+     */
+    public ConnectState getConnectState() {
+        return connectState;
+    }
+    
+    /**
+     * Set connectState.
+     * @param connectState the value to set.
+     */
+    public void setConnectState(ConnectState connectState) {
+        this.connectState = connectState;
+    }
+}
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/SessionException.java b/Open-ILS/src/Android/opensrf/org/opensrf/SessionException.java
new file mode 100644 (file)
index 0000000..bd90a76
--- /dev/null
@@ -0,0 +1,13 @@
+package org.opensrf;
+/**
+ * Used by sessions to indicate communication errors
+ */
+public class SessionException extends Exception {
+    public SessionException(String info) {
+        super(info);
+    }
+    public SessionException(String info, Throwable cause) {
+        super(info, cause);
+    }
+}
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/Stack.java b/Open-ILS/src/Android/opensrf/org/opensrf/Stack.java
new file mode 100644 (file)
index 0000000..3e7e606
--- /dev/null
@@ -0,0 +1,105 @@
+package org.opensrf;
+import org.opensrf.net.xmpp.XMPPMessage;
+import org.opensrf.util.*;
+import java.util.Date;
+import java.util.List;
+import java.util.Iterator;
+
+
+public class Stack {
+
+    public static void processXMPPMessage(XMPPMessage msg) throws MethodException {
+
+        if(msg == null) return;
+
+        //System.out.println(msg.getBody());
+
+        /** fetch this session from the cache */
+        Session ses = Session.findCachedSession(msg.getThread());
+
+        if(ses == null) {
+            /** inbound client request, create a new server session */
+            return;
+        }
+
+        /** parse the JSON message body, which should result in a list of OpenSRF messages */
+        List msgList; 
+
+        try {
+            msgList = new JSONReader(msg.getBody()).readArray();
+        } catch(JSONException e) {
+            /** XXX LOG error */
+            e.printStackTrace();
+            return;
+        }
+
+        Iterator itr = msgList.iterator();
+
+        OSRFObject obj = null;
+        long start = new Date().getTime();
+
+        /** cycle through the messages and push them up the stack */
+        while(itr.hasNext()) {
+
+            /** Construct a Message object from the parsed generic OSRFObject */
+            obj = (OSRFObject) itr.next();
+
+            processOSRFMessage(
+                ses, 
+                new Message(
+                    obj.getInt("threadTrace"),
+                    obj.getString("type"),
+                    obj.get("payload")
+                )
+            );
+        }
+
+        /** LOG the duration */
+    }
+
+    private static void processOSRFMessage(Session ses, Message msg) throws MethodException {
+
+        Logger.debug("received id=" + msg.getId() + 
+            " type=" + msg.getType() + " payload=" + msg.getPayload());
+
+        if( ses instanceof ClientSession ) 
+            processResponse((ClientSession) ses, msg);
+        else
+            processRequest((ServerSession) ses, msg);
+    }
+
+    /** 
+     * Process a server response
+     */
+    private static void processResponse(ClientSession session, Message msg) throws MethodException {
+        String type = msg.getType();
+
+        if(msg.RESULT.equals(type)) {
+            session.pushResponse(msg);
+            return;
+        }
+
+        if(msg.STATUS.equals(type)) {
+
+            OSRFObject obj = (OSRFObject) msg.getPayload();
+            Status stat = new Status(obj.getString("status"), obj.getInt("statusCode"));
+            int statusCode = stat.getStatusCode();
+            String status = stat.getStatus();
+
+            switch(statusCode) {
+                case Status.COMPLETE:
+                    session.setRequestComplete(msg.getId());
+                    break;
+                case Status.NOTFOUND: 
+                    session.setRequestComplete(msg.getId());
+                    throw new MethodException(status);
+            }
+        }
+    }
+
+    /**
+     * Process a client request
+     */
+    private static void processRequest(ServerSession session, Message msg) {
+    }
+}
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/Status.java b/Open-ILS/src/Android/opensrf/org/opensrf/Status.java
new file mode 100644 (file)
index 0000000..8026c7b
--- /dev/null
@@ -0,0 +1,63 @@
+package org.opensrf;
+import org.opensrf.util.*;
+
+public class Status {
+
+    public static final int CONTINUE            = 100;
+    public static final int OK                  = 200;
+    public static final int ACCEPTED            = 202;
+    public static final int COMPLETE            = 205;
+    public static final int REDIRECTED          = 307;
+    public static final int EST                 = 400;
+    public static final int STATUS_UNAUTHORIZED = 401;
+    public static final int FORBIDDEN           = 403;
+    public static final int NOTFOUND            = 404;
+    public static final int NOTALLOWED          = 405;
+    public static final int TIMEOUT             = 408;
+    public static final int EXPFAILED           = 417;
+    public static final int INTERNALSERVERERROR = 500;
+    public static final int NOTIMPLEMENTED      = 501;
+    public static final int VERSIONNOTSUPPORTED = 505;
+
+    private OSRFRegistry registry = OSRFRegistry.registerObject(
+        "osrfConnectStatus",
+        OSRFRegistry.WireProtocol.HASH,
+        new String[] {"status", "statusCode"});
+
+    /** The name of the status */
+    String status;
+    /** The status code */
+    int statusCode;
+
+    public Status(String status, int statusCode) {
+        this.status = status;
+        this.statusCode = statusCode;
+    }
+
+    public int getStatusCode() {
+        return statusCode;
+    }
+    public String getStatus() {
+        return status;
+    }
+
+    /**
+     * Implements the generic get() API required by OSRFSerializable
+     */
+    public Object get(String field) {
+        if("status".equals(field))
+            return getStatus();
+        if("statusCode".equals(field))
+            return new Integer(getStatusCode());
+        return null;
+    }
+
+    /**
+     * @return The osrfMessage registry.
+     */
+    public OSRFRegistry getRegistry() {
+        return registry;
+    }
+}
+
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/Sys.java b/Open-ILS/src/Android/opensrf/org/opensrf/Sys.java
new file mode 100644 (file)
index 0000000..d65f8a4
--- /dev/null
@@ -0,0 +1,86 @@
+package org.opensrf;
+
+import org.opensrf.util.*;
+import org.opensrf.net.xmpp.*;
+import java.util.Random;
+import java.util.Date;
+import java.net.InetAddress;
+
+
+public class Sys {
+
+    private static void initLogger(Config config) {
+        if(Logger.instance() == null) {
+            try {
+                String logFile = config.getString("/logfile");
+                int logLevel = config.getInt("/loglevel");
+                Logger.init( (short) config.getInt("/loglevel"), new FileLogger(logFile));
+                /** add syslog support... */
+            } catch(Exception e) {
+                /* by default, log to stderr at WARN level */
+                Logger.init(Logger.WARN, new Logger()); 
+            }
+        }
+    }
+
+    /**
+     * Connects to the OpenSRF network so that client sessions may communicate.
+     * @param configFile The OpenSRF config file 
+     * @param configContext Where in the XML document the config chunk lives.  This
+     * allows an OpenSRF client config chunk to live in XML files where other config
+     * information lives.
+     */
+    public static void bootstrapClient(String configFile, String configContext) 
+            throws ConfigException, SessionException  {
+
+
+        /** see if the current thread already has a connection */
+        XMPPSession existing = XMPPSession.getThreadSession();
+        if(existing != null && existing.connected())
+            return;
+
+        /** create the config parser */
+        Config config = new Config(configContext);
+        config.parse(configFile);
+        Config.setConfig(config); /* set this as the global config */
+
+        initLogger(config);
+
+        /** Collect the network connection info from the config */
+        String username = config.getString("/username");
+        String passwd = config.getString("/passwd");
+        String host = (String) config.getFirst("/domain");
+        int port = config.getInt("/port");
+
+
+        /** Create a random login resource string */
+        String res = "java_";
+        try {
+            res += InetAddress.getLocalHost().getHostAddress();
+        } catch(java.net.UnknownHostException e) {}
+        res += "_"+Math.abs(new Random(new Date().getTime()).nextInt()) 
+            + "_t"+ Thread.currentThread().getId();
+
+
+
+        try {
+
+            /** Connect to the Jabber network */
+            Logger.info("attempting to create XMPP session "+username+"@"+host+"/"+res);
+            XMPPSession xses = new XMPPSession(host, port);
+            xses.connect(username, passwd, res);
+            XMPPSession.setThreadSession(xses);
+
+        } catch(XMPPException e) {
+            throw new SessionException("Unable to bootstrap client", e);
+        }
+    }
+
+    /**
+     * Shuts down the connection to the opensrf network
+     */
+    public static void shutdown() {
+        XMPPSession.getThreadSession().disconnect();
+    }
+}
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/net/http/GatewayRequest.java b/Open-ILS/src/Android/opensrf/org/opensrf/net/http/GatewayRequest.java
new file mode 100644 (file)
index 0000000..ab35946
--- /dev/null
@@ -0,0 +1,133 @@
+package org.opensrf.net.http;
+
+import org.opensrf.*;
+import org.opensrf.util.*;
+
+import java.io.IOException;
+import java.io.BufferedInputStream;
+import java.io.OutputStreamWriter;
+import java.io.InputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.net.URI;
+import java.net.HttpURLConnection;
+import java.lang.StringBuffer;
+import java.util.List;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+public class GatewayRequest extends HttpRequest {
+
+    private boolean readComplete;
+
+    public GatewayRequest(HttpConnection conn, String service, Method method) {
+        super(conn, service, method);
+        readComplete = false;
+    }
+
+    public GatewayRequest send() {
+        try {
+
+            String postData = compilePostData(service, method);
+
+            urlConn = (HttpURLConnection) httpConn.url.openConnection();
+            urlConn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); 
+            urlConn.setDoInput(true);
+            urlConn.setDoOutput(true);
+
+            
+            
+            OutputStreamWriter wr = new OutputStreamWriter(urlConn.getOutputStream());
+            wr.write(postData);
+            wr.flush();
+            wr.close();
+
+        } catch (java.io.IOException ex) {
+            failed = true;
+            failure = ex;
+        }
+
+        return this;
+    }
+
+    public Object recv() {
+
+        if (readComplete) 
+            return nextResponse();
+
+        try {
+
+            InputStream netStream = new BufferedInputStream(urlConn.getInputStream());
+            StringBuffer readBuf = new StringBuffer();
+
+            int bytesRead = 0;
+            byte[] buffer = new byte[1024];
+
+            while ((bytesRead = netStream.read(buffer)) != -1) {
+                readBuf.append(new String(buffer, 0, bytesRead));
+            }
+            
+            netStream.close();
+            urlConn = null;
+
+            Map<String,?> result = null;
+
+            System.out.println("Received " +  readBuf.toString());
+            try {
+                result = (Map<String, ?>) new JSONReader(readBuf.toString()).readObject();
+            } catch (org.opensrf.util.JSONException ex) {
+                ex.printStackTrace();
+                return null;
+            }
+          System.out.println("Converted object " + result); 
+            String status = result.get("status").toString(); 
+            if (!"200".equals(status)) {
+                failed = true;
+                // failure = <some new exception>
+            }
+
+             // gateway always returns a wrapper array with the full results set
+             responseList = (List) result.get("payload"); 
+
+            // System.out.println("Response list : " + responseList);
+        } catch (java.io.IOException ex) { 
+            failed = true;
+            failure = ex;
+        }
+
+        readComplete = true;
+        return nextResponse();
+    }
+
+    private String compilePostData(String service, Method method) {
+        URI uri = null;
+        StringBuffer postData = new StringBuffer();
+
+        postData.append("service=");
+        postData.append(service);
+        postData.append("&method=");
+        postData.append(method.getName());
+
+        List params = method.getParams();
+        Iterator itr = params.iterator();
+
+        while (itr.hasNext()) {
+            postData.append("&param=");
+            postData.append(new JSONWriter(itr.next()).write());
+        }
+
+        try {
+            // not using URLEncoder because it replaces ' ' with '+'.
+            uri = new URI("http", "", null, postData.toString(), null);
+        } catch (java.net.URISyntaxException ex) {
+            ex.printStackTrace(); 
+        }
+
+        return uri.getRawQuery();
+    }
+}
+
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpConnection.java b/Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpConnection.java
new file mode 100644 (file)
index 0000000..32fdebc
--- /dev/null
@@ -0,0 +1,97 @@
+package org.opensrf.net.http;
+
+import java.net.URL;
+import java.net.MalformedURLException;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import org.opensrf.*;
+import org.opensrf.util.*;
+
+
+/**
+ * Manages connection parameters and thread limiting for opensrf json gateway connections.
+ */
+
+public class HttpConnection {
+
+    /** Compiled URL object */
+    protected URL url;
+    /** Number of threads currently communicating with the server */
+    protected int activeThreads;
+    /** Queue of pending async requests */
+    protected Queue<HttpRequest> pendingThreadQueue;
+    /** maximum number of actively communicating threads allowed */
+    protected int maxThreads = 10;
+
+    public HttpConnection(String fullUrl) throws java.net.MalformedURLException {
+        activeThreads = 0;
+        pendingThreadQueue = new ConcurrentLinkedQueue();
+        url = new URL(fullUrl);
+    }
+
+    public int getMaxThreads() {
+        return maxThreads;
+    }
+
+    /** 
+     * Set the maximum number of actively communicating threads allowed 
+     */
+    public void setMaxThreads(int max) {
+        maxThreads = max;
+    }
+
+    /**
+     * Launches or queues an asynchronous request.
+     *
+     * If the maximum active thread count has not been reached,
+     * start a new thread and use it to send and receive the request.
+     * The response is passed to the request's HttpRequestHandler
+     * onComplete().  After complete, if the number of active threads
+     * is still lower than the max, one request will be pulled (if 
+     * present) from the async queue and fired.
+     *
+     * If there are too many active threads, the main request is
+     * pushed onto the async queue for later processing
+     */
+    protected void manageAsyncRequest(final HttpRequest request) {
+
+        if (activeThreads >= maxThreads) {
+            pendingThreadQueue.offer(request);
+            return;
+        }
+
+        activeThreads++;
+
+         //Send the request receive the response, fire off the next 
+         //thread if necessary, then pass the result to the handler
+        Runnable r = new Runnable() {
+            public void run() {
+
+                Object response;
+                request.send();
+
+                while ((response = request.recv()) != null) {
+                    if (request.handler != null) 
+                        request.handler.onResponse(request, response);
+                }
+
+                if (request.handler != null)
+                    request.handler.onComplete(request);
+
+                activeThreads--;
+
+                if (activeThreads < maxThreads) {
+                    try {
+                        manageAsyncRequest(pendingThreadQueue.remove());
+                    } catch (java.util.NoSuchElementException ex) {
+                        // may have been gobbled by another thread
+                    }
+                }
+            }
+        };
+
+        new Thread(r).start();
+    }
+}
+
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpRequest.java b/Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpRequest.java
new file mode 100644 (file)
index 0000000..8af4623
--- /dev/null
@@ -0,0 +1,70 @@
+package org.opensrf.net.http;
+import org.opensrf.*;
+import org.opensrf.util.*;
+
+import java.util.List;
+import java.util.LinkedList;
+import java.net.HttpURLConnection;
+
+public abstract class HttpRequest {
+
+    protected String service;
+    protected Method method;
+    protected HttpURLConnection urlConn;
+    protected HttpConnection httpConn;
+    protected HttpRequestHandler handler;
+    protected List<Object> responseList;
+    protected Exception failure;
+    protected boolean failed;
+    protected boolean complete;
+
+    public HttpRequest() {
+        failed = false;
+        complete = false;
+        handler = null;
+        urlConn = null;
+    }
+
+    public HttpRequest(HttpConnection conn, String service, Method method) {
+        this();
+        this.httpConn = conn;
+        this.service = service;
+        this.method = method;
+    }
+
+    public void sendAsync(final HttpRequestHandler handler) {
+        this.handler = handler;
+        httpConn.manageAsyncRequest(this);
+    }
+
+    protected void pushResponse(Object response) {
+        if (responseList == null)
+            responseList = new LinkedList<Object>();
+        responseList.add(response);
+    }
+
+    protected List responses() {
+        return responseList;
+    }
+    
+    protected Object nextResponse() {
+        if (complete || failed) return null;
+        if (responseList.size() > 0)
+            return responseList.remove(0);
+        return null;
+    }
+
+    public Exception getFailure() {
+        return failure;
+    }
+
+    public abstract HttpRequest send();
+
+    public abstract Object recv();
+    
+    public boolean failed(){
+       return failed;
+    }
+}
+
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpRequestHandler.java b/Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpRequestHandler.java
new file mode 100644 (file)
index 0000000..9c0f9e5
--- /dev/null
@@ -0,0 +1,25 @@
+package org.opensrf.net.http;
+
+import java.util.List;
+
+/*
+ * Handler for async gateway responses.
+ */
+public abstract class HttpRequestHandler {
+
+    /**
+     * Called when all responses have been received.
+     *
+     * If discardResponses() returns true, will be passed null.
+     */
+    public void onComplete(HttpRequest request) {
+    }
+
+    /**
+     * Called with each response received from the server.
+     * 
+     * @param payload the value returned from the server.
+     */
+    public void onResponse(HttpRequest request, Object response) {
+    }
+}
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPException.java b/Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPException.java
new file mode 100644 (file)
index 0000000..8c20ab7
--- /dev/null
@@ -0,0 +1,10 @@
+package org.opensrf.net.xmpp;
+
+/**
+ * Used for XMPP stream/authentication errors
+ */
+public class XMPPException extends Exception {
+    public XMPPException(String info) {
+        super(info);
+    }
+}
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPMessage.java b/Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPMessage.java
new file mode 100644 (file)
index 0000000..b6e2c76
--- /dev/null
@@ -0,0 +1,101 @@
+package org.opensrf.net.xmpp;
+
+import java.io.*;
+
+/**
+ * Models a single XMPP message.
+ */
+public class XMPPMessage {
+
+    /** Message body */
+    private String body;
+    /** Message recipient */
+    private String to;
+    /** Message sender */
+    private String from;
+    /** Message thread */
+    private String thread;
+    /** Message xid */
+    private String xid;
+
+    public XMPPMessage() {
+    }
+
+    public String getBody() {
+        return body;
+    }
+    public String getTo() { 
+        return to; 
+    }
+    public String getFrom() { 
+        return from;
+    }
+    public String getThread() { 
+        return thread; 
+    }
+    public String getXid() {
+        return xid;
+    }
+    public void setBody(String body) {
+        this.body = body;
+    }
+    public void setTo(String to) { 
+        this.to = to; 
+    }
+    public void setFrom(String from) { 
+        this.from = from; 
+    }
+    public void setThread(String thread) { 
+        this.thread = thread; 
+    }
+    public void setXid(String xid) {
+        this.xid = xid; 
+    }
+
+
+    /**
+     * Generates the XML representation of this message.
+     */
+    public String toXML() {
+        StringBuffer sb = new StringBuffer("<message to='");
+        escapeXML(to, sb);
+        sb.append("' osrf_xid='");
+        escapeXML(xid, sb);
+        sb.append("'><thread>");
+        escapeXML(thread, sb);
+        sb.append("</thread><body>");
+        escapeXML(body, sb);
+        sb.append("</body></message>");
+        return sb.toString();
+    }
+
+
+    /**
+     * Escapes non-valid XML characters.
+     * @param s The string to escape.
+     * @param sb The StringBuffer to append new data to.
+     */
+    private void escapeXML(String s, StringBuffer sb) {
+        if( s == null ) return;
+        char c;
+        int l = s.length();
+        for( int i = 0; i < l; i++ ) {
+            c = s.charAt(i);
+            switch(c) {
+                case '<': 
+                    sb.append("&lt;");
+                    break;
+                case '>': 
+                    sb.append("&gt;");
+                    break;
+                case '&': 
+                    sb.append("&amp;");
+                    break;
+                default:
+                    sb.append(c);
+            }
+        }
+    }
+}
+
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPReader.java b/Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPReader.java
new file mode 100644 (file)
index 0000000..406298a
--- /dev/null
@@ -0,0 +1,293 @@
+package org.opensrf.net.xmpp;
+
+import javax.xml.stream.*;
+import javax.xml.stream.events.* ;
+import javax.xml.namespace.QName;
+import java.util.Queue;
+import java.io.InputStream;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.Date;
+import org.opensrf.util.Logger;
+
+/**
+ * Slim XMPP Stream reader.  This reader only understands enough XMPP
+ * to handle logins and recv messages.  It's implemented as a StAX parser.
+ * @author Bill Erickson, Georgia Public Library Systems
+ */
+public class XMPPReader implements Runnable {
+
+    /** Queue of received messages. */
+    private Queue<XMPPMessage> msgQueue;
+    /** Incoming XMPP XML stream */
+    private InputStream inStream;
+    /** Current message body */
+    private StringBuffer msgBody;
+    /** Current message thread */
+    private StringBuffer msgThread;
+    /** Current message status */
+    private StringBuffer msgStatus;
+    /** Current message error type */
+    private StringBuffer msgErrType;
+    /** Current message sender */
+    private String msgFrom;
+    /** Current message recipient */
+    private String msgTo;
+    /** Current message error code */
+    private int msgErrCode;
+
+    /** Where this reader currently is in the document */
+    private XMLState xmlState;
+
+    /** The current connect state to the XMPP server */
+    private XMPPStreamState streamState;
+
+
+    /** Used to represent out connection state to the XMPP server */
+    public static enum XMPPStreamState {
+        DISCONNECTED,   /* not connected to the server */
+        CONNECT_SENT,   /* we've sent the initial connect message */
+        CONNECT_RECV,   /* we've received a response to our connect message */
+        AUTH_SENT,      /* we've sent an authentication request */
+        CONNECTED       /* authentication is complete */
+    };
+
+
+    /** Used to represents where we are in the XML document stream. */
+    public static enum XMLState {
+        IN_NOTHING,
+        IN_BODY,
+        IN_THREAD,
+        IN_STATUS
+    };
+
+
+    /**
+     * Creates a new reader. Initializes the message queue.
+     * Sets the stream state to disconnected, and the xml
+     * state to in_nothing.
+     * @param inStream the inbound XML stream
+     */
+    public XMPPReader(InputStream inStream) {
+        msgQueue = new ConcurrentLinkedQueue<XMPPMessage>();
+        this.inStream = inStream;
+        resetBuffers();
+        xmlState = XMLState.IN_NOTHING;
+        streamState = XMPPStreamState.DISCONNECTED;
+    }
+
+    /**
+     * Change the connect state and notify that a core 
+     * event has occurred.
+     */
+    protected void setXMPPStreamState(XMPPStreamState state) {
+        streamState = state;
+        notifyCoreEvent();
+    }
+
+    /**
+     * @return The current stream state of the reader 
+     */
+    public XMPPStreamState getXMPPStreamState() {
+        return streamState;
+    }
+
+
+    /**
+     * @return The next message in the queue, or null
+     */
+    public XMPPMessage popMessageQueue() {
+        return (XMPPMessage) msgQueue.poll();
+    }
+
+
+    /**
+     * Initializes the message buffers 
+     */
+    private void resetBuffers() {
+        msgBody = new StringBuffer();
+        msgThread = new StringBuffer();
+        msgStatus = new StringBuffer(); 
+        msgErrType = new StringBuffer();
+        msgFrom = "";
+        msgTo = "";
+    }
+
+
+    /**
+     * Notifies the waiting thread that a core event has occurred.
+     * Each reader should have exactly one dependent session thread. 
+     */
+    private synchronized void notifyCoreEvent() {
+        notifyAll();
+    }
+
+
+    /**
+     * Waits up to timeout milliseconds for a core event to occur. 
+     * Also, having a message already waiting in the queue 
+     * constitutes a core event.
+     * @param timeout The number of milliseconds to wait.  If 
+     * timeout is negative, waits potentially forever.
+     * @return The number of milliseconds in wait
+     */
+    public synchronized long waitCoreEvent(long timeout) {
+
+        if(msgQueue.peek() != null || timeout == 0) return 0;
+        long start = new Date().getTime();
+
+        try{
+            if(timeout < 0) 
+                wait();
+            else 
+                wait(timeout);
+        } catch(InterruptedException ie) {}
+
+        return new Date().getTime() - start;
+    }
+
+
+
+    /** Kickoff the thread */
+    public void run() {
+        read();
+    }
+
+
+    /**
+     * Parses XML data from the provided XMPP stream.
+     */
+    public void read() {
+
+        try {
+
+            XMLInputFactory factory = XMLInputFactory.newInstance();
+
+            /** disable as many unused features as possible to speed up the parsing */
+            factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.FALSE);
+            factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE);
+            factory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE);
+            factory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.FALSE);
+            factory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
+
+            /** create the stream reader */
+            XMLStreamReader reader = factory.createXMLStreamReader(inStream);
+            int eventType;
+
+            while(reader.hasNext()) {
+                /** cycle through the XML events */
+
+                eventType = reader.next();
+
+                switch(eventType) {
+
+                    case XMLEvent.START_ELEMENT:
+                        handleStartElement(reader);
+                        break;
+
+                    case XMLEvent.CHARACTERS:
+                        switch(xmlState) {
+                            case IN_BODY:
+                                msgBody.append(reader.getText());
+                                break;
+                            case IN_THREAD:
+                                msgThread.append(reader.getText());
+                                break;
+                            case IN_STATUS:
+                                msgStatus.append(reader.getText());
+                                break;
+                        }
+                        break;
+
+                    case XMLEvent.END_ELEMENT: 
+                        xmlState = XMLState.IN_NOTHING;
+                        if("message".equals(reader.getName().toString())) {
+
+                           /** build a message and add it to the message queue */
+                           XMPPMessage msg = new XMPPMessage();
+                           msg.setFrom(msgFrom);
+                           msg.setTo(msgTo);
+                           msg.setBody(msgBody.toString());
+                           msg.setThread(msgThread.toString());
+
+                           Logger.internal("xmpp message from="+msgFrom+" " + msg.getBody());
+
+                           msgQueue.offer(msg);
+                           resetBuffers(); 
+                           notifyCoreEvent();
+                        }
+                        break;
+                }
+            }
+
+        } catch(javax.xml.stream.XMLStreamException se) {
+            /* XXX log an error */
+            xmlState = XMLState.IN_NOTHING;
+            streamState = XMPPStreamState.DISCONNECTED;
+            notifyCoreEvent();
+        }
+    }
+
+
+    /**
+     * Handles the start_element event.
+     */
+    private void handleStartElement(XMLStreamReader reader) {
+
+        String name = reader.getName().toString();
+
+        if("message".equals(name)) {
+            xmlState = XMLState.IN_BODY;
+
+            /** add a special case for the opensrf "router_from" attribute */
+            String rf = reader.getAttributeValue(null, "router_from");
+            if( rf != null )
+                msgFrom = rf;
+            else
+                msgFrom = reader.getAttributeValue(null, "from");
+            msgTo = reader.getAttributeValue(null, "to");
+            return;
+        }
+
+        if("body".equals(name)) {
+            xmlState = XMLState.IN_BODY;
+            return;
+        }
+
+        if("thread".equals(name)) {
+            xmlState = XMLState.IN_THREAD;
+            return;
+        }
+
+        if("stream:stream".equals(name)) {
+            setXMPPStreamState(XMPPStreamState.CONNECT_RECV);
+            return;
+        }
+
+        if("iq".equals(name)) {
+            if("result".equals(reader.getAttributeValue(null, "type")))
+                setXMPPStreamState(XMPPStreamState.CONNECTED);
+            return;
+        }
+
+        if("status".equals(name)) {
+            xmlState = XMLState.IN_STATUS;
+            return;
+        }
+
+        if("stream:error".equals(name)) {
+            setXMPPStreamState(XMPPStreamState.DISCONNECTED);
+            return;
+        }
+
+        if("error".equals(name)) {
+            msgErrType.append(reader.getAttributeValue(null, "type"));
+            msgErrCode = Integer.parseInt(reader.getAttributeValue(null, "code"));
+            setXMPPStreamState(XMPPStreamState.DISCONNECTED);
+            return;
+        }
+    }
+}
+
+
+
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPSession.java b/Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPSession.java
new file mode 100644 (file)
index 0000000..f9be7d2
--- /dev/null
@@ -0,0 +1,263 @@
+package org.opensrf.net.xmpp;
+
+import java.io.*;
+import java.net.Socket;
+import java.util.Map;
+import java.util.Iterator;
+import java.util.concurrent.ConcurrentHashMap;
+
+
+/**
+ * Represents a single XMPP session.  Sessions are responsible for writing to
+ * the stream and for managing a stream reader.
+ */
+public class XMPPSession {
+
+    /** Initial jabber message */
+    public static final String JABBER_CONNECT = 
+        "<stream:stream to='%s' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'>";
+
+    /** Basic auth message */
+    public static final String JABBER_BASIC_AUTH =  
+        "<iq id='123' type='set'><query xmlns='jabber:iq:auth'>" +
+        "<username>%s</username><password>%s</password><resource>%s</resource></query></iq>";
+
+    public static final String JABBER_DISCONNECT = "</stream:stream>";
+
+    private static Map threadConnections = new ConcurrentHashMap();
+
+    /** jabber domain */
+    private String host;
+    /** jabber port */
+    private int port;
+    /** jabber username */
+    private String username;
+    /** jabber password */
+    private String password;
+    /** jabber resource */
+    private String resource;
+
+    /** XMPP stream reader */
+    XMPPReader reader;
+    /** Fprint-capable socket writer */
+    PrintWriter writer;
+    /** Raw socket output stream */
+    OutputStream outStream;
+    /** The raw socket */
+    Socket socket;
+
+    /** The process-wide session.  All communication occurs
+     * accross this single connection */
+    private static XMPPSession globalSession;
+
+
+    /**
+     * Creates a new session.
+     * @param host The jabber domain
+     * @param port The jabber port
+     */
+    public XMPPSession( String host, int port ) {
+        this.host = host;
+        this.port = port;
+    }
+
+    /**
+     * Returns the global, process-wide session
+     */
+    /*
+    public static XMPPSession getGlobalSession() {
+        return globalSession;
+    }
+    */
+
+    public static XMPPSession getThreadSession() {
+        return (XMPPSession) threadConnections.get(new Long(Thread.currentThread().getId()));
+    }
+
+    /**
+     * Sets the given session as the global session for the current thread
+     * @param ses The session
+     */
+    public static void setThreadSession(XMPPSession ses) {
+        /* every time we create a new connection, clean up any dead threads. 
+         * this is cheaper than cleaning up the dead threads at every access. */
+        cleanupThreadSessions();
+        threadConnections.put(new Long(Thread.currentThread().getId()), ses);
+    }
+
+    /**
+     * Analyzes the threadSession data to see if there are any sessions
+     * whose controlling thread has gone away.  
+     */
+    private static void cleanupThreadSessions() {
+        Thread threads[] = new Thread[Thread.activeCount()]; 
+        Thread.enumerate(threads);
+        for(Iterator i = threadConnections.keySet().iterator(); i.hasNext(); ) {
+            boolean found = false;
+            Long id = (Long) i.next();
+            for(Thread t : threads) {
+                if(t.getId() == id.longValue()) {
+                    found = true;
+                    break;
+                }
+            }
+            if(!found) 
+                threadConnections.remove(id);
+        }
+    }
+
+    /**
+     * Sets the global, process-wide section
+     */
+    /*
+    public static void setGlobalSession(XMPPSession ses) {
+        globalSession = ses;
+    }
+    */
+
+
+    /** true if this session is connected to the server */
+    public boolean connected() {
+        return (
+                reader != null && 
+                reader.getXMPPStreamState() == XMPPReader.XMPPStreamState.CONNECTED &&
+                !socket.isClosed()
+            );
+    }
+
+
+    /**
+     * Connects to the network.
+     * @param username The jabber username
+     * @param password The jabber password
+     * @param resource The Jabber resource
+     */
+    public void connect(String username, String password, String resource) throws XMPPException {
+
+        this.username = username;
+        this.password = password;
+        this.resource = resource;
+
+        try { 
+            /* open the socket and associated streams */
+            socket = new Socket(host, port);
+
+            /** the session maintains control over the output stream */
+            outStream = socket.getOutputStream();
+            writer = new PrintWriter(outStream, true);
+
+            /** pass the input stream to the reader */
+            reader = new XMPPReader(socket.getInputStream());
+
+        } catch(IOException ioe) {
+            throw new 
+                XMPPException("unable to communicate with host " + host + " on port " + port);
+        }
+
+        /* build the reader thread */
+        Thread thread = new Thread(reader);
+        thread.setDaemon(true);
+        thread.start();
+
+        synchronized(reader) {
+            /* send the initial jabber message */
+            sendConnect();
+            reader.waitCoreEvent(10000);
+        }
+        if( reader.getXMPPStreamState() != XMPPReader.XMPPStreamState.CONNECT_RECV ) 
+            throw new XMPPException("unable to connect to jabber server");
+
+        synchronized(reader) {
+            /* send the basic auth message */
+            sendBasicAuth(); 
+            reader.waitCoreEvent(10000);
+        }
+        if(!connected()) 
+            throw new XMPPException("Authentication failed");
+    }
+
+    /** Sends the initial jabber message */
+    private void sendConnect() {
+        reader.setXMPPStreamState(XMPPReader.XMPPStreamState.CONNECT_SENT);
+        writer.printf(JABBER_CONNECT, host);
+    }
+
+    /** Send the basic auth message */
+    private void sendBasicAuth() {
+        reader.setXMPPStreamState(XMPPReader.XMPPStreamState.AUTH_SENT);
+        writer.printf(JABBER_BASIC_AUTH, username, password, resource);
+    }
+
+
+    /**
+     * Sends an XMPPMessage.
+     * @param msg The message to send.
+     */
+    public synchronized void send(XMPPMessage msg) throws XMPPException {
+        checkConnected();
+        try {
+            String xml = msg.toXML();
+            outStream.write(xml.getBytes()); 
+        } catch (Exception e) {
+            throw new XMPPException(e.toString());
+        }
+    }
+
+
+    /**
+     * @throws XMPPException if we are no longer connected.
+     */
+    private void checkConnected() throws XMPPException {
+        if(!connected())
+            throw new XMPPException("Disconnected stream");
+    }
+
+
+    /**
+     * Receives messages from the network.  
+     * @param timeout Maximum number of milliseconds to wait for a message to arrive.
+     * If timeout is negative, this method will wait indefinitely.
+     * If timeout is 0, this method will not block at all, but will return a 
+     * message if there is already a message available.
+     */
+    public XMPPMessage recv(long timeout) throws XMPPException {
+
+        XMPPMessage msg;
+
+        if(timeout < 0) {
+
+            while(true) { /* wait indefinitely for a message to arrive */
+                reader.waitCoreEvent(timeout);
+                msg = reader.popMessageQueue();
+                if( msg != null ) return msg;
+                checkConnected();
+            }
+
+        } else {
+
+            while(timeout >= 0) { /* wait at most 'timeout' milleseconds for a message to arrive */
+                msg = reader.popMessageQueue();
+                if( msg != null ) return msg;
+                timeout -= reader.waitCoreEvent(timeout);
+                msg = reader.popMessageQueue();
+                if( msg != null ) return msg;
+                checkConnected();
+                if(timeout == 0) break;
+            }
+        }
+
+        return reader.popMessageQueue();
+    }
+
+
+    /**
+     * Disconnects from the jabber server and closes the socket
+     */
+    public void disconnect() {
+        try {
+            outStream.write(JABBER_DISCONNECT.getBytes());
+            socket.close();
+        } catch(Exception e) {}
+    }
+}
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/MathBench.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/MathBench.java
new file mode 100644 (file)
index 0000000..b6e67f9
--- /dev/null
@@ -0,0 +1,79 @@
+package org.opensrf.test;
+import org.opensrf.*;
+import org.opensrf.util.*;
+import java.util.Date;
+import java.util.List;
+import java.util.ArrayList;
+import java.io.PrintStream;
+
+
+public class MathBench {
+
+    public static void main(String args[]) throws Exception {
+
+        PrintStream out = System.out;
+
+        if(args.length < 2) {
+            out.println("usage: java org.opensrf.test.MathBench <osrfConfig> <numIterations>");
+            return;
+        }
+
+        /** connect to the opensrf network */
+        Sys.bootstrapClient(args[0], "/config/opensrf");
+
+        /** how many iterations */
+        int count = Integer.parseInt(args[1]);
+
+        /** create the client session */
+        ClientSession session = new ClientSession("opensrf.math");
+
+        /** params are 1,2 */
+        List<Object> params = new ArrayList<Object>();
+        params.add(new Integer(1));
+        params.add(new Integer(2));
+
+        Request request;
+        Result result;
+        long start;
+        double total = 0;
+
+        for(int i = 0; i < count; i++) {
+
+            start = new Date().getTime();
+
+            /** create (and send) the request */
+            request = session.request("add", params);
+
+            /** wait up to 3 seconds for a response */
+            result = request.recv(3000);
+
+            /** collect the round-trip time */
+            total += new Date().getTime() - start;
+
+            if(result.getStatusCode() == Status.OK) {
+                out.print("+");
+            } else {
+                out.println("\nrequest failed");
+                out.println("status = " + result.getStatus());
+                out.println("status code = " + result.getStatusCode());
+            }
+
+            /** remove this request from the session's request set */
+            request.cleanup();
+
+            if((i+1) % 100 == 0) /** print 100 responses per line */
+                out.println(" [" + (i+1) + "]");
+        }
+
+        out.println("\nAverage request time is " + (total/count) + " ms");
+        
+        /** remove this session from the global session cache */
+        session.cleanup();
+
+        /** disconnect from the opensrf network */
+        Sys.shutdown();
+    }
+}
+
+
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestCache.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestCache.java
new file mode 100644 (file)
index 0000000..d555444
--- /dev/null
@@ -0,0 +1,24 @@
+package org.opensrf.test;
+import org.opensrf.util.Cache;
+
+public class TestCache {
+    public static void main(String args[]) throws Exception {
+
+        /**
+         * args is a list of string like so:  server:port server2:port server3:port ...
+         */
+
+        Cache.initCache(args);
+        Cache cache = new Cache();
+
+        cache.set("key1", "HI, MA!");
+        cache.set("key2", "HI, MA! 2");
+        cache.set("key3", "HI, MA! 3");
+
+        System.out.println("got key1 = " + (String) cache.get("key1"));
+        System.out.println("got key2 = " + (String) cache.get("key2"));
+        System.out.println("got key3 = " + (String) cache.get("key3"));
+    }
+}
+
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestClient.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestClient.java
new file mode 100644 (file)
index 0000000..a1136cd
--- /dev/null
@@ -0,0 +1,80 @@
+package org.opensrf.test;
+import org.opensrf.*;
+import org.opensrf.util.*;
+import java.util.Map;
+import java.util.Date;
+import java.util.List;
+import java.util.ArrayList;
+import java.io.PrintStream;
+
+public class TestClient {
+
+    public static void main(String args[]) throws Exception {
+
+        /** which opensrf service are we sending our request to */
+        String service; 
+        /** which opensrf method we're calling */
+        String method;
+        /** method params, captures from command-line args */
+        List<Object> params;
+        /** knows how to read JSON */
+        JSONReader reader;
+        /** opensrf request */
+        Request request;
+        /** request result */
+        Result result;
+        /** start time for the request */
+        long start;
+        /** for brevity */
+        PrintStream out = System.out;
+
+        if(args.length < 3) {
+            out.println( "usage: org.opensrf.test.TestClient "+
+                "<osrfConfigFile> <service> <method> [<JSONparam1>, <JSONparam2>]");
+            return;
+        }
+
+        /** connect to the opensrf network,  default config context 
+         * for opensrf_core.xml is /config/opensrf */
+        Sys.bootstrapClient(args[0], "/config/opensrf");
+
+        /* grab the server, method, and any params from the command line */
+        service = args[1];
+        method = args[2];
+        params = new ArrayList<Object>();
+        for(int i = 3; i < args.length; i++) 
+            params.add(new JSONReader(args[i]).read());
+
+
+        /** build the client session */
+        ClientSession session = new ClientSession(service);
+
+        /** kick off the timer */
+        start = new Date().getTime();
+
+        /** Create the request object from the session, method and params */
+        request = session.request(method, params);
+
+        while( (result = request.recv(60000)) != null ) { 
+            /** loop over the results and print the JSON version of the content */
+
+            if(result.getStatusCode() != 200) { 
+                /** make sure the request succeeded */
+                out.println("status = " + result.getStatus());
+                out.println("status code = " + result.getStatusCode());
+                continue;
+            }
+
+            /** JSON-ify the resulting object and print it */
+            out.println("\nresult JSON: " + new JSONWriter(result.getContent()).write());
+        }
+        
+        /** How long did the request take? */
+        out.println("Request round trip took: " + (new Date().getTime() - start) + " ms.");
+
+        Sys.shutdown();
+    }
+}
+
+
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestConfig.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestConfig.java
new file mode 100644 (file)
index 0000000..f65a84f
--- /dev/null
@@ -0,0 +1,16 @@
+package org.opensrf.test;
+import org.opensrf.*;
+import org.opensrf.util.*;
+
+public class TestConfig {
+    public static void main(String args[]) throws Exception {
+        Config config = new Config("");
+        config.parse(args[0]);
+        Config.setConfig(config);
+        System.out.println(config);
+        System.out.println("");
+
+        for(int i = 1; i < args.length; i++) 
+            System.out.println("Found config value: " + args[i] + ": " + Config.global().get(args[i]));
+    }
+}
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestJSON.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestJSON.java
new file mode 100644 (file)
index 0000000..b19d408
--- /dev/null
@@ -0,0 +1,51 @@
+package org.opensrf.test;
+
+import org.opensrf.*;
+import org.opensrf.util.*;
+import java.util.*;
+
+public class TestJSON {
+
+    public static void main(String args[]) throws Exception {
+        
+        Map<String,Object> map = new HashMap<String,Object>();
+        map.put("key1", "value1");
+        map.put("key2", "value2");
+        map.put("key3", "value3");
+        map.put("key4", "athe\u0301s");
+        map.put("key5", null);
+
+        List<Object> list = new ArrayList<Object>(16);
+        list.add(new Integer(1));
+        list.add(new Boolean(true));
+        list.add("WATER");
+        list.add(null);
+        map.put("key6", list);
+
+        System.out.println(new JSONWriter(map).write() + "\n");
+
+        String[] fields = {"isnew", "name", "shortname", "ill_address"};
+        OSRFRegistry.registerObject("aou", OSRFRegistry.WireProtocol.ARRAY, fields);
+
+        OSRFObject obj = new OSRFObject(OSRFRegistry.getRegistry("aou"));
+        obj.put("name", "athens clarke county");
+        obj.put("ill_address", new Integer(1));
+        obj.put("shortname", "ARL-ATH");
+
+        map.put("key7", obj);
+        list.add(obj);
+        System.out.println(new JSONWriter(map).write() + "\n");
+
+
+        Message m = new Message(1, Message.REQUEST);
+        Method method = new Method("opensrf.settings.host_config.get");
+        method.addParam("app07.dev.gapines.org");
+        m.setPayload(method);
+
+        String s = new JSONWriter(m).write();
+        System.out.println(s + "\n");
+
+        Object o = new JSONReader(s).read();
+        System.out.println("Read+Wrote: " + new JSONWriter(o).write());
+    }
+}
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestLog.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestLog.java
new file mode 100644 (file)
index 0000000..1d60242
--- /dev/null
@@ -0,0 +1,15 @@
+package org.opensrf.test;
+import org.opensrf.util.Logger;
+import org.opensrf.util.FileLogger;
+
+
+/** Simple test class for tesing the logging functionality */
+public class TestLog {
+    public static void main(String args[]) {
+       Logger.init(Logger.DEBUG, new FileLogger("test.log")); 
+       Logger.error("Hello, world");
+       Logger.warn("Hello, world");
+       Logger.info("Hello, world");
+       Logger.debug("Hello, world");
+    }
+}
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestMultiSession.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestMultiSession.java
new file mode 100644 (file)
index 0000000..bb0f1a1
--- /dev/null
@@ -0,0 +1,26 @@
+package org.opensrf.test;
+import org.opensrf.*;
+import org.opensrf.util.*;
+
+public class TestMultiSession {
+    public static void main(String[] args) {
+        try {
+            String config = args[0];
+
+            Sys.bootstrapClient(config, "/config/opensrf");
+            MultiSession ses = new MultiSession();
+
+            for(int i = 0; i < 40; i++) {
+                ses.request("opensrf.settings", "opensrf.system.time");
+            }
+
+            while(!ses.isComplete()) 
+                System.out.println("result = " + ses.recv(5000) + " and id = " + ses.lastId());
+
+            System.out.println("done");
+            Sys.shutdown();
+        } catch(Exception e) {
+            e.printStackTrace();
+        }
+    }
+}
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestSettings.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestSettings.java
new file mode 100644 (file)
index 0000000..116bbe1
--- /dev/null
@@ -0,0 +1,14 @@
+package org.opensrf.test;
+import org.opensrf.*;
+import org.opensrf.util.*;
+
+public class TestSettings {
+    public static void main(String args[]) throws Exception {
+        Sys.bootstrapClient(args[0], "/config/opensrf");
+        SettingsClient client = SettingsClient.instance();
+        String lang = client.getString("/apps/opensrf.settings/language");
+        String impl = client.getString("/apps/opensrf.settings/implementation");
+        System.out.println("opensrf.settings language = " + lang);
+        System.out.println("opensrf.settings implementation = " + impl);
+    }
+}
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestThread.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestThread.java
new file mode 100644 (file)
index 0000000..bb4cf06
--- /dev/null
@@ -0,0 +1,68 @@
+package org.opensrf.test;
+import org.opensrf.*;
+import org.opensrf.util.*;
+import java.util.Map;
+import java.util.Date;
+import java.util.List;
+import java.util.ArrayList;
+import java.io.PrintStream;
+
+/**
+ * Connects to the opensrf network once per thread and runs
+ * and runs a series of request acccross all launched threads.
+ * The purpose is to verify that the java threaded client api 
+ * is functioning as expected
+ */
+public class TestThread implements Runnable {
+
+    String args[];
+
+    public TestThread(String args[]) {
+        this.args = args;
+    }
+
+    public void run() {
+
+        try {
+
+            Sys.bootstrapClient(args[0], "/config/opensrf");
+            ClientSession session = new ClientSession(args[3]);
+    
+            List params = new ArrayList<Object>();
+            for(int i = 5; i < args.length; i++) 
+                params.add(new JSONReader(args[3]).read());
+    
+            for(int i = 0; i < Integer.parseInt(args[2]); i++) {
+                System.out.println("thread " + Thread.currentThread().getId()+" sending request " + i);
+                Request request = session.request(args[4], params);
+                Result result = request.recv(3000);
+                if(result != null) {
+                    System.out.println("thread " + Thread.currentThread().getId()+ 
+                        " got result JSON: " + new JSONWriter(result.getContent()).write());
+                } else {
+                    System.out.println("* thread " + Thread.currentThread().getId()+ " got NO result");
+                }
+            }
+    
+            Sys.shutdown();
+        } catch(Exception e) {
+            System.err.println(e);
+        }
+    }
+
+    public static void main(String args[]) throws Exception {
+
+        if(args.length < 5) {
+            System.out.println( "usage: org.opensrf.test.TestClient "+
+                "<osrfConfigFile> <numthreads> <numiter> <service> <method> [<JSONparam1>, <JSONparam2>]");
+            return;
+        }
+
+        int numThreads = Integer.parseInt(args[1]);
+        for(int i = 0; i < numThreads; i++) 
+            new Thread(new TestThread(args)).start();
+    }
+}
+
+
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMLFlattener.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMLFlattener.java
new file mode 100644 (file)
index 0000000..c1fa394
--- /dev/null
@@ -0,0 +1,11 @@
+package org.opensrf.test;
+import org.opensrf.util.XMLFlattener;
+import java.io.FileInputStream;
+
+public class TestXMLFlattener {
+    public static void main(String args[]) throws Exception {
+        FileInputStream fis = new FileInputStream(args[0]);
+        XMLFlattener f = new XMLFlattener(fis);
+        System.out.println(f.read());
+    }
+}
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMLTransformer.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMLTransformer.java
new file mode 100644 (file)
index 0000000..9768372
--- /dev/null
@@ -0,0 +1,22 @@
+package org.opensrf.test;
+import org.opensrf.util.XMLTransformer;
+import java.io.File;
+
+public class TestXMLTransformer {
+    /**
+     * arg[0] path to an XML file
+     * arg[1] path to the XSL file to apply
+     */
+    public static void main(String[] args) {
+        try {
+            File xmlFile = new File(args[0]);
+            File xslFile = new File(args[1]);
+            XMLTransformer t = new XMLTransformer(xmlFile, xslFile);
+            System.out.println(t.apply());
+        } catch(Exception e) {
+            e.printStackTrace();
+        }
+    }
+}
+
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMPP.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMPP.java
new file mode 100644 (file)
index 0000000..2fba67f
--- /dev/null
@@ -0,0 +1,63 @@
+package org.opensrf.test;
+
+import org.opensrf.net.xmpp.XMPPReader;
+import org.opensrf.net.xmpp.XMPPMessage;
+import org.opensrf.net.xmpp.XMPPSession;
+
+public class TestXMPP {
+
+    /**
+     * Connects to the jabber server and waits for inbound messages.
+     * If a recipient is provided, a small message is sent to the recipient.
+     */
+    public static void main(String args[]) throws Exception {
+
+        String host;
+        int port;
+        String username;
+        String password;
+        String resource;
+        String recipient;
+
+        try {
+            host = args[0];
+            port = Integer.parseInt(args[1]);
+            username = args[2];
+            password = args[3];
+            resource = args[4];
+
+        } catch(ArrayIndexOutOfBoundsException e) {
+            System.err.println("usage: org.opensrf.test.TestXMPP <host> <port> <username> <password> <resource> [<recipient>]");
+            return;
+        }
+
+        XMPPSession session = new XMPPSession(host, port);
+        session.connect(username, password, resource);
+
+        XMPPMessage msg;
+
+        if( args.length == 6 ) {
+
+            /** they specified a recipient */
+
+            recipient = args[5];
+            msg = new XMPPMessage();
+            msg.setTo(recipient);
+            msg.setThread("test-thread");
+            msg.setBody("Hello, from java-xmpp");
+            System.out.println("Sending message to " + recipient);
+            session.send(msg);
+        }
+
+        while(true) {
+            System.out.println("waiting for message...");
+            msg = session.recv(-1); /* wait forever for a message to arrive */
+            System.out.println("got message: " + msg.toXML());
+        }
+    }
+}
+
+
+
+
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/Cache.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/Cache.java
new file mode 100644 (file)
index 0000000..5303688
--- /dev/null
@@ -0,0 +1,38 @@
+package org.opensrf.util;
+import com.danga.MemCached.*;
+import java.util.List;
+
+/**
+ * Memcache client
+ */
+public class Cache extends MemCachedClient {
+
+    public Cache() {
+        super();
+        setCompressThreshold(4096); /* ?? */
+    }
+
+    /**
+     * Initializes the cache client
+     * @param serverList Array of server:port strings specifying the
+     * set of memcache servers this client will talk to
+     */
+    public static void initCache(String[] serverList) {
+        SockIOPool pool = SockIOPool.getInstance();
+        pool.setServers(serverList);
+        pool.initialize();      
+        com.danga.MemCached.Logger logger = 
+            com.danga.MemCached.Logger.getLogger(MemCachedClient.class.getName());
+        logger.setLevel(logger.LEVEL_ERROR);
+    }
+
+    /**
+     * Initializes the cache client
+     * @param serverList List of server:port strings specifying the
+     * set of memcache servers this client will talk to
+     */
+    public static void initCache(List<String> serverList) {
+        initCache(serverList.toArray(new String[]{}));
+    }
+}
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/Config.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/Config.java
new file mode 100644 (file)
index 0000000..ddac9c0
--- /dev/null
@@ -0,0 +1,139 @@
+package org.opensrf.util;
+
+import org.json.*;
+import java.util.Map;
+import java.util.List;
+
+
+/**
+ * Config reader and accesor module.  This module reads an XML config file,
+ * then loads the file into an internal config, whose values may be accessed
+ * by xpath-style lookup paths.
+ */
+public class Config {
+
+    /** The globl config instance */
+    private static Config config;
+    /** The object form of the parsed config */
+    private Map configObject;
+    /** 
+     * The log parsing context.  This is used as a prefix to the
+     * config item search path.  This allows config XML chunks to 
+     * be inserted into arbitrary XML files.
+     */
+    private String context;
+
+    public static Config global() {
+        return config;
+    }
+
+
+    /**
+     * @param context The config context
+     */
+    public Config(String context) {
+        this.context = context;
+    }
+
+    /**
+     * Sets the global config object.
+     * @param c The config object to use.
+     */
+    public static void setGlobalConfig(Config c) {
+        config = c;
+    }
+
+    /**
+     * Parses an XML config file.
+     * @param filename The path to the file to parse.
+     */
+    public void parse(String filename) throws ConfigException {
+        try {
+            String xml = Utils.fileToString(filename);
+            JSONObject jobj = XML.toJSONObject(xml);
+            configObject = (Map) new JSONReader(jobj.toString()).readObject();
+        } catch(Exception e) {
+            throw new ConfigException("Error parsing config", e);
+        }
+    }
+
+    public static void setConfig(Config conf) {
+        config = conf;
+    }
+
+    public void setConfigObject(Map config) {
+        this.configObject = config;
+    }
+
+    protected Map getConfigObject() {
+        return this.configObject;
+    }
+
+
+    /**
+     * Returns the configuration value found at the requested path.
+     * @param path The search path
+     * @return The config value, or null if no value exists at the given path.  
+     * @throws ConfigException thrown if nothing is found at the path
+     */
+    public String getString(String path) throws ConfigException {
+        try {
+            return (String) get(path);
+        } catch(Exception e) {
+            throw new 
+                ConfigException("No config string found at " + path);
+        }
+    }
+
+    /**
+     * Gets the int value at the given path
+     * @param path The search path
+     */
+    public int getInt(String path) throws ConfigException {
+        try {
+            return Integer.parseInt(getString(path));
+        } catch(Exception e) {
+            throw new
+                ConfigException("No config int found at " + path);
+        }
+    }
+
+    /**
+     * Returns the configuration object found at the requested path.
+     * @param path The search path
+     * @return The config value
+     * @throws ConfigException thrown if nothing is found at the path
+     */
+    public Object get(String path) throws ConfigException {
+        try {
+            Object obj = Utils.findPath(configObject, context + path);
+            if(obj == null)
+                throw new ConfigException("");
+            return obj;
+        } catch(Exception e) {
+            e.printStackTrace();
+            throw new ConfigException("No config object found at " + path);
+        }
+    }
+
+    /**
+     * Returns the first item in the list found at the given path.  If
+     * no list is found, ConfigException is thrown.
+     * @param path The search path
+     */
+    public Object getFirst(String path) throws ConfigException {
+        Object obj = get(path); 
+        if(obj instanceof List) 
+            return ((List) obj).get(0);
+        return obj;
+    }
+
+
+    /**
+     * Returns the config as a JSON string
+     */
+    public String toString() {
+        return new JSONWriter(configObject).write();
+    }
+}
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/ConfigException.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/ConfigException.java
new file mode 100644 (file)
index 0000000..c1c491e
--- /dev/null
@@ -0,0 +1,14 @@
+package org.opensrf.util;
+
+/**
+ * Thrown by the Config module when a user requests a configuration
+ * item that does not exist
+ */
+public class ConfigException extends Exception {
+    public ConfigException(String info) {
+        super(info);
+    }
+    public ConfigException(String info, Throwable t) {
+        super(info, t);
+    }
+}
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/FileLogger.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/FileLogger.java
new file mode 100644 (file)
index 0000000..9eb838d
--- /dev/null
@@ -0,0 +1,44 @@
+package org.opensrf.util;
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+
+
+public class FileLogger extends Logger {
+
+    /** File to log to */
+    private String filename;
+
+    /** 
+     * FileLogger constructor
+     * @param filename The path to the log file
+     */
+    public FileLogger(String filename) {
+        this.filename = filename;
+    }
+
+    /**
+     * Logs the mesage to a file.
+     * @param level The log level
+     * @param msg The mesage to log
+     */
+    protected synchronized void log(short level, String msg) {
+        if(level > logLevel) return;
+
+        BufferedWriter out = null;
+        try {
+            out = new BufferedWriter(new FileWriter(this.filename, true));
+            out.write(formatMessage(level, msg) + "\n");
+
+        } catch(Exception e) {
+            /** If we are unable to write our log message, go ahead and
+              * fall back to the default (stdout) logger */
+            Logger.init(logLevel, new Logger());
+            Logger.logByLevel(ERROR, "Unable to write to log file " + this.filename);
+            Logger.logByLevel(level, msg);
+        }
+
+        try {
+            out.close();
+        } catch(Exception e) {}
+    }
+}
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/JSONException.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/JSONException.java
new file mode 100644 (file)
index 0000000..ec28e1d
--- /dev/null
@@ -0,0 +1,9 @@
+package org.opensrf.util;
+/**
+ * Used to indicate JSON parsing errors
+ */
+public class JSONException extends Exception {
+    public JSONException(String s) {
+        super(s);
+    }
+}
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/JSONReader.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/JSONReader.java
new file mode 100644 (file)
index 0000000..55eb5c0
--- /dev/null
@@ -0,0 +1,176 @@
+package org.opensrf.util;
+
+import java.io.*;
+import java.util.*;
+
+import org.json.JSONTokener;
+import org.json.JSONObject;
+import org.json.JSONArray;
+
+
+/**
+ * JSON utilities.
+ */
+public class JSONReader {
+
+    /** Special OpenSRF serializable object netClass key */
+    public static final String JSON_CLASS_KEY = "__c";
+
+    /** Special OpenSRF serializable object payload key */
+    public static final String JSON_PAYLOAD_KEY = "__p";
+
+    /** The JSON string to parser */
+    private String json;
+
+    /**
+     * @param json The JSON to parse
+     */
+    public JSONReader(String json) {
+        this.json = json;
+    }
+
+    /**
+     * Parses JSON and creates an object.
+     * @return The resulting object which may be a List, 
+     * Map, Number, String, Boolean, or null
+     */
+    public Object read() throws JSONException {
+        JSONTokener tk = new JSONTokener(json);
+        try {
+            return readSubObject(tk.nextValue());
+        } catch(org.json.JSONException e) {
+            throw new JSONException(e.toString());
+        }
+    }
+
+    /**
+     * Assumes that a JSON array will be read.  Returns
+     * the resulting array as a list.
+     */
+    public List<?> readArray() throws JSONException {
+        Object o = read();
+        try {
+            return (List<?>) o;
+        } catch(Exception e) {
+            throw new JSONException("readArray(): JSON cast exception");
+        }
+    }
+
+    /**
+     * Assumes that a JSON object will be read.  Returns 
+     * the resulting object as a map.
+     */
+    public Map<?,?> readObject() throws JSONException {
+        Object o = read();
+        try {
+            return (Map<?,?>) o;
+        } catch(Exception e) {
+            throw new JSONException("readObject(): JSON cast exception");
+        }
+    }
+
+
+    /**
+     * Recurse through the object and turn items into maps, lists, etc.
+     */
+    private Object readSubObject(Object obj) throws JSONException {
+
+        if( obj == null || 
+            obj instanceof String || 
+            obj instanceof Number ||
+            obj instanceof Boolean)
+                return obj;
+
+        try {
+
+            if( obj instanceof JSONObject ) {
+
+                /* read objects */
+                String key;
+                JSONObject jobj = (JSONObject) obj;
+                Map<String, Object> map = new HashMap<String, Object>();
+
+                for( Iterator e = jobj.keys(); e.hasNext(); ) {
+                    key = (String) e.next();
+
+                    /* we encoutered the special class key */
+                    if( JSON_CLASS_KEY.equals(key) ) 
+                        return buildRegisteredObject(
+                            (String) jobj.get(key), jobj.get(JSON_PAYLOAD_KEY));
+
+                    /* we encountered the data key */
+                    if( JSON_PAYLOAD_KEY.equals(key) ) 
+                        return buildRegisteredObject(
+                            (String) jobj.get(JSON_CLASS_KEY), jobj.get(key));
+
+                    map.put(key, readSubObject(jobj.get(key)));
+                }
+                return map;
+            } 
+            
+            if ( obj instanceof JSONArray ) {
+
+                JSONArray jarr = (JSONArray) obj;
+                int length = jarr.length();
+                List<Object> list = new ArrayList<Object>(length);
+
+                for( int i = 0; i < length; i++ ) 
+                    list.add(readSubObject(jarr.get(i)));   
+                return list;
+                
+            }
+
+        } catch(org.json.JSONException e) {
+
+            throw new JSONException(e.toString());
+        }
+
+        return null;
+    }
+
+
+
+    /**
+     * Builds an OSRFObject map registered OSRFHash object based on the JSON object data.
+     * @param netClass The network class hint for this object.
+     * @param paylaod The actual object on the wire.
+     */
+    private OSRFObject buildRegisteredObject(
+        String netClass, Object payload) throws JSONException {
+
+        OSRFRegistry registry = OSRFRegistry.getRegistry(netClass);
+        OSRFObject obj = new OSRFObject(registry);
+        try {
+            if( payload instanceof JSONArray ) {
+                JSONArray jarr = (JSONArray) payload;
+
+                /* for each array item, instert the item into the hash.  the hash 
+                 * key is found by extracting the fields array from the registered 
+                 * object at the current array index */
+                String fields[] = registry.getFields();
+                for( int i = 0; i < jarr.length(); i++ ) {
+                    obj.put(fields[i], readSubObject(jarr.get(i)));   
+                }
+
+            } else if( payload instanceof JSONObject ) {
+
+                /* since this is a hash, simply copy the data over */
+                JSONObject jobj = (JSONObject) payload;
+                String key;
+                for( Iterator e = jobj.keys(); e.hasNext(); ) {
+                    key = (String) e.next();
+                    obj.put(key, readSubObject(jobj.get(key)));
+                }
+            }
+
+        } catch(org.json.JSONException e) {
+            throw new JSONException(e.toString());
+        }
+
+        return obj;
+    }
+}
+
+
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/JSONWriter.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/JSONWriter.java
new file mode 100644 (file)
index 0000000..7cb2cca
--- /dev/null
@@ -0,0 +1,172 @@
+package org.opensrf.util;
+
+import java.io.*;
+import java.util.*;
+
+
+/**
+ * JSONWriter
+ */
+public class JSONWriter {
+
+    /** The object to serialize to JSON */
+    private Object obj;
+
+    /**
+     * @param obj The object to encode
+     */
+    public JSONWriter(Object obj) {
+        this.obj = obj;
+    }
+
+
+    /**
+     * Encodes a java object to JSON.
+     */
+    public String write() {
+        StringBuffer sb = new StringBuffer();
+        write(sb);
+        return sb.toString();
+    }
+
+
+
+    /**
+     * Encodes a java object to JSON.
+     * Maps (HashMaps, etc.) are encoded as JSON objects.  
+     * Iterable's (Lists, etc.) are encoded as JSON arrays
+     */
+    public void write(StringBuffer sb) {
+        write(obj, sb);
+    }
+
+    /**
+     * Encodes the object as JSON into the provided buffer
+     */
+    public void write(Object obj, StringBuffer sb) {
+
+        /** JSON null */
+        if(obj == null) {
+            sb.append("null");
+            return;
+        }
+
+        /** JSON string */
+        if(obj instanceof String) {
+            sb.append('"');
+            Utils.escape((String) obj, sb);
+            sb.append('"');
+            return;
+        }
+
+        /** JSON number */
+        if(obj instanceof Number) {
+            sb.append(obj.toString());
+            return;
+        }
+
+        /** JSON array */
+        if(obj instanceof Iterable) {
+            encodeJSONArray((Iterable) obj, sb);
+            return;
+        }
+
+        /** OpenSRF serializable objects */
+        if(obj instanceof OSRFSerializable) {
+            encodeOSRFSerializable((OSRFSerializable) obj, sb);
+            return;
+        }
+
+        /** JSON object */
+        if(obj instanceof Map) {
+            encodeJSONObject((Map) obj, sb);
+            return;
+        }
+
+        /** JSON boolean */
+        if(obj instanceof Boolean) {
+            sb.append((((Boolean) obj).booleanValue() ? "true" : "false"));
+            return;
+        }
+    }
+
+
+    /**
+     * Encodes a List as a JSON array
+     */
+    private void encodeJSONArray(Iterable iterable, StringBuffer sb) {
+        Iterator itr = iterable.iterator();
+        sb.append("[");
+        boolean some = false;
+
+        while(itr.hasNext()) {
+            some = true;
+            write(itr.next(), sb);
+            sb.append(',');
+        }
+
+        /* remove the trailing comma if the array has any items*/
+        if(some) 
+            sb.deleteCharAt(sb.length()-1); 
+        sb.append("]");
+    }
+
+
+    /**
+     * Encodes a Map as a JSON object
+     */
+    private void encodeJSONObject(Map map, StringBuffer sb) {
+        Iterator itr = map.keySet().iterator();
+        sb.append("{");
+        Object key = null;
+
+        while(itr.hasNext()) {
+            key = itr.next();
+            write(key, sb);
+            sb.append(':');
+            write(map.get(key), sb);
+            sb.append(',');
+        }
+
+        /* remove the trailing comma if the object has any items*/
+        if(key != null) 
+            sb.deleteCharAt(sb.length()-1); 
+        sb.append("}");
+    }
+
+
+    /**
+     * Encodes a network-serializable OpenSRF object
+     */
+    private void encodeOSRFSerializable(OSRFSerializable obj, StringBuffer sb) {
+
+        OSRFRegistry reg = obj.getRegistry();
+        String[] fields = reg.getFields();
+        Map<String, Object> map = new HashMap<String, Object>();
+        map.put(JSONReader.JSON_CLASS_KEY, reg.getNetClass());
+
+        if( reg.getWireProtocol() == OSRFRegistry.WireProtocol.ARRAY ) {
+
+            /** encode arrays as lists */
+            List<Object> list = new ArrayList<Object>(fields.length);
+            for(String s : fields)
+                list.add(obj.get(s));
+            map.put(JSONReader.JSON_PAYLOAD_KEY, list);
+
+        } else {
+
+            /** encode hashes as maps */
+            Map<String, Object> subMap = new HashMap<String, Object>();
+            for(String s : fields)
+                subMap.put(s, obj.get(s));
+            map.put(JSONReader.JSON_PAYLOAD_KEY, subMap);
+                
+        }
+
+        /** now serialize the encoded object */
+        write(map, sb);
+    }
+}
+
+
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/Logger.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/Logger.java
new file mode 100644 (file)
index 0000000..2923c23
--- /dev/null
@@ -0,0 +1,144 @@
+package org.opensrf.util;
+import java.text.SimpleDateFormat;
+import java.text.FieldPosition;
+import java.util.Date;
+
+/**
+ * Basic OpenSRF logging API.  This default implementation
+ * logs to stderr.
+ */
+public class Logger {
+
+    /** Log levels */
+    public static final short ERROR = 1;
+    public static final short WARN  = 2;
+    public static final short INFO  = 3;
+    public static final short DEBUG = 4;
+    public static final short INTERNAL = 5;
+
+    /** The global log instance */
+    private static Logger instance;
+    /** The global log level */
+    protected static short logLevel;
+
+    public Logger() {}
+
+    /** Sets the global Logger instance
+     * @param level The global log level.
+     * @param l The Logger instance to use
+     */
+    public static void init(short level, Logger l) {
+        instance = l;
+        logLevel = level;
+    }
+
+    /** 
+     * @return The global Logger instance
+     */
+    public static Logger instance() {
+        return instance;
+    }
+
+    /**
+     * Logs an error message
+     * @param msg The message to log
+     */
+    public static void error(String msg) {
+        instance.log(ERROR, msg);
+    }
+
+    /**
+     * Logs an warning message
+     * @param msg The message to log
+     */
+    public static void warn(String msg) {
+        instance.log(WARN, msg);
+    }
+
+    /**
+     * Logs an info message
+     * @param msg The message to log
+     */
+    public static void info(String msg) {
+        instance.log(INFO, msg);
+    }
+
+    /**
+     * Logs an debug message
+     * @param msg The message to log
+     */
+    public static void debug(String msg) {
+        instance.log(DEBUG, msg);
+    }
+
+    /**
+     * Logs an internal message
+     * @param msg The message to log
+     */
+    public static void internal(String msg) {
+        instance.log(INTERNAL, msg);
+    }
+
+
+    /** 
+     * Appends the text representation of the log level
+     * @param sb The stringbuffer to append to
+     * @param level The log level
+     */
+    protected static void appendLevelString(StringBuffer sb, short level) {
+        switch(level) {
+            case DEBUG:
+                sb.append("DEBG"); break;
+            case INFO:
+                sb.append("INFO"); break;
+            case INTERNAL:
+                sb.append("INT "); break;
+            case WARN:
+                sb.append("WARN"); break;
+            case ERROR:
+                sb.append("ERR "); break;
+        }
+    }
+
+    /**
+     * Formats a message for logging.  Appends the current date+time
+     * and the log level string.
+     * @param level The log level
+     * @param msg The message to log
+     */
+    protected static String formatMessage(short level, String msg) {
+
+        StringBuffer sb = new StringBuffer();
+        new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(
+            new Date(), sb, new FieldPosition(0));
+
+        sb.append(" [");
+        appendLevelString(sb, level);
+        sb.append(":");
+        sb.append(Thread.currentThread().getId());
+        sb.append("] ");
+        sb.append(msg);
+        return sb.toString();
+    }
+
+    /**
+     * Logs a message by passing the log level explicitly
+     * @param level The log level
+     * @param msg The message to log
+     */
+    public static void logByLevel(short level, String msg) {
+        instance.log(level, msg);
+    }
+
+    /**
+     * Performs the actual logging.  Subclasses should override 
+     * this method.
+     * @param level The log level
+     * @param msg The message to log
+     */
+    protected synchronized void log(short level, String msg) {
+        if(level > logLevel) return;
+        System.err.println(formatMessage(level, msg));
+    }
+}
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFObject.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFObject.java
new file mode 100644 (file)
index 0000000..af28f4a
--- /dev/null
@@ -0,0 +1,63 @@
+package org.opensrf.util;
+
+import java.util.Map;
+import java.util.HashMap;
+
+
+/**
+ * Generic OpenSRF network-serializable object.  This allows
+ * access to object fields.  
+ */
+public class OSRFObject extends HashMap<String, Object> implements OSRFSerializable {
+    
+    /** This objects registry */
+    private OSRFRegistry registry;
+
+    public OSRFObject() {
+    }
+
+
+    /**
+     * Creates a new object with the provided registry
+     */
+    public OSRFObject(OSRFRegistry reg) {
+        this();
+        registry = reg;
+    }
+
+
+    /**
+     * Creates a new OpenSRF object based on the net class string
+     * */
+    public OSRFObject(String netClass) {
+        this(OSRFRegistry.getRegistry(netClass));
+    }
+
+
+    /**
+     * @return This object's registry
+     */
+    public OSRFRegistry getRegistry() {
+        return registry;
+    }
+
+    /**
+     * Implement get() to fulfill our contract with OSRFSerializable
+     */
+    public Object get(String field) {
+        return super.get(field);
+    }
+
+    /** Returns the string value found at the given field */
+    public String getString(String field) {
+        return (String) get(field);
+    }
+
+    /** Returns the int value found at the given field */
+    public int getInt(String field) {
+        Object o = get(field);
+        if(o instanceof String)
+            return Integer.parseInt((String) o);
+        return ((Integer) get(field)).intValue();
+    }
+}
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFRegistry.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFRegistry.java
new file mode 100644 (file)
index 0000000..c6e6bc6
--- /dev/null
@@ -0,0 +1,112 @@
+package org.opensrf.util;
+
+import java.io.Serializable;
+import java.util.Map;
+import java.util.HashMap;
+
+
+/**
+ * Manages the registration of OpenSRF network-serializable objects.  
+ * A serializable object has a class "hint" (called netClass within) which
+ * describes the type of object.  Each object also has a set of field names
+ * for accessing/mutating object properties.  Finally, objects have a 
+ * serialization wire protocol.  Currently supported protocols are HASH
+ * and ARRAY.
+ */
+public class OSRFRegistry implements Serializable{
+
+
+    /**
+        * 
+        */
+       private static final long serialVersionUID = 1L;
+       /**
+     * Global collection of registered net objects.  
+     * Maps netClass names to registries.
+     */
+    private static HashMap<String, OSRFRegistry> 
+        registry = new HashMap<String, OSRFRegistry>();
+
+
+    /** Serialization types for registered objects */
+    public enum WireProtocol {
+        ARRAY, HASH
+    };
+
+
+    /** Array of field names for this registered object */
+    String fields[];
+    /** The wire protocol for this object */
+    WireProtocol wireProtocol;
+    /** The network class for this object */
+    String netClass;
+
+    /**
+     * Returns the array of field names
+     */
+    public String[] getFields() {
+        return this.fields;
+    }
+
+
+    /**
+     * Registers a new object.
+     * @param netClass The net class for this object
+     * @param wireProtocol The object's wire protocol
+     * @param fields An array of field names.  For objects whose
+     * wire protocol is ARRAY, the positions of the field names 
+     * will be used as the array indices for the fields at serialization time
+     */
+    public static OSRFRegistry registerObject(String netClass, WireProtocol wireProtocol, String fields[]) {
+        OSRFRegistry r = new OSRFRegistry(netClass, wireProtocol, fields);
+        registry.put(netClass, r);
+        return r;
+    }
+
+    /**
+     * Returns the registry for the given netclass
+     * @param netClass The network class to lookup
+     */
+    public static OSRFRegistry getRegistry(String netClass) {
+        if( netClass == null ) return null;
+        return (OSRFRegistry) registry.get(netClass);
+    }
+
+
+    /**
+     * @param field The name of the field to lookup
+     * @return the index into the fields array of the given field name.
+     */
+    public int getFieldIndex(String field) {
+        for( int i = 0; i < fields.length; i++ )
+            if( fields[i].equals(field) ) 
+                return i;
+        return -1;
+    }
+
+    /** Returns the wire protocol of this object */
+    public WireProtocol getWireProtocol() {
+        return this.wireProtocol;
+    }
+
+    /** Returns the netClass ("hint") of this object */
+    public String getNetClass() {
+        return this.netClass;
+    }
+
+    /**
+     * Creates a new registry object.
+     * @param netClass The network class/hint
+     * @param wireProtocol The wire protocol
+     * @param fields The array of field names.  For array-based objects,
+     * the fields array must be sorted in accordance with the sorting
+     * of the objects in the array.
+     */ 
+    public OSRFRegistry(String netClass, WireProtocol wireProtocol, String fields[]) {
+        this.netClass = netClass;
+        this.wireProtocol = wireProtocol;
+        this.fields = fields;
+    }
+}
+
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFSerializable.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFSerializable.java
new file mode 100644 (file)
index 0000000..64b5d6f
--- /dev/null
@@ -0,0 +1,19 @@
+package org.opensrf.util;
+
+/**
+ * All network-serializable OpenSRF object must implement this interface.
+ */
+public interface OSRFSerializable {
+
+    /**
+     * Returns the object registry object for the implementing class.
+     */
+    public abstract OSRFRegistry getRegistry();
+
+    /**
+     * Returns the object found at the given field
+     */
+    public abstract Object get(String field);
+}
+
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/SettingsClient.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/SettingsClient.java
new file mode 100644 (file)
index 0000000..71e570a
--- /dev/null
@@ -0,0 +1,53 @@
+package org.opensrf.util;
+import org.opensrf.*;
+import java.util.Map;
+
+/**
+ * Connects to the OpenSRF Settings server to fetch the settings config.  
+ * Provides a Config interface for fetching settings via path
+ */
+public class SettingsClient extends Config {
+
+    /** Singleton SettingsClient instance */
+    private static SettingsClient client = new SettingsClient();
+
+    public SettingsClient() {
+        super("");
+    }
+
+    /**
+     * @return The global settings client instance 
+     */
+    public static SettingsClient instance() throws ConfigException {
+        if(client.getConfigObject() == null) 
+            client.fetchConfig();
+        return client;
+    }
+
+    /**
+     * Fetches the settings object from the settings server
+     */
+    private void fetchConfig() throws ConfigException {
+
+        ClientSession ses = new ClientSession("opensrf.settings");
+        try {
+
+            Request req = ses.request(
+                "opensrf.settings.host_config.get", 
+                new String[]{(String)Config.global().getFirst("/domain")});
+    
+            Result res = req.recv(12000);
+            if(res == null) {
+                /** throw exception */
+            }
+            setConfigObject((Map) res.getContent());
+
+        } catch(Exception e) {
+            throw new ConfigException("Error fetching settings config", e);
+
+        } finally {
+            ses.cleanup();
+        }
+    }
+}
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/Utils.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/Utils.java
new file mode 100644 (file)
index 0000000..159d254
--- /dev/null
@@ -0,0 +1,106 @@
+package org.opensrf.util;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * Collection of general, static utility methods
+ */
+public class Utils {
+
+    /**
+     * Returns the string representation of a given file.
+     * @param filename The file to turn into a string
+     */
+    public static String fileToString(String filename) 
+            throws FileNotFoundException, IOException {
+
+        StringBuffer sb = new StringBuffer();
+        BufferedReader in = new BufferedReader(new FileReader(filename));
+        String str;
+        while ((str = in.readLine()) != null) 
+            sb.append(str);
+        in.close();
+        return sb.toString();
+    }
+
+
+    /**
+     * Escapes a string.
+     */
+    public static String escape(String string) {
+        StringBuffer sb = new StringBuffer();
+        escape(string, sb);
+        return sb.toString();
+    }
+
+    /**
+     * Escapes a string.  Turns bare newlines into \n, etc.
+     * Escapes \n, \r, \t, ", \f
+     * Encodes non-ascii characters as UTF-8: \u0000
+     * @param string The string to escape
+     * @param sb The string buffer to write the escaped string into
+     */
+    public static void escape(String string, StringBuffer sb) {
+        int len = string.length();
+        String utf;
+        char c;
+        for( int i = 0; i < len; i++ ) {
+            c = string.charAt(i);
+            switch (c) {
+                case '\\':
+                    sb.append("\\\\");
+                    break;
+                case '"':
+                    sb.append("\\\"");
+                    break;
+                case '\b':
+                    sb.append("\\b");
+                    break;
+                case '\t':
+                    sb.append("\\t");
+                    break;
+                case '\n':
+                    sb.append("\\n");
+                    break;
+                case '\f':
+                    sb.append("\\f");
+                    break;
+                case '\r':
+                    sb.append("\\r");
+                    break;
+                default:
+                    if (c < 32 || c > 126 ) { 
+                        /* escape all non-ascii or control characters as UTF-8 */
+                        utf = "000" + Integer.toHexString(c);
+                        sb.append("\\u" + utf.substring(utf.length() - 4));
+                    } else {
+                        sb.append(c);
+                    }
+            }
+        }
+    }
+
+
+    /** 
+     * Descends into the map along the given XPATH-style path 
+     * and returns the object found there.
+     * @param path The XPATH-style path to search.  Path 
+     * components are separated by '/' characters.  
+     * Example:  /opensrf/loglevel
+     * @return The found object. 
+     */
+
+    public static Object findPath(Map map, String path) {
+        String keys[] = path.split("/", -1);
+        int i = 0;
+        if(path.charAt(0) == '/') i++;
+        for(; i < keys.length - 1; i++ ) 
+            map = (Map) map.get(keys[i]);
+
+        return map.get(keys[i]);
+    }
+}
+
+
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/XMLFlattener.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/XMLFlattener.java
new file mode 100644 (file)
index 0000000..7abefe0
--- /dev/null
@@ -0,0 +1,128 @@
+package org.opensrf.util;
+
+import javax.xml.stream.*;
+import javax.xml.stream.events.* ;
+import javax.xml.namespace.QName;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.ListIterator;
+import java.io.InputStream;
+import org.opensrf.util.JSONWriter;
+import org.opensrf.util.JSONReader;
+
+/**
+ * Flattens an XML file into a properties map.  Values are stored as JSON strings or arrays.
+ * An array is created if more than one value resides at the same key.
+ * e.g. html.head.script = "alert('hello');"
+ */
+public class XMLFlattener {
+
+    /** Flattened properties map */
+    private Map<String, String> props;
+    /** Incoming XML stream */
+    private InputStream inStream;
+    /** Runtime list of encountered elements */
+    private List<String> elementList;
+
+    /**
+     * Creates a new reader. Initializes the message queue.
+     * Sets the stream state to disconnected, and the xml
+     * state to in_nothing.
+     * @param inStream the inbound XML stream
+     */
+    public XMLFlattener(InputStream inStream) {
+        props = new HashMap<String, String>();
+        this.inStream = inStream;
+        elementList = new ArrayList<String>();
+    }
+
+    /** Turns an array of strings into a dot-separated key string */
+    private String listToString() {
+        ListIterator itr = elementList.listIterator();
+        StringBuffer sb = new StringBuffer();
+        while(itr.hasNext()) {
+            sb.append(itr.next());
+            if(itr.hasNext())
+                sb.append(".");
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Parses XML data from the provided stream.
+     */
+    public Map read() throws javax.xml.stream.XMLStreamException {
+
+        XMLInputFactory factory = XMLInputFactory.newInstance();
+
+        /** disable as many unused features as possible to speed up the parsing */
+        factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.TRUE);
+        factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE);
+        factory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE);
+        factory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.FALSE);
+        factory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
+
+        /** create the stream reader */
+        XMLStreamReader reader = factory.createXMLStreamReader(inStream);
+        int eventType;
+
+        while(reader.hasNext()) {
+            /** cycle through the XML events */
+
+            eventType = reader.next();
+            if(reader.isWhiteSpace()) continue;
+
+            switch(eventType) {
+
+                case XMLEvent.START_ELEMENT:
+                    elementList.add(reader.getName().toString());
+                    break;
+
+                case XMLEvent.CHARACTERS:
+                    String text = reader.getText();
+                    String key = listToString();
+
+                    if(props.containsKey(key)) {
+
+                        /* something in the map already has this key */
+
+                        Object o = null;
+                        try {
+                            o = new JSONReader(props.get(key)).read();
+                        } catch(org.opensrf.util.JSONException e){}
+
+                        if(o instanceof List) {
+                            /* if the map contains a list, append to the list and re-encode */
+                            ((List) o).add(text);
+
+                        } else {
+                            /* if the map just contains a string, start building a new list
+                             * with the old string and append the new string */
+                            List<String> arr = new ArrayList<String>();
+                            arr.add((String) o);
+                            arr.add(text);
+                            o = arr;
+                        }
+
+                        props.put(key, new JSONWriter(o).write());
+
+                    } else {
+                        props.put(key, new JSONWriter(text).write());
+                    }
+                    break;
+
+                case XMLEvent.END_ELEMENT: 
+                    elementList.remove(elementList.size()-1);
+                    break;
+            }
+        }
+
+        return props;
+    }
+}
+
+
+
+
diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/XMLTransformer.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/XMLTransformer.java
new file mode 100644 (file)
index 0000000..f8bc0d3
--- /dev/null
@@ -0,0 +1,59 @@
+package org.opensrf.util;
+import javax.xml.transform.*;
+import javax.xml.transform.stream.*;
+import javax.xml.parsers.*;
+import java.io.File;
+import java.io.ByteArrayInputStream;
+import java.io.OutputStream;
+import java.io.ByteArrayOutputStream;
+
+
+/**
+ * Performs XSL transformations.  
+ * TODO: Add ability to pass in XSL variables
+ */
+public class XMLTransformer {
+
+    /** The XML to transform */
+    private Source xmlSource;
+    /** The stylesheet to apply */
+    private Source xslSource;
+
+    public XMLTransformer(Source xmlSource, Source xslSource) {
+        this.xmlSource = xmlSource;
+        this.xslSource = xslSource;
+    }
+
+    public XMLTransformer(String xmlString, File xslFile) {
+        this(
+            new StreamSource(new ByteArrayInputStream(xmlString.getBytes())),
+            new StreamSource(xslFile));
+    }
+
+    public XMLTransformer(File xmlFile, File xslFile) {
+        this(
+            new StreamSource(xmlFile),
+            new StreamSource(xslFile));
+    }
+
+    /** 
+     * Applies the transformation and puts the result into the provided output stream
+     */
+    public void apply(OutputStream outStream) throws TransformerException, TransformerConfigurationException {
+        Result result = new StreamResult(outStream);
+        Transformer trans = TransformerFactory.newInstance().newTransformer(xslSource);
+        trans.transform(xmlSource, result);
+    }
+
+    /**
+     * Applies the transformation and return the resulting string
+     * @return The String created by the XSL transformation
+     */
+    public String apply() throws TransformerException, TransformerConfigurationException {
+        OutputStream outStream = new ByteArrayOutputStream();
+        this.apply(outStream);
+        return outStream.toString();
+    }
+}
+
+