From 9c82481eb52c16ccdefb37a1ef2bbffe0acfd6bb Mon Sep 17 00:00:00 2001
From: Stefan Kalscheuer <stefan@stklcode.de>
Date: Wed, 2 Sep 2020 15:47:41 +0200
Subject: [PATCH] introduce custom exception class instead of RuntimeExceptions
 (#10)

We now use a custom, checked exceptions on errors that can occur
with the API communication or configuration. instead of throwing an
unchecked IllegalStateException.
---
 CHANGELOG.md                                  |   1 +
 .../de/stklcode/pubtrans/ura/UraClient.java   | 129 ++++++++++++------
 .../UraClientConfigurationException.java      |  37 +++++
 .../ura/exception/UraClientException.java     |  39 ++++++
 src/main/java/module-info.java                |   1 +
 .../stklcode/pubtrans/ura/UraClientTest.java  |  23 ++--
 6 files changed, 175 insertions(+), 55 deletions(-)
 create mode 100644 src/main/java/de/stklcode/pubtrans/ura/exception/UraClientConfigurationException.java
 create mode 100644 src/main/java/de/stklcode/pubtrans/ura/exception/UraClientException.java

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3691156..befca39 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
 ### Changes
 * Using native Java 11 HTTP client
 * Client configuration with separate `UraClientConfiguration` class and builder
+* Client throws custom checked exception `UraClientException` instead of runtime exceptions on errors (#10)
 
 
 ## 1.3.0 - 2019-12-04
diff --git a/src/main/java/de/stklcode/pubtrans/ura/UraClient.java b/src/main/java/de/stklcode/pubtrans/ura/UraClient.java
index eb12944..fa61785 100644
--- a/src/main/java/de/stklcode/pubtrans/ura/UraClient.java
+++ b/src/main/java/de/stklcode/pubtrans/ura/UraClient.java
@@ -17,12 +17,15 @@
 package de.stklcode.pubtrans.ura;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
+import de.stklcode.pubtrans.ura.exception.UraClientConfigurationException;
+import de.stklcode.pubtrans.ura.exception.UraClientException;
 import de.stklcode.pubtrans.ura.model.Message;
 import de.stklcode.pubtrans.ura.model.Stop;
 import de.stklcode.pubtrans.ura.model.Trip;
 import de.stklcode.pubtrans.ura.reader.AsyncUraTripReader;
 
 import java.io.*;
+import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URL;
 import java.net.URLEncoder;
@@ -207,8 +210,11 @@ public class UraClient implements Serializable {
      * If forStops() and/or forLines() has been called, those will be used as filter.
      *
      * @return List of trips.
+     * @throws UraClientException Error with API communication.
+     * @since 1.0
+     * @since 2.0 Throws {@link UraClientException}.
      */
-    public List<Trip> getTrips() {
+    public List<Trip> getTrips() throws UraClientException {
         return getTrips(new Query(), null);
     }
 
@@ -218,8 +224,11 @@ public class UraClient implements Serializable {
      *
      * @param limit Maximum number of results.
      * @return List of trips.
+     * @throws UraClientException Error with API communication.
+     * @since 1.0
+     * @since 2.0 Throws {@link UraClientException}.
      */
-    public List<Trip> getTrips(final Integer limit) {
+    public List<Trip> getTrips(final Integer limit) throws UraClientException {
         return getTrips(new Query(), limit);
     }
 
@@ -229,8 +238,12 @@ public class UraClient implements Serializable {
      *
      * @param query The query.
      * @return List of trips.
+     * @throws UraClientException Error with API communication.
+     * @throws UraClientException Error with API communication.
+     * @since 1.0
+     * @since 2.0 Throws {@link UraClientException}.
      */
-    public List<Trip> getTrips(final Query query) {
+    public List<Trip> getTrips(final Query query) throws UraClientException {
         return getTrips(query, null);
     }
 
@@ -240,8 +253,11 @@ public class UraClient implements Serializable {
      * @param query The query.
      * @param limit Maximum number of results.
      * @return List of trips.
+     * @throws UraClientException Error with API communication.
+     * @since 1.0
+     * @since 2.0 Throws {@link UraClientException}.
      */
-    public List<Trip> getTrips(final Query query, final Integer limit) {
+    public List<Trip> getTrips(final Query query, final Integer limit) throws UraClientException {
         List<Trip> trips = new ArrayList<>();
         try (InputStream is = requestInstant(REQUEST_TRIP, query);
              BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
@@ -260,7 +276,7 @@ public class UraClient implements Serializable {
                 line = br.readLine();
             }
         } catch (IOException e) {
-            throw new IllegalStateException("Failed to read trips from API", e);
+            throw new UraClientException("Failed to read trips from API", e);
         }
         return trips;
     }
@@ -271,11 +287,11 @@ public class UraClient implements Serializable {
      * @param query    The query.
      * @param consumer Consumer(s) for single trips.
      * @return Trip reader.
-     * @throws IOException Error reading response.
+     * @throws UraClientConfigurationException Error reading response.
      * @see #getTripsStream(Query, List)
-     * @since 1.2.0
+     * @since 1.2
      */
-    public AsyncUraTripReader getTripsStream(final Query query, final Consumer<Trip> consumer) throws IOException {
+    public AsyncUraTripReader getTripsStream(final Query query, final Consumer<Trip> consumer) throws UraClientConfigurationException {
         return getTripsStream(query, Collections.singletonList(consumer));
     }
 
@@ -285,28 +301,36 @@ public class UraClient implements Serializable {
      * @param query     The query.
      * @param consumers Consumer(s) for single trips.
      * @return Trip reader.
-     * @throws IOException Error retrieving stream response.
-     * @since 1.2.0
+     * @throws UraClientConfigurationException Error retrieving stream response.
+     * @since 1.2
+     * @since 2.0 Throws {@link UraClientConfigurationException}.
      */
-    public AsyncUraTripReader getTripsStream(final Query query, final List<Consumer<Trip>> consumers) throws IOException {
+    public AsyncUraTripReader getTripsStream(final Query query, final List<Consumer<Trip>> consumers) throws UraClientConfigurationException {
         // Create the reader.
-        AsyncUraTripReader reader = new AsyncUraTripReader(
-                new URL(requestURL(config.getBaseURL() + config.getStreeamPath(), REQUEST_TRIP, query)),
-                consumers
-        );
+        try {
+            AsyncUraTripReader reader = new AsyncUraTripReader(
+                    new URL(requestURL(config.getBaseURL() + config.getStreeamPath(), REQUEST_TRIP, query)),
+                    consumers
+            );
 
-        // Open the reader, i.e. start reading from API.
-        reader.open();
+            // Open the reader, i.e. start reading from API.
+            reader.open();
 
-        return reader;
+            return reader;
+        } catch (MalformedURLException e) {
+            throw new UraClientConfigurationException("Invalid API URL, check client configuration.", e);
+        }
     }
 
     /**
      * Get list of stops without filters.
      *
      * @return The list of stops.
+     * @throws UraClientException Error with API communication.
+     * @since 1.0
+     * @since 2.0 Throws {@link UraClientException}.
      */
-    public List<Stop> getStops() {
+    public List<Stop> getStops() throws UraClientException {
         return getStops(new Query());
     }
 
@@ -316,8 +340,11 @@ public class UraClient implements Serializable {
      *
      * @param query The query.
      * @return The list.
+     * @throws UraClientException Error with API communication.
+     * @since 1.0
+     * @since 2.0 Throws {@link UraClientException}.
      */
-    public List<Stop> getStops(final Query query) {
+    public List<Stop> getStops(final Query query) throws UraClientException {
         List<Stop> stops = new ArrayList<>();
         try (InputStream is = requestInstant(REQUEST_STOP, query);
              BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
@@ -330,7 +357,7 @@ public class UraClient implements Serializable {
                 }
             }
         } catch (IOException e) {
-            throw new IllegalStateException("Failed to read stops from API", e);
+            throw new UraClientException("Failed to read stops from API", e);
         }
         return stops;
     }
@@ -339,9 +366,11 @@ public class UraClient implements Serializable {
      * Get list of messages.
      *
      * @return List of messages.
+     * @throws UraClientException Error with API communication.
      * @since 1.3
+     * @since 2.0 Throw {@link UraClientException}.
      */
-    public List<Message> getMessages() {
+    public List<Message> getMessages() throws UraClientException {
         return getMessages(new Query(), null);
     }
 
@@ -352,9 +381,11 @@ public class UraClient implements Serializable {
      *
      * @param query The query.
      * @return List of trips.
+     * @throws UraClientException Error with API communication.
      * @since 1.3
+     * @since 2.0 Throw {@link UraClientException}.
      */
-    public List<Message> getMessages(final Query query) {
+    public List<Message> getMessages(final Query query) throws UraClientException {
         return getMessages(query, null);
     }
 
@@ -364,9 +395,11 @@ public class UraClient implements Serializable {
      * @param query The query.
      * @param limit Maximum number of results.
      * @return List of trips.
+     * @throws UraClientException Error with API communication.
      * @since 1.3
+     * @since 2.0 Throw {@link UraClientException}.
      */
-    public List<Message> getMessages(final Query query, final Integer limit) {
+    public List<Message> getMessages(final Query query, final Integer limit) throws UraClientException {
         List<Message> messages = new ArrayList<>();
         try (InputStream is = requestInstant(REQUEST_MESSAGE, query);
              BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
@@ -385,7 +418,7 @@ public class UraClient implements Serializable {
                 line = br.readLine();
             }
         } catch (IOException e) {
-            throw new IllegalStateException("Failed to read messages from API", e);
+            throw new UraClientException("Failed to read messages from API", e);
         }
         return messages;
     }
@@ -409,35 +442,35 @@ public class UraClient implements Serializable {
      * @param returnList  Fields to fetch.
      * @param query       The query.
      * @return The URL
-     * @throws IOException on errors
-     * @since 1.2.0
+     * @since 1.2
+     * @since 2.0 Does not throw exception anymore.
      */
-    private String requestURL(final String endpointURL, final String[] returnList, final Query query) throws IOException {
+    private String requestURL(final String endpointURL, final String[] returnList, final Query query) {
         String urlStr = endpointURL + "?ReturnList=" + String.join(",", returnList);
 
         if (query.stopIDs != null && query.stopIDs.length > 0) {
-            urlStr += "&" + PAR_STOP_ID + "=" + URLEncoder.encode(String.join(",", query.stopIDs), UTF_8.name());
+            urlStr += "&" + PAR_STOP_ID + "=" + URLEncoder.encode(String.join(",", query.stopIDs), UTF_8);
         }
         if (query.stopNames != null && query.stopNames.length > 0) {
-            urlStr += "&" + PAR_STOP_NAME + "=" + URLEncoder.encode(String.join(",", query.stopNames), UTF_8.name());
+            urlStr += "&" + PAR_STOP_NAME + "=" + URLEncoder.encode(String.join(",", query.stopNames), UTF_8);
         }
         if (query.lineIDs != null && query.lineIDs.length > 0) {
-            urlStr += "&" + PAR_LINE_ID + "=" + URLEncoder.encode(String.join(",", query.lineIDs), UTF_8.name());
+            urlStr += "&" + PAR_LINE_ID + "=" + URLEncoder.encode(String.join(",", query.lineIDs), UTF_8);
         }
         if (query.lineNames != null && query.lineNames.length > 0) {
-            urlStr += "&" + PAR_LINE_NAME + "=" + URLEncoder.encode(String.join(",", query.lineNames), UTF_8.name());
+            urlStr += "&" + PAR_LINE_NAME + "=" + URLEncoder.encode(String.join(",", query.lineNames), UTF_8);
         }
         if (query.direction != null) {
             urlStr += "&" + PAR_DIR_ID + "=" + query.direction;
         }
         if (query.destinationNames != null) {
-            urlStr += "&" + PAR_DEST_NAME + "=" + URLEncoder.encode(String.join(",", query.destinationNames), UTF_8.name());
+            urlStr += "&" + PAR_DEST_NAME + "=" + URLEncoder.encode(String.join(",", query.destinationNames), UTF_8);
         }
         if (query.towards != null) {
-            urlStr += "&" + PAR_TOWARDS + "=" + URLEncoder.encode(String.join(",", query.towards), UTF_8.name());
+            urlStr += "&" + PAR_TOWARDS + "=" + URLEncoder.encode(String.join(",", query.towards), UTF_8);
         }
         if (query.circle != null) {
-            urlStr += "&" + PAR_CIRCLE + "=" + URLEncoder.encode(query.circle, UTF_8.name());
+            urlStr += "&" + PAR_CIRCLE + "=" + URLEncoder.encode(query.circle, UTF_8);
         }
 
         return urlStr;
@@ -572,8 +605,11 @@ public class UraClient implements Serializable {
          * Get stops for set filters.
          *
          * @return List of matching trips.
+         * @throws UraClientException Error with API communication.
+         * @since 1.0
+         * @since 2.0 Throws {@link UraClientException}.
          */
-        public List<Stop> getStops() {
+        public List<Stop> getStops() throws UraClientException {
             return UraClient.this.getStops(this);
         }
 
@@ -581,8 +617,11 @@ public class UraClient implements Serializable {
          * Get trips for set filters.
          *
          * @return List of matching trips.
+         * @throws UraClientException Error with API communication.
+         * @since 1.0
+         * @since 2.0 Throws {@link UraClientException}.
          */
-        public List<Trip> getTrips() {
+        public List<Trip> getTrips() throws UraClientException {
             return UraClient.this.getTrips(this);
         }
 
@@ -591,11 +630,11 @@ public class UraClient implements Serializable {
          *
          * @param consumer Consumer for single trips.
          * @return Trip reader.
-         * @throws IOException Errors retrieving stream response.
+         * @throws UraClientConfigurationException Error reading response.
          * @see #getTripsStream(List)
-         * @since 1.2.0
+         * @since 1.2
          */
-        public AsyncUraTripReader getTripsStream(Consumer<Trip> consumer) throws IOException {
+        public AsyncUraTripReader getTripsStream(Consumer<Trip> consumer) throws UraClientConfigurationException {
             return UraClient.this.getTripsStream(this, consumer);
         }
 
@@ -604,10 +643,10 @@ public class UraClient implements Serializable {
          *
          * @param consumers Consumers for single trips.
          * @return Trip reader.
-         * @throws IOException Errors retrieving stream response.
-         * @since 1.2.0
+         * @throws UraClientConfigurationException Errors retrieving stream response.
+         * @since 1.2
          */
-        public AsyncUraTripReader getTripsStream(List<Consumer<Trip>> consumers) throws IOException {
+        public AsyncUraTripReader getTripsStream(List<Consumer<Trip>> consumers) throws UraClientConfigurationException {
             return UraClient.this.getTripsStream(this, consumers);
         }
 
@@ -615,9 +654,11 @@ public class UraClient implements Serializable {
          * Get trips for set filters.
          *
          * @return List of matching messages.
+         * @throws UraClientException Error with API communication.
          * @since 1.3
+         * @since 2.0 Throws {@link UraClientException}.
          */
-        public List<Message> getMessages() {
+        public List<Message> getMessages() throws UraClientException {
             return UraClient.this.getMessages(this);
         }
     }
diff --git a/src/main/java/de/stklcode/pubtrans/ura/exception/UraClientConfigurationException.java b/src/main/java/de/stklcode/pubtrans/ura/exception/UraClientConfigurationException.java
new file mode 100644
index 0000000..e2d413f
--- /dev/null
+++ b/src/main/java/de/stklcode/pubtrans/ura/exception/UraClientConfigurationException.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016-2020 Stefan Kalscheuer
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.stklcode.pubtrans.ura.exception;
+
+/**
+ * Custom exception class indicating an error with the URA client configuration.
+ *
+ * @author Stefan Kalscheuer
+ * @since 2.0
+ */
+public class UraClientConfigurationException extends UraClientException {
+    private static final long serialVersionUID = -8035752391477338659L;
+
+    /**
+     * Default constructor.
+     *
+     * @param message The detail message (which is saved for later retrieval by the {@link #getMessage()} method)
+     * @param cause   The cause (which is saved for later retrieval by the {@link #getCause()} method).
+     */
+    public UraClientConfigurationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/src/main/java/de/stklcode/pubtrans/ura/exception/UraClientException.java b/src/main/java/de/stklcode/pubtrans/ura/exception/UraClientException.java
new file mode 100644
index 0000000..7de90bf
--- /dev/null
+++ b/src/main/java/de/stklcode/pubtrans/ura/exception/UraClientException.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016-2020 Stefan Kalscheuer
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.stklcode.pubtrans.ura.exception;
+
+import java.io.IOException;
+
+/**
+ * Custom exception class indicating an error with the URA API communication.
+ *
+ * @author Stefan Kalscheuer
+ * @since 2.0
+ */
+public class UraClientException extends IOException {
+    private static final long serialVersionUID = 4585240685746203433L;
+
+    /**
+     * Default constructor.
+     *
+     * @param message The detail message (which is saved for later retrieval by the {@link #getMessage()} method)
+     * @param cause   The cause (which is saved for later retrieval by the {@link #getCause()} method).
+     */
+    public UraClientException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 76ecbcf..d92b502 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -16,6 +16,7 @@
 
 module de.stklcode.pubtrans.juraclient {
     exports de.stklcode.pubtrans.ura;
+    exports de.stklcode.pubtrans.ura.exception;
     exports de.stklcode.pubtrans.ura.model;
     exports de.stklcode.pubtrans.ura.reader;
 
diff --git a/src/test/java/de/stklcode/pubtrans/ura/UraClientTest.java b/src/test/java/de/stklcode/pubtrans/ura/UraClientTest.java
index 054e2ea..86779c0 100644
--- a/src/test/java/de/stklcode/pubtrans/ura/UraClientTest.java
+++ b/src/test/java/de/stklcode/pubtrans/ura/UraClientTest.java
@@ -19,6 +19,7 @@ package de.stklcode.pubtrans.ura;
 import com.github.tomakehurst.wiremock.WireMockServer;
 import com.github.tomakehurst.wiremock.client.WireMock;
 import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import de.stklcode.pubtrans.ura.exception.UraClientException;
 import de.stklcode.pubtrans.ura.model.Message;
 import de.stklcode.pubtrans.ura.model.Stop;
 import de.stklcode.pubtrans.ura.model.Trip;
@@ -63,7 +64,7 @@ public class UraClientTest {
     }
 
     @Test
-    public void getStopsTest() {
+    public void getStopsTest() throws UraClientException {
         // Mock the HTTP call.
         mockHttpToFile(2, "instant_V2_stops.txt");
 
@@ -89,7 +90,7 @@ public class UraClientTest {
     }
 
     @Test
-    public void getStopsForLineTest() {
+    public void getStopsForLineTest() throws UraClientException {
         // Mock the HTTP call.
         mockHttpToFile(2, "instant_V2_stops_line.txt");
 
@@ -107,7 +108,7 @@ public class UraClientTest {
     }
 
     @Test
-    public void getStopsForPositionTest() {
+    public void getStopsForPositionTest() throws UraClientException {
         // Mock the HTTP call.
         mockHttpToFile(1, "instant_V1_stops_circle.txt");
 
@@ -133,7 +134,7 @@ public class UraClientTest {
     }
 
     @Test
-    public void getTripsForDestinationNamesTest() {
+    public void getTripsForDestinationNamesTest() throws UraClientException {
         // Mock the HTTP call.
         mockHttpToFile(1, "instant_V1_trips_destination.txt");
 
@@ -156,7 +157,7 @@ public class UraClientTest {
     }
 
     @Test
-    public void getTripsTowardsTest() {
+    public void getTripsTowardsTest() throws UraClientException {
         // Mock the HTTP call.
         mockHttpToFile(1, "instant_V1_trips_towards.txt");
 
@@ -171,7 +172,7 @@ public class UraClientTest {
     }
 
     @Test
-    public void getTripsTest() {
+    public void getTripsTest() throws UraClientException {
         // Mock the HTTP call.
         mockHttpToFile(1, "instant_V1_trips_all.txt");
 
@@ -224,7 +225,7 @@ public class UraClientTest {
     }
 
     @Test
-    public void getTripsForStopTest() {
+    public void getTripsForStopTest() throws UraClientException {
         // Mock the HTTP call.
         mockHttpToFile(1, "instant_V1_trips_stop.txt");
 
@@ -254,7 +255,7 @@ public class UraClientTest {
     }
 
     @Test
-    public void getTripsForLine() {
+    public void getTripsForLine() throws UraClientException {
         // Mock the HTTP call.
         mockHttpToFile(1, "instant_V1_trips_line.txt");
 
@@ -303,7 +304,7 @@ public class UraClientTest {
     }
 
     @Test
-    public void getTripsForStopAndLine() {
+    public void getTripsForStopAndLine() throws UraClientException {
         // Mock the HTTP call.
         mockHttpToFile(1, "instant_V1_trips_stop_line.txt");
 
@@ -324,7 +325,7 @@ public class UraClientTest {
 
 
     @Test
-    public void getMessages() {
+    public void getMessages() throws UraClientException {
         // Mock the HTTP call.
         mockHttpToFile(1, "instant_V1_messages.txt");
 
@@ -343,7 +344,7 @@ public class UraClientTest {
     }
 
     @Test
-    public void getMessagesForStop() {
+    public void getMessagesForStop() throws UraClientException {
         // Mock the HTTP call.
         mockHttpToFile(2, "instant_V2_messages_stop.txt");