com.digi.wva.test_auxiliary.SimulatedEventChannel.java Source code

Java tutorial

Introduction

Here is the source code for com.digi.wva.test_auxiliary.SimulatedEventChannel.java

Source

/*
 * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of
 * the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Copyright (c) 2014 Digi International Inc., All Rights Reserved.
 */

package com.digi.wva.test_auxiliary;

import android.util.Log;

import com.digi.wva.async.AlarmType;
import com.digi.wva.async.EventFactory;
import com.digi.wva.async.FaultCodeCommon;
import com.digi.wva.internal.VehicleData;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Provides a simulated WVA event channel to connect to.
 *
 * <p>
 *     Use {@link #getPort()} to determine the port to connect on,
 *     {@link #start()} to run the loop (in an automatically-created background thread),
 *     methods like {@link #sendVehicleData(String, double)} to send data through,
 *     {@link #triggerRemoteClose()} to close the socket, and
 *     {@link #shutdown()} to completely stop the event channel.
 * </p>
 */
public class SimulatedEventChannel {
    private static final String TAG = "SimulatedEventChannel";
    private final ServerSocket socket;
    private Socket client;
    private BlockingQueue<JSONObject> outputQueue = new ArrayBlockingQueue<JSONObject>(100, true);
    private boolean runLoop = true;
    private Thread worker;

    private final Object clientLock = new Object();

    AtomicInteger alarmSequence = new AtomicInteger(), dataSequence = new AtomicInteger();
    DateTimeFormatter formatter = ISODateTimeFormat.dateTimeNoMillis();

    /**
     * Constructor.
     * @throws IOException if an error occurs while creating the socket
     */
    public SimulatedEventChannel() throws IOException {
        // Create a new server socket, on a random port, on localhost
        socket = new ServerSocket(0, 0, InetAddress.getByName("127.0.0.1"));
        // Allow the port to be reused
        socket.setReuseAddress(true);
    }

    /**
     * Gets the port on which this event channel is being served.
     * @return the port to connect to
     */
    public int getPort() {
        return socket.getLocalPort();
    }

    private void mainloop() {
        PrintWriter writer;

        while (runLoop) {
            try {
                if (socket.isClosed()) {
                    // SimulatedEventChannel has been shut down.
                    return;
                }

                // Accept a client connection.
                synchronized (clientLock) {
                    client = socket.accept();
                    writer = new PrintWriter(client.getOutputStream(), true);

                    clientLock.notifyAll();
                }

                while (runLoop) {
                    JSONObject next = outputQueue.take();

                    Log.v(TAG, "Writing into event channel: " + next.toString());
                    writer.println(next.toString());
                }
            } catch (IOException e) {
                Log.w(TAG, "IOException in main loop");
                e.printStackTrace();
            } catch (InterruptedException e) {
                // Thread is interrupted by shutdown()
                runLoop = false;
            }
        }
    }

    /**
     * Commence the event channel's operations.
     */
    public void start() {
        worker = new Thread(new Runnable() {
            @Override
            public void run() {
                mainloop();
            }
        });
        worker.start();
    }

    /**
     * Permanently stop this event channel's operations.
     */
    public void shutdown() {
        if (client != null) {
            try {
                client.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        try {
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        worker.interrupt();
    }

    public boolean isShutDown() {
        return worker.isInterrupted() && socket.isClosed();
    }

    /**
     * Construct a template event channel JSON object, containing a timestamp and
     * with the top-level key corresponding to the event type ("data" or "alarm").
     * @param type the event type to use
     * @return a template JSON object
     * @throws JSONException if there is an error in building this JSON
     */
    private JSONObject buildStub(EventFactory.Type type) throws JSONException {
        JSONObject stub = new JSONObject();
        String timestamp = formatter.print(DateTime.now(DateTimeZone.UTC));

        // Add inner object
        stub.put(type == EventFactory.Type.SUBSCRIPTION ? "data" : "alarm",
                new JSONObject().put("timestamp", timestamp));

        return stub;
    }

    /**
     * Used to generate vehicle data events.
     * @param eventType alarm or subscription
     * @param alarmType the alarm type. Leave null if using subscription
     * @param endpoint the endpoint name
     * @param value the value
     * @return a JSON object for the event channel
     * @throws JSONException if there's an error building the JSON
     */
    private JSONObject buildVehicleData(EventFactory.Type eventType, AlarmType alarmType, String endpoint,
            double value) throws JSONException {
        JSONObject stub = buildStub(eventType);
        JSONObject inner = stub.getJSONObject(eventType == EventFactory.Type.SUBSCRIPTION ? "data" : "alarm");

        // Make deep JSON object, copy timestamp
        JSONObject inner2 = new JSONObject().put("timestamp", inner.getString("timestamp"));
        inner2.put("value", value);
        // Place the deep object inside the inner one
        inner.put(endpoint, inner2);

        // Build out inner object
        inner.put("uri", "vehicle/data/" + endpoint);
        switch (eventType) {
        case ALARM:
            inner.put("short_name", endpoint + "~" + AlarmType.makeString(alarmType));
            inner.put("sequence", alarmSequence.incrementAndGet());
            break;
        case SUBSCRIPTION:
            inner.put("short_name", endpoint + VehicleData.SUB_SUFFIX);
            inner.put("sequence", dataSequence.incrementAndGet());
            break;
        }

        return stub;
    }

    /**
     * Used to generate fault code events.
     * @param eventType alarm or subscription
     * @param bus the CAN bus
     * @param type the message type
     * @param ecu the ECU name
     * @param value the value
     * @return a JSON object for the event channel
     * @throws JSONException if there's an error building the JSON
     */
    private JSONObject buildFaultCode(EventFactory.Type eventType, FaultCodeCommon.Bus bus,
            FaultCodeCommon.FaultCodeType type, String ecu, String value) throws JSONException {
        JSONObject stub = buildStub(eventType);
        JSONObject inner = stub.getJSONObject(eventType == EventFactory.Type.SUBSCRIPTION ? "data" : "alarm");

        // Make deep JSON object, copy timestamp
        JSONObject inner2 = new JSONObject().put("timestamp", inner.getString("timestamp"));
        inner2.put("value", value);
        // Place the deep object inside the inner one
        inner.put(ecu, inner2);

        // Build out inner object
        String ecuPath = FaultCodeCommon.createEcuPath(bus, type, ecu);
        inner.put("uri", FaultCodeCommon.createUri(ecuPath));
        String shortnameBase = ecuPath.replace('/', '~');
        switch (eventType) {
        case ALARM:
            inner.put("short_name", shortnameBase + "~change");
            inner.put("sequence", alarmSequence.incrementAndGet());
            break;
        case SUBSCRIPTION:
            inner.put("short_name", shortnameBase + "~dtcsub");
            inner.put("sequence", dataSequence.incrementAndGet());
            break;
        }

        return stub;
    }

    /**
     * Enqueue a new fault code subscription event to be sent through the event channel.
     * @param bus the CAN bus
     * @param type the message type
     * @param ecu the ECU name
     * @param value the fault code value
     * @throws JSONException if there is an error in building the JSON
     */
    public void sendFaultCodeData(FaultCodeCommon.Bus bus, FaultCodeCommon.FaultCodeType type, String ecu,
            String value) throws JSONException {
        outputQueue.add(buildFaultCode(EventFactory.Type.SUBSCRIPTION, bus, type, ecu, value));
    }

    /**
     * Enqueue a new fault code alarm event to be sent through the event channel.
     * @param bus the CAN bus
     * @param type the message type
     * @param ecu the ECU name
     * @param value the fault code value
     * @throws JSONException if there is an error in building the JSON
     */
    public void sendFaultCodeAlarm(FaultCodeCommon.Bus bus, FaultCodeCommon.FaultCodeType type, String ecu,
            String value) throws JSONException {
        outputQueue.add(buildFaultCode(EventFactory.Type.ALARM, bus, type, ecu, value));
    }

    /**
     * Enqueue a new vehicle data subscription event to be sent through the event channel.
     * @param endpoint the endpoint name
     * @param value the value
     * @throws JSONException if there is an error in building the JSON
     */
    public void sendVehicleData(String endpoint, double value) throws JSONException {
        outputQueue.add(buildVehicleData(EventFactory.Type.SUBSCRIPTION, null, endpoint, value));
    }

    /**
     * Enqueue a new vehicle data alarm event to be sent through the event channel.
     * @param type the alarm type to use
     * @param endpoint the endpoint name
     * @param value the value
     * @throws JSONException if there is an error in building the JSON
     */
    public void sendVehicleDataAlarm(AlarmType type, String endpoint, double value) throws JSONException {
        outputQueue.add(buildVehicleData(EventFactory.Type.ALARM, type, endpoint, value));
    }

    /**
     * Close the currently-connected 'client' socket, if any.
     */
    public void triggerRemoteClose() {
        synchronized (clientLock) {
            if (client == null) {
                try {
                    // If the test code is calling triggerRemoteClose, it should be because we expect
                    // there to already be a connection. Let's wait for up to 5 seconds for that to
                    // happen.
                    clientLock.wait(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if (client == null) {
                Log.d(TAG, "triggerRemoteClose called without client socket");
            } else {
                try {
                    client.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

                client = null;
            }
        }
    }
}