Repository: Vantiq/vantiq-extension-sources (language: python)

Issue:
Extjsdk: Handle connectors that may have multiple configs
Some types of connectors may want to connect to multiple Vantiqs.  Extend Utils class to handle that.


Discussion / hints:

Fix patch:
diff --git a/extjsdk/src/main/java/io/vantiq/extjsdk/ExtensionWebSocketClient.java b/extjsdk/src/main/java/io/vantiq/extjsdk/ExtensionWebSocketClient.java
index fcd274dc..266b6205 100644
--- a/extjsdk/src/main/java/io/vantiq/extjsdk/ExtensionWebSocketClient.java
+++ b/extjsdk/src/main/java/io/vantiq/extjsdk/ExtensionWebSocketClient.java
@@ -14,11 +14,7 @@
 
 // For decoding of the messages received
 import com.fasterxml.jackson.databind.ObjectMapper;
-
-// WebSocket imports
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.WebSocket;
+import com.google.common.collect.EvictingQueue;
 
 import java.io.IOException;
 import java.net.ServerSocket;
@@ -29,17 +25,19 @@
 import java.util.concurrent.*;
 import java.util.Queue;
 import java.util.UUID;
-
-import com.google.common.collect.EvictingQueue;
-
-// Logging
+// WebSocket imports
 import okio.ByteString;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.WebSocket;
+// Logging
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
  * A client that handles the WebSocket connection with a Vantiq deployment, for the purposes of Extension sources.
  */
+@SuppressWarnings("PMD.TooManyFields")
 public class ExtensionWebSocketClient {
     
     /**
@@ -178,7 +176,16 @@ public String getSourceName() {
      * @param sourceName    The name of the source to which this client will be connected
      */
     public ExtensionWebSocketClient (String sourceName) {
-        this(sourceName, DEFAULT_FAILED_MESSAGE_QUEUE_SIZE);
+        this(sourceName, DEFAULT_FAILED_MESSAGE_QUEUE_SIZE, null);
+    }
+    
+    /**
+     * Creates an {@link ExtensionWebSocketClient} that will connect to the source {@code sourceName}.
+     * @param sourceName    The name of the source to which this client will be connected
+     * @param utility       Utils Utils instance used for this client.
+     */
+    public ExtensionWebSocketClient (String sourceName, InstanceConfigUtils utility) {
+        this(sourceName, DEFAULT_FAILED_MESSAGE_QUEUE_SIZE, utility);
     }
 
     /**
@@ -186,21 +193,40 @@ public ExtensionWebSocketClient (String sourceName) {
      * shared secret allows instances of this client to perform reconnects to the server.
      */
     protected static final String clientReconnectSecret = UUID.randomUUID().toString();
-
+    
+    /**
+     * Utility object used to collect & store config parameters
+     */
+    protected InstanceConfigUtils utils = null;
+    
     /**
      * Creates an {@link ExtensionWebSocketClient} that will connect to the source {@code sourceName}.
      * @param sourceName                The name of the source to which this client will be connected
      * @param failedMessageQueueSize    The max size of the failedMessageQueue
      */
     public ExtensionWebSocketClient (String sourceName, int failedMessageQueueSize) {
+        this(sourceName, failedMessageQueueSize, null);
+    }
+        
+        /**
+         * Creates an {@link ExtensionWebSocketClient} that will connect to the source {@code sourceName}.
+         * @param sourceName                The name of the source to which this client will be connected
+         * @param failedMessageQueueSize    The max size of the failedMessageQueue
+         * @param utility                   Utils Utils instance to be used for this client
+         */
+    public ExtensionWebSocketClient (String sourceName, int failedMessageQueueSize, InstanceConfigUtils utility) {
         this.sourceName = sourceName;
         outstandingNotifications = new Semaphore(5, true);
         log = LoggerFactory.getLogger(this.getClass().getCanonicalName() + "#" + sourceName);
+        if (utility == null) {
+            utility = Utils.getInstanceUtilsConfigInstance();
+            utils = utility;
+        }
         listener = new ExtensionWebSocketListener(this);
 
         // Check for Environment Variable to overwrite failedMessageQueue size, otherwise use default
         if (System.getenv(FAILED_MESAGE_QUEUE_SIZE) != null) {
-            int customQueueSize = new Integer(System.getenv(FAILED_MESAGE_QUEUE_SIZE));
+            int customQueueSize = Integer.parseInt(System.getenv(FAILED_MESAGE_QUEUE_SIZE));
             failedMessageQueue = EvictingQueue.create(customQueueSize);
         } else {
             failedMessageQueue = EvictingQueue.create(failedMessageQueueSize);
@@ -218,7 +244,7 @@ public synchronized void declareHealthy() {
             return;
         }
 
-        Integer port = Utils.obtainTCPProbePort();
+        Integer port = utils.obtainTCPProbePort();
         if (port == null) {
             port = DEFAULT_TCP_PROBE_PORT;
         }
@@ -270,9 +296,9 @@ public boolean isMarkedHealthy() {
         ServerSocket localLivenessSocket = livenessSocket;
         return localProbeFuture != null && localLivenessSocket != null && !localLivenessSocket.isClosed();
     }
-
+    
     /**
-     * Attempts to connect to the source using the given target url and authentication token. If the connection fails, 
+     * Attempts to connect to the source using the given target url and authentication token. If the connection fails,
      * {@link #isOpen}, {@link #isAuthed}, and {@link #isConnected} can be used to identify if the connection failed
      * or succeeded at the WebSocket, authentication attempt, and source, respectively. This function may be called
      * again regardless of where the failure occurred.
@@ -287,16 +313,47 @@ public CompletableFuture<Boolean> initiateFullConnection(String url, String toke
         authenticate(token);
         return connectToSource();
     }
-
+    
+    /**
+     * Attempts to connect to the source using the given target url and authentication token. If the connection fails,
+     * {@link #isOpen}, {@link #isAuthed}, and {@link #isConnected} can be used to identify if the connection failed
+     * or succeeded at the WebSocket, authentication attempt, and source, respectively. This function may be called
+     * again regardless of where the failure occurred.
+     *
+     * @param url   The url of the target Vantiq server.
+     * @param token The authentication token for the target namespace.
+     * @param sendPings boolean indicating whether to send ping messages
+     * @return      An {@link CompletableFuture} that completes as {@code true} when the connection to the source is
+     *              fully completed, or {@code false} when the connection fails at any point along the way.
+     */
+    public CompletableFuture<Boolean> initiateFullConnection(String url, String token, boolean sendPings) {
+        initiateWebsocketConnection(url, sendPings);
+        authenticate(token);
+        return connectToSource();
+    }
+    
+    /**
+     * Creates a WebSocket connection to the given URL. Does nothing if a connection has already been established
+     *
+     * @param url   The url of the Vantiq system to which you wish to connect.
+     *              Typically "wss://dev.vantiq.com/api/v1/wsock/websocket"
+     * @return      A {@link CompletableFuture} that will return true when the connection succeeds, or false
+     *              WebSocket fails to connect.
+     */
+    public synchronized CompletableFuture<Boolean> initiateWebsocketConnection(String url) {
+        boolean sendPings = utils.obtainSendPingStatus();
+        return initiateWebsocketConnection(url, sendPings);
+    }
     /**
      * Creates a WebSocket connection to the given URL. Does nothing if a connection has already been established
      *
      * @param url   The url of the Vantiq system to which you wish to connect.
      *              Typically "wss://dev.vantiq.com/api/v1/wsock/websocket"
+     * @param sendPings boolean indicating whether to send pings.
      * @return      A {@link CompletableFuture} that will return true when the connection succeeds, or false
      *              WebSocket fails to connect.
      */
-    synchronized public CompletableFuture<Boolean> initiateWebsocketConnection(String url) {
+    public synchronized CompletableFuture<Boolean> initiateWebsocketConnection(String url, boolean sendPings) {
         // Only create the webSocketFuture if the websocket connection has completed or it has failed
         if (webSocket == null || !webSocketFuture.getNow(true)) {
             webSocketFuture = new CompletableFuture<>();
@@ -305,8 +362,6 @@ synchronized public CompletableFuture<Boolean> initiateWebsocketConnection(Strin
             OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder()
                     .readTimeout(0, TimeUnit.MILLISECONDS)
                     .writeTimeout(0, TimeUnit.MILLISECONDS);
-
-            boolean sendPings = Utils.obtainSendPingStatus();
             if (sendPings) {
                 clientBuilder.pingInterval(5000, TimeUnit.MILLISECONDS);
             }
@@ -576,7 +631,7 @@ protected void doAuthentication() {
      * @return      A {@link CompletableFuture} that will return true when the authentication succeeds, or false
      *              when the WebSocket connection fails before authentication can occur.
      */
-    synchronized public CompletableFuture<Boolean> authenticate(String user, String pass) {
+    public synchronized CompletableFuture<Boolean> authenticate(String user, String pass) {
         Map<String, String> authData = new LinkedHashMap<>();
         authData.put("username", user);
         authData.put("password", pass);
@@ -610,7 +665,7 @@ synchronized public CompletableFuture<Boolean> authenticate(String user, String
      * @return      A {@link CompletableFuture} that will return true when the authentication succeeds, or false
      *              when the WebSocket connection fails before authentication can occur.
      */
-    synchronized public CompletableFuture<Boolean> authenticate(String token) {
+    public synchronized CompletableFuture<Boolean> authenticate(String token) {
         authData = token;
         // Only create the authFuture if there has been no request or a failed request, and a websocket request has 
         // been made
@@ -665,7 +720,7 @@ protected void doConnectionToSource() {
      */
     // Send a connection request for the source
     // Note that this client MUST already be authenticated or else the message will be ignored
-    synchronized public CompletableFuture<Boolean> connectToSource() {
+    public synchronized CompletableFuture<Boolean> connectToSource() {
         // Only create the authFuture if there has been no request or a failed request, and a websocket request has 
         // been made
         if (authFuture != null && (sourceFuture == null || !sourceFuture.getNow(true))) {
diff --git a/extjsdk/src/main/java/io/vantiq/extjsdk/InstanceConfigUtils.java b/extjsdk/src/main/java/io/vantiq/extjsdk/InstanceConfigUtils.java
new file mode 100644
index 00000000..ea22392f
--- /dev/null
+++ b/extjsdk/src/main/java/io/vantiq/extjsdk/InstanceConfigUtils.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extjsdk;
+
+import static io.vantiq.extjsdk.Utils.AUTH_TOKEN_PROPERTY_NAME;
+import static io.vantiq.extjsdk.Utils.PORT_PROPERTY_NAME;
+import static io.vantiq.extjsdk.Utils.SECRET_CREDENTIALS;
+import static io.vantiq.extjsdk.Utils.SEND_PING_PROPERTY_NAME;
+import static io.vantiq.extjsdk.Utils.SERVER_CONFIG_DIR;
+import static io.vantiq.extjsdk.Utils.SERVER_CONFIG_FILENAME;
+import static io.vantiq.extjsdk.Utils.SOURCES_PROPERTY_NAME;
+import static io.vantiq.extjsdk.Utils.TARGET_SERVER_PROPERTY_NAME;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Properties;
+
+public class InstanceConfigUtils {
+    public static final String CONFIG_WAS_PROGRAMMATIC = "configWasProgrammatic";
+
+    // The properties object containing the data from the server configuration file
+    private Properties serverConfigProperties;
+    
+    /**
+     * Provide server config properties programmatically.
+     *
+     * Used when the configuration properties necessary to connect to Vantiq are provided via a means outside
+     * the reading of the server.config file.  This also sets a property telling this class that the information
+     * was provided programmatically so that subsequent calls to obtainServerConfig will not overwrite things from
+     * the server.config file or generate an error when that file doesn't exist.
+     *
+     * If this is called repeatedly, each will override the last.
+     *
+     * @param targetServer  String URL for the Vantiq server
+     * @param authToken     String indicating the access token to use to connect to the server at targetServer
+*    * @param sources       String containing a comma separated list of source names to use.
+     * @param sendPings     Boolean indicating whether the client connection should ping the Vantiq server to maintain
+     *                      the connection.
+     * @param tcpProbePort  Integer providing a port to open for TCP probes when running in a Kubernetes environment.
+     *
+     * @return              Properties the server config properties as provided.
+     */
+    public Properties provideServerConfig(String targetServer, String authToken, String sources,
+                                                 Boolean sendPings, Integer tcpProbePort) {
+        Properties localSCP = new Properties();
+        if (StringUtils.isAnyEmpty(targetServer, authToken, sources)) {
+            throw new IllegalArgumentException("The targetServer, authToken, and sources parameters " +
+                    "must be non-null & non-empty.");
+        }
+        localSCP.put(TARGET_SERVER_PROPERTY_NAME, targetServer);
+        localSCP.put(AUTH_TOKEN_PROPERTY_NAME, authToken);
+        localSCP.put(SOURCES_PROPERTY_NAME, sources);
+        localSCP.put(SEND_PING_PROPERTY_NAME, (sendPings == null ? Boolean.FALSE.toString() : sendPings));
+        if (tcpProbePort != null) {
+            localSCP.put(PORT_PROPERTY_NAME, tcpProbePort.toString());
+        }
+        localSCP.put(CONFIG_WAS_PROGRAMMATIC, true);
+        
+        synchronized (this) {
+            serverConfigProperties = localSCP;
+        }
+        return serverConfigProperties;
+    }
+
+    public Properties obtainServerConfig() {
+        return obtainServerConfig(SERVER_CONFIG_FILENAME);
+    }
+    
+    /**
+     * Turn the given configuration file into a {@link Properties} object.
+     *
+     * @param fileName  The name of the configuration file holding the server configuration.
+     * @return          The properties specified in the file.
+     */
+    public Properties obtainServerConfig(String fileName) {
+        synchronized (this) {
+            boolean wasProgrammatic = false;
+            if (serverConfigProperties != null) {
+                if (serverConfigProperties.get(CONFIG_WAS_PROGRAMMATIC) instanceof Boolean) {
+                    wasProgrammatic = (Boolean) serverConfigProperties.get(CONFIG_WAS_PROGRAMMATIC);
+                }
+            }
+            // If config props were provided programmatically, don't override them.  Simply return
+            // what's already there.
+            if (wasProgrammatic) {
+                return serverConfigProperties;
+            }
+            
+            // Otherwse, re-read the file
+            File configFile = new File(SERVER_CONFIG_DIR, fileName);
+            serverConfigProperties = new Properties();
+
+            try {
+                if (!configFile.exists()) {
+                    configFile = new File(fileName);
+                }
+                serverConfigProperties.load(Files.newInputStream(configFile.toPath()));
+
+                // Next we check for the existence of an environment variable containing a secret reference to the authToken
+                // We only set it if the value is not empty and if the authToken wasn't already specified in the
+                // server.config
+                String secretAuthToken = System.getenv(SECRET_CREDENTIALS);
+                if (secretAuthToken != null && !StringUtils.isBlank(secretAuthToken)
+                        && serverConfigProperties.getProperty(AUTH_TOKEN_PROPERTY_NAME) == null) {
+                    serverConfigProperties.setProperty(AUTH_TOKEN_PROPERTY_NAME, secretAuthToken);
+                }
+            } catch (IOException e) {
+                throw new RuntimeException("Could not find valid server configuration file. Expected location: '"
+                        + configFile.getAbsolutePath() + "'", e);
+            } catch (Exception e) {
+                throw new RuntimeException("Error occurred when trying to read the server configuration file. "
+                        + "Please ensure it is formatted properly.", e);
+            }
+
+            return serverConfigProperties;
+        }
+    }
+
+    /**
+     * Helper method used to get the TCP Probe Port if specified in the server.config
+     *
+     * @return An Integer for the port value provided in the server.config file, or null if non was specified.
+     */
+    public Integer obtainTCPProbePort() {
+        Properties localServerConfigProps;
+
+        // Get a local copy of the props while synchronized
+        synchronized (this) {
+            localServerConfigProps = serverConfigProperties;
+        }
+
+        if (localServerConfigProps != null) {
+            // Grab the property and return result
+            String portString = localServerConfigProps.getProperty(PORT_PROPERTY_NAME);
+            if (portString != null) {
+                return Integer.valueOf(portString);
+            }
+        } else {
+            throw new RuntimeException("Error occurred when checking for the tcpProbePort property. The " +
+                    "server.config properties have not yet been captured. Before checking for specific properties, " +
+                    "the 'obtainServerConfig' method must first be called.");
+        }
+
+        return null;
+    }
+
+    /**
+     * Helper method used to get the sendPings property if specified in the server.config
+     *
+     * @return The boolean value for the sendPings property, or false if it wasn't specified
+     */
+    public boolean obtainSendPingStatus() {
+        Properties localServerConfigProps;
+
+        // Get a local copy of the props while synchronized
+        synchronized (this) {
+            localServerConfigProps = serverConfigProperties;
+        }
+
+        if (localServerConfigProps != null) {
+            String sendPingString = localServerConfigProps.getProperty(SEND_PING_PROPERTY_NAME);
+            if (sendPingString != null) {
+                return Boolean.parseBoolean(sendPingString);
+            }
+        } else {
+            throw new RuntimeException("Error occurred when checking for the sendPings property. The server.config " +
+                    "properties have not yet been captured. Before checking for specific properties, the " +
+                    "'obtainServerConfig' method must first be called.");
+        }
+
+        return false;
+    }
+
+    /**
+     * Method used to clear the local copy of server.config properties
+     */
+    public void clearServerConfigProperties() {
+        synchronized (this) {
+            serverConfigProperties = null;
+        }
+    }
+}
diff --git a/extjsdk/src/main/java/io/vantiq/extjsdk/Utils.java b/extjsdk/src/main/java/io/vantiq/extjsdk/Utils.java
index f110f630..e78345ef 100644
--- a/extjsdk/src/main/java/io/vantiq/extjsdk/Utils.java
+++ b/extjsdk/src/main/java/io/vantiq/extjsdk/Utils.java
@@ -1,30 +1,46 @@
 package io.vantiq.extjsdk;
 
-import org.apache.commons.lang3.StringUtils;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
 import java.util.Properties;
 
-public class Utils {
-
-    // String used by methods to synch on
-    private static final String SYNCH_LOCK = "synchLockString";
+/**
+ * Maintains a static configuration instance.  This class is used when the configuration
+ * details are shared by all sources in the connector.
+ *
+ * Makes use of the InstanceConfigUtils class to manage the static instance.
+ */
 
+public class Utils {
+    
+    private static final String SYNCH_LOCK = "UtilsSyncLock";
     public static final String SEND_PING_PROPERTY_NAME = "sendPings";
     public static final String PORT_PROPERTY_NAME = "tcpProbePort";
     public static final String SERVER_CONFIG_DIR = "serverConfig";
     public static final String SERVER_CONFIG_FILENAME = "server.config";
     public static final String SECRET_CREDENTIALS = "CONNECTOR_AUTH_TOKEN";
-
-    // The properties object containing the data from the server configuration file
-    private static Properties serverConfigProperties;
+    public static final String TARGET_SERVER_PROPERTY_NAME = "targetServer";
+    public static final String AUTH_TOKEN_PROPERTY_NAME = "authToken";
+    public static final String SOURCES_PROPERTY_NAME = "sources";
+    
+    private static InstanceConfigUtils staticInstance = null;
+    
+    private static void ensureStaticInstance() {
+        synchronized (SYNCH_LOCK) {
+            if (staticInstance == null) {
+                staticInstance = new InstanceConfigUtils();
+            }
+        }
+    }
+    
+    public static InstanceConfigUtils getInstanceUtilsConfigInstance() {
+        ensureStaticInstance();
+        return staticInstance;
+    }
 
     public static Properties obtainServerConfig() {
-        return obtainServerConfig(SERVER_CONFIG_FILENAME);
+        ensureStaticInstance();
+        return staticInstance.obtainServerConfig(SERVER_CONFIG_FILENAME);
     }
-
+    
     /**
      * Turn the given configuration file into a {@link Properties} object.
      *
@@ -32,34 +48,8 @@ public static Properties obtainServerConfig() {
      * @return          The properties specified in the file.
      */
     public static Properties obtainServerConfig(String fileName) {
-        synchronized (SYNCH_LOCK) {
-            File configFile = new File(SERVER_CONFIG_DIR, fileName);
-            serverConfigProperties = new Properties();
-
-            try {
-                if (!configFile.exists()) {
-                    configFile = new File(fileName);
-                }
-                serverConfigProperties.load(Files.newInputStream(configFile.toPath()));
-
-                // Next we check for the existence of an environment variable containing a secret reference to the authToken
-                // We only set it if the value is not empty and if the authToken wasn't already specified in the
-                // server.config
-                String secretAuthToken = System.getenv(SECRET_CREDENTIALS);
-                if (secretAuthToken != null && !StringUtils.isBlank(secretAuthToken)
-                        && serverConfigProperties.getProperty("authToken") == null) {
-                    serverConfigProperties.setProperty("authToken", secretAuthToken);
-                }
-            } catch (IOException e) {
-                throw new RuntimeException("Could not find valid server configuration file. Expected location: '"
-                        + configFile.getAbsolutePath() + "'", e);
-            } catch (Exception e) {
-                throw new RuntimeException("Error occurred when trying to read the server configuration file. "
-                        + "Please ensure it is formatted properly.", e);
-            }
-
-            return serverConfigProperties;
-        }
+        ensureStaticInstance();
+        return staticInstance.obtainServerConfig(fileName);
     }
 
     /**
@@ -68,26 +58,8 @@ public static Properties obtainServerConfig(String fileName) {
      * @return An Integer for the port value provided in the server.config file, or null if non was specified.
      */
     public static Integer obtainTCPProbePort() {
-        Properties localServerConfigProps;
-
-        // Get a local copy of the props while synchronized
-        synchronized (SYNCH_LOCK) {
-            localServerConfigProps = serverConfigProperties;
-        }
-
-        if (localServerConfigProps != null) {
-            // Grab the property and return result
-            String portString = localServerConfigProps.getProperty(PORT_PROPERTY_NAME);
-            if (portString != null) {
-                return Integer.valueOf(portString);
-            }
-        } else {
-            throw new RuntimeException("Error occurred when checking for the tcpProbePort property. The " +
-                    "server.config properties have not yet been captured. Before checking for specific properties, " +
-                    "the 'obtainServerConfig' method must first be called.");
-        }
-
-        return null;
+        ensureStaticInstance();
+        return staticInstance.obtainTCPProbePort();
     }
 
     /**
@@ -96,33 +68,15 @@ public static Integer obtainTCPProbePort() {
      * @return The boolean value for the sendPings property, or false if it wasn't specified
      */
     public static boolean obtainSendPingStatus() {
-        Properties localServerConfigProps;
-
-        // Get a local copy of the props while synchronized
-        synchronized (SYNCH_LOCK) {
-            localServerConfigProps = serverConfigProperties;
-        }
-
-        if (localServerConfigProps != null) {
-            String sendPingString = localServerConfigProps.getProperty(SEND_PING_PROPERTY_NAME);
-            if (sendPingString != null) {
-                return Boolean.parseBoolean(sendPingString);
-            }
-        } else {
-            throw new RuntimeException("Error occurred when checking for the sendPings property. The server.config " +
-                    "properties have not yet been captured. Before checking for specific properties, the " +
-                    "'obtainServerConfig' method must first be called.");
-        }
-
-        return false;
+        ensureStaticInstance();
+        return staticInstance.obtainSendPingStatus();
     }
 
     /**
      * Method used to clear the local copy of server.config properties
      */
     public static void clearServerConfigProperties() {
-        synchronized (SYNCH_LOCK) {
-            serverConfigProperties = null;
-        }
+        ensureStaticInstance();
+        staticInstance.clearServerConfigProperties();
     }
 }


Test patch:
diff --git a/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/ObjRecTestBase.java b/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/ObjRecTestBase.java
index 969a8e7c..98dee011 100644
--- a/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/ObjRecTestBase.java
+++ b/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/ObjRecTestBase.java
@@ -44,8 +44,8 @@ public class ObjRecTestBase {
 //    public static final String IP_CAMERA_URL = "https://wzmedia.dot.ca.gov/D4/S680_at_N_Main_St.stream/playlist.m3u8";
     // Walnut Creek/North Main camera (above) currently malfunctioning.  Swapping to Hwy 242 Junction for now.
     // Keeping old around to swap back sometime.
-    public static final String IP_CAMERA_URL = "https://wzmedia.dot.ca.gov/D4/N680_JSO_JCT_242.stream/playlist.m3u8";
-
+//    public static final String IP_CAMERA_URL = "https://wzmedia.dot.ca.gov/D4/N680_JSO_JCT_242.stream/playlist.m3u8";
+    public static final String IP_CAMERA_URL = "https://wzmedia.dot.ca.gov/D3/80_reed.stream/playlist.m3u8";
     @BeforeClass
     public static void getProps() {
         testAuthToken = System.getProperty("TestAuthToken", null);