diff --git a/CHANGELOG.md b/CHANGELOG.md index c0319b5..f6fe189 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file. ### Security * Updated dependencies +### Features +* Added support for reading messages, using `getMessages()` method (#5) + ## 1.2.0 - 2019-06-20 ### Security diff --git a/pom.xml b/pom.xml index ffe38e3..3aaa5e9 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ de.stklcode.pubtrans juraclient - 1.2.1-SNAPSHOT + 1.3.0-SNAPSHOT UTF-8 diff --git a/src/main/java/de/stklcode/pubtrans/ura/UraClient.java b/src/main/java/de/stklcode/pubtrans/ura/UraClient.java index f539dcc..e8ae35b 100644 --- a/src/main/java/de/stklcode/pubtrans/ura/UraClient.java +++ b/src/main/java/de/stklcode/pubtrans/ura/UraClient.java @@ -17,6 +17,7 @@ package de.stklcode.pubtrans.ura; import com.fasterxml.jackson.databind.ObjectMapper; +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; @@ -58,14 +59,21 @@ public class UraClient implements Serializable { private static final String PAR_ESTTIME = "EstimatedTime"; private static final String PAR_TOWARDS = "Towards"; private static final String PAR_CIRCLE = "Circle"; + private static final String PAR_MSG_UUID = "MessageUUID"; + private static final String PAR_MSG_TYPE = "MessageType"; + private static final String PAR_MSG_PRIORITY = "MessagePriority"; + private static final String PAR_MSG_TEXT = "MessageText"; private static final Integer RES_TYPE_STOP = 0; private static final Integer RES_TYPE_PREDICTION = 1; + private static final Integer RES_TYPE_FLEX_MESSAGE = 2; private static final Integer RES_TYPE_URA_VERSION = 4; private static final String[] REQUEST_STOP = {PAR_STOP_NAME, PAR_STOP_ID, PAR_STOP_INDICATOR, PAR_STOP_STATE, PAR_GEOLOCATION}; private static final String[] REQUEST_TRIP = {PAR_STOP_NAME, PAR_STOP_ID, PAR_STOP_INDICATOR, PAR_STOP_STATE, PAR_GEOLOCATION, PAR_VISIT_NUMBER, PAR_LINE_ID, PAR_LINE_NAME, PAR_DIR_ID, PAR_DEST_NAME, PAR_DEST_TEXT, PAR_VEHICLE_ID, PAR_TRIP_ID, PAR_ESTTIME}; + private static final String[] REQUEST_MESSAGE = {PAR_STOP_NAME, PAR_STOP_ID, PAR_STOP_INDICATOR, PAR_STOP_STATE, PAR_GEOLOCATION, + PAR_MSG_UUID, PAR_MSG_TYPE, PAR_MSG_PRIORITY, PAR_MSG_TEXT}; private final String baseURL; private final String instantURL; @@ -309,6 +317,61 @@ public class UraClient implements Serializable { return stops; } + /** + * Get list of messages. + * + * @return List of messages. + * @since 1.3 + */ + public List getMessages() { + return getMessages(new Query(), null); + } + + + /** + * Get list of messages. + * If forStops() has been called, those will be used as filter. + * + * @param query The query. + * @return List of trips. + * @since 1.3 + */ + public List getMessages(final Query query) { + return getMessages(query, null); + } + + /** + * Get list of messages for given stopIDs with result limit. + * + * @param query The query. + * @param limit Maximum number of results. + * @return List of trips. + * @since 1.3 + */ + public List getMessages(final Query query, final Integer limit) { + List messages = new ArrayList<>(); + try (InputStream is = requestInstant(REQUEST_MESSAGE, query); + BufferedReader br = new BufferedReader(new InputStreamReader(is))) { + String version = null; + String line = br.readLine(); + while (line != null && (limit == null || messages.size() < limit)) { + List l = mapper.readValue(line, List.class); + /* Check if result exists and has correct response type */ + if (l != null && !l.isEmpty()) { + if (l.get(0).equals(RES_TYPE_URA_VERSION)) { + version = l.get(1).toString(); + } else if (l.get(0).equals(RES_TYPE_FLEX_MESSAGE)) { + messages.add(new Message(l, version)); + } + } + line = br.readLine(); + } + } catch (IOException e) { + throw new IllegalStateException("Failed to read from API", e); + } + return messages; + } + /** * Issue request to instant endpoint and return input stream. * @@ -517,5 +580,15 @@ public class UraClient implements Serializable { public AsyncUraTripReader getTripsStream(List> consumers) throws IOException { return UraClient.this.getTripsStream(this, consumers); } + + /** + * Get trips for set filters. + * + * @return List of matching messages. + * @since 1.3 + */ + public List getMessages() { + return UraClient.this.getMessages(this); + } } } diff --git a/src/main/java/de/stklcode/pubtrans/ura/model/Message.java b/src/main/java/de/stklcode/pubtrans/ura/model/Message.java new file mode 100644 index 0000000..24f3ad8 --- /dev/null +++ b/src/main/java/de/stklcode/pubtrans/ura/model/Message.java @@ -0,0 +1,165 @@ +package de.stklcode.pubtrans.ura.model; + +import java.io.IOException; +import java.util.List; + +/** + * Entity for a message. + * + * @author Stefan Kalscheuer + * @since 1.3 + */ +public class Message implements Model { + private static final int MSG_UUID = 7; + private static final int MSG_TYPE = 8; + private static final int MSG_PRIORITY = 9; + private static final int MSG_TEXT = 10; + private static final int NUM_OF_FIELDS = 11; + + private final Stop stop; + private final String uuid; + private final Integer type; + private final Integer priority; + private final String text; + + /** + * Construct Message object from complete set of data. + * + * @param stopID Stop ID. + * @param stopName Stop name. + * @param stopIndicator Stop Indicator. + * @param stopState Stop state. + * @param stopLatitude Stop geolocation latitude. + * @param stopLongitude Stop geolocation latitude. + * @param msgUUID Message UUID. + * @param msgType Message type. + * @param msgPriority Message priority. + * @param msgText Message text. + */ + public Message(final String stopID, + final String stopName, + final String stopIndicator, + final Integer stopState, + final Double stopLatitude, + final Double stopLongitude, + final String msgUUID, + final Integer msgType, + final Integer msgPriority, + final String msgText) { + this(new Stop(stopID, + stopName, + stopIndicator, + stopState, + stopLatitude, + stopLongitude), + msgUUID, + msgType, + msgPriority, + msgText); + } + + /** + * Construct Message object from Stop model and set of additional data. + * + * @param stop Stop model + * @param msgUUID Message UUID. + * @param msgType Message type. + * @param msgPriority Message priority. + * @param msgText Message text. + */ + public Message(final Stop stop, + final String msgUUID, + final Integer msgType, + final Integer msgPriority, + final String msgText) { + this.stop = stop; + this.uuid = msgUUID; + this.type = msgType; + this.priority = msgPriority; + this.text = msgText; + } + + /** + * Construct Message object from raw list of attributes parsed from JSON. + * + * @param raw List of attributes from JSON line + * @throws IOException Thrown on invalid line format. + */ + public Message(final List raw) throws IOException { + this(raw, null); + } + + /** + * Construct Message object from raw list of attributes parsed from JSON with explicitly specified version. + * + * @param raw List of attributes from JSON line + * @param version API version + * @throws IOException Thrown on invalid line format. + */ + public Message(final List raw, final String version) throws IOException { + if (raw == null || raw.size() < NUM_OF_FIELDS) { + throw new IOException("Invalid number of fields"); + } + + stop = new Stop(raw); + + if (raw.get(MSG_UUID) instanceof String) { + uuid = (String) raw.get(MSG_UUID); + } else { + throw Model.typeErrorString(MSG_UUID, raw.get(MSG_UUID).getClass()); + } + + if (raw.get(MSG_TYPE) instanceof Integer) { + type = (Integer) raw.get(MSG_TYPE); + } else { + throw Model.typeError(MSG_TYPE, raw.get(MSG_TYPE).getClass(), "Integer"); + } + + if (raw.get(MSG_PRIORITY) instanceof Integer) { + priority = (Integer) raw.get(MSG_PRIORITY); + } else { + throw Model.typeError(MSG_PRIORITY, raw.get(MSG_PRIORITY).getClass(), "Integer"); + } + + if (raw.get(MSG_TEXT) instanceof String) { + text = (String) raw.get(MSG_TEXT); + } else { + throw Model.typeErrorString(MSG_TEXT, raw.get(MSG_TEXT).getClass()); + } + } + + /** + * @return The affected stop. + */ + public Stop getStop() { + return stop; + } + + /** + * @return Message's unique identifier. + */ + public String getUuid() { + return uuid; + } + + /** + * @return Message type. + */ + public Integer getType() { + return type; + } + + /** + * @return Message priority. Lower value equals higher priority. + */ + public Integer getPriority() { + return priority; + } + + /** + * @return Message text. + */ + public String getText() { + return text; + } +} diff --git a/src/test/java/de/stklcode/pubtrans/ura/UraClientTest.java b/src/test/java/de/stklcode/pubtrans/ura/UraClientTest.java index e770153..b327ac4 100644 --- a/src/test/java/de/stklcode/pubtrans/ura/UraClientTest.java +++ b/src/test/java/de/stklcode/pubtrans/ura/UraClientTest.java @@ -16,6 +16,7 @@ package de.stklcode.pubtrans.ura; +import de.stklcode.pubtrans.ura.model.Message; import de.stklcode.pubtrans.ura.model.Stop; import de.stklcode.pubtrans.ura.model.Trip; import net.bytebuddy.ByteBuddy; @@ -331,6 +332,43 @@ public class UraClientTest { } + @Test + public void getMessages() { + // Mock the HTTP call. + mockHttpToFile("instant_V1_messages.txt"); + + // Get messages without filter and verify some values. + List messages = new UraClient("mocked") + .getMessages(); + assertThat(messages, hasSize(2)); + assertThat(messages.get(0).getStop().getId(), is("100707")); + assertThat(messages.get(0).getUuid(), is("016e1231d4e30014_100707")); + assertThat(messages.get(1).getStop().getName(), is("Herzogenr. Rathaus")); + assertThat(messages.get(1).getUuid(), is("016e2cc3a3750006_210511")); + assertThat(messages.get(0).getType(), is(0)); + assertThat(messages.get(1).getPriority(), is(0)); + assertThat(messages.get(0).getText(), is("Sehr geehrte Fahrgäste, wegen Strassenbauarbeiten kann diese Haltestelle nicht von den Bussen der Linien 17, 44 und N2 angefahren werden.")); + assertThat(messages.get(1).getText(), is("Sehr geehrte Fahrgäste, diese Haltestelle wird vorübergehend von den Linien 47, 147 und N3 nicht angefahren.")); + } + + @Test + public void getMessagesForStop() { + // Mock the HTTP call. + mockHttpToFile("instant_V2_messages_stop.txt"); + + // Get trips for stop ID 100707 (Berensberger Str.) and verify some values. + List messages = new UraClient("mocked") + .forStops("100707") + .getMessages(); + assertThat(messages, hasSize(1)); + assertThat(messages.stream().filter(t -> !t.getStop().getId().equals("100707")).findAny(), is(Optional.empty())); + assertThat(messages.get(0).getUuid(), is("016e1231d4e30014_100707")); + assertThat(messages.get(0).getType(), is(0)); + assertThat(messages.get(0).getPriority(), is(3)); + assertThat(messages.get(0).getText(), is("Sehr geehrte Fahrgäste, wegen Strassenbauarbeiten kann diese Haltestelle nicht von den Bussen der Linien 17, 44 und N2 angefahren werden.")); + } + + private static void mockHttpToFile(String newResourceFile) { mockResource = newResourceFile; } diff --git a/src/test/java/de/stklcode/pubtrans/ura/model/MessageTest.java b/src/test/java/de/stklcode/pubtrans/ura/model/MessageTest.java new file mode 100644 index 0000000..f7dd17d --- /dev/null +++ b/src/test/java/de/stklcode/pubtrans/ura/model/MessageTest.java @@ -0,0 +1,153 @@ +/* + * Copyright 2016-2019 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.model; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Unit test for the {@link Message} meta model. + * + * @author Stefan Kalscheuer + */ +public class MessageTest { + @Test + public void basicConstructorTest() { + Message message = new Message("sid", + "name", + "indicator", + 1, + 2.345, + 6.789, + "msg_uuid", + 1, + 3, + "message text"); + assertThat(message.getStop().getId(), is("sid")); + assertThat(message.getStop().getName(), is("name")); + assertThat(message.getStop().getIndicator(), is("indicator")); + assertThat(message.getStop().getState(), is(1)); + assertThat(message.getStop().getLatitude(), is(2.345)); + assertThat(message.getStop().getLongitude(), is(6.789)); + assertThat(message.getUuid(), is("msg_uuid")); + assertThat(message.getType(), is(1)); + assertThat(message.getPriority(), is(3)); + assertThat(message.getText(), is("message text")); + } + + @Test + public void listConstructorTest() { + /* Create valid raw data list */ + List raw = new ArrayList<>(); + raw.add(1); + raw.add("stopName"); + raw.add("stopId"); + raw.add("stopIndicator"); + raw.add(9); + raw.add(8.765); + raw.add(43.21); + raw.add("msg_uuid"); + raw.add(1); + raw.add(3); + raw.add("message text"); + + try { + Message message = new Message(raw); + assertThat(message.getStop().getId(), is("stopId")); + assertThat(message.getStop().getName(), is("stopName")); + assertThat(message.getStop().getIndicator(), is("stopIndicator")); + assertThat(message.getStop().getState(), is(9)); + assertThat(message.getStop().getLatitude(), is(8.765)); + assertThat(message.getStop().getLongitude(), is(43.21)); + assertThat(message.getUuid(), is("msg_uuid")); + assertThat(message.getType(), is(1)); + assertThat(message.getPriority(), is(3)); + assertThat(message.getText(), is("message text")); + } catch (IOException e) { + fail("Creation of Message from valid list failed: " + e.getMessage()); + } + + /* Excess elements should be ignored */ + raw.add("foo"); + try { + Message message = new Message(raw); + assertThat(message, is(notNullValue())); + raw.remove(11); + } catch (IOException e) { + fail("Creation of Message from valid list failed: " + e.getMessage()); + } + + /* Test exceptions on invalid data */ + List invalid = new ArrayList<>(raw); + invalid.remove(7); + invalid.add(7, 123L); + try { + new Message(invalid); + fail("Creation of Message with invalid UUID field successful"); + } catch (Exception e) { + assertThat(e, is(instanceOf(IOException.class))); + } + + invalid = new ArrayList<>(raw); + invalid.remove(8); + invalid.add(8, "abc"); + try { + new Message(invalid); + fail("Creation of Message with invalid type field successful"); + } catch (Exception e) { + assertThat(e, is(instanceOf(IOException.class))); + } + + invalid = new ArrayList<>(raw); + invalid.remove(9); + invalid.add(9, "xyz"); + try { + new Message(invalid); + fail("Creation of Message with invalid priority field successful"); + } catch (Exception e) { + assertThat(e, is(instanceOf(IOException.class))); + } + + invalid = new ArrayList<>(raw); + invalid.remove(10); + invalid.add(10, 1.23); + try { + new Message(invalid); + fail("Creation of Message with invalid text field successful"); + } catch (Exception e) { + assertThat(e, is(instanceOf(IOException.class))); + } + + invalid = new ArrayList<>(raw); + invalid.remove(10); + try { + new Message(invalid); + fail("Creation of Message with too short list successful"); + } catch (Exception e) { + assertThat(e, is(instanceOf(IOException.class))); + } + } +} diff --git a/src/test/resources/de/stklcode/pubtrans/ura/instant_V1_messages.txt b/src/test/resources/de/stklcode/pubtrans/ura/instant_V1_messages.txt new file mode 100644 index 0000000..02356b4 --- /dev/null +++ b/src/test/resources/de/stklcode/pubtrans/ura/instant_V1_messages.txt @@ -0,0 +1,3 @@ +[4,"1.0",1572882473479] +[2,"Berensberger Str.","100707","",0,50.8087069,6.0607177,"016e1231d4e30014_100707",0,3,"Sehr geehrte Fahrgäste, wegen Strassenbauarbeiten kann diese Haltestelle nicht von den Bussen der Linien 17, 44 und N2 angefahren werden."] +[2,"Herzogenr. Rathaus","210511","",0,50.8718175,6.1025675,"016e2cc3a3750006_210511",0,0,"Sehr geehrte Fahrgäste, diese Haltestelle wird vorübergehend von den Linien 47, 147 und N3 nicht angefahren."] diff --git a/src/test/resources/de/stklcode/pubtrans/ura/instant_V2_messages_stop.txt b/src/test/resources/de/stklcode/pubtrans/ura/instant_V2_messages_stop.txt new file mode 100644 index 0000000..d02e15f --- /dev/null +++ b/src/test/resources/de/stklcode/pubtrans/ura/instant_V2_messages_stop.txt @@ -0,0 +1,2 @@ +[4,"2.0",1572882473479] +[2,"Berensberger Str.","100707","",0,50.8087069,6.0607177,"016e1231d4e30014_100707",0,3,"Sehr geehrte Fahrgäste, wegen Strassenbauarbeiten kann diese Haltestelle nicht von den Bussen der Linien 17, 44 und N2 angefahren werden."]