From fb7c4475b1867fae85d4fce5994c39fe29f07c63 Mon Sep 17 00:00:00 2001 From: Coffeemakr Date: Tue, 12 Sep 2017 20:35:59 +0200 Subject: [PATCH] Improve some things and update dependencies --- app/build.gradle | 8 +- .../unstable/ost/ConnectionListFragment.java | 25 +++-- .../ost/api/model/PassingCheckpoint.java | 3 +- .../ch/unstable/ost/api/model/Section.java | 5 +- .../ch/unstable/ost/api/search/SearchAPI.java | 64 ++++++++++- .../search/types/ConnectionDeserializer.java | 21 ++++ .../types/PassingCheckpointsDeserializer.java | 82 ++++++++++++++ .../types/SearchCHIconClassDeserializer.java | 3 +- .../search/types/SectionsDeserializer.java | 102 ++++++++++++++++++ .../transport/types/LocationDeserializer.java | 1 + .../ost/api/search/SearchAPITest.java | 52 ++++++++- .../ost/api/transport/TransportAPITest.java | 8 +- 12 files changed, 347 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/ch/unstable/ost/api/search/types/ConnectionDeserializer.java create mode 100644 app/src/main/java/ch/unstable/ost/api/search/types/PassingCheckpointsDeserializer.java create mode 100644 app/src/main/java/ch/unstable/ost/api/search/types/SectionsDeserializer.java diff --git a/app/build.gradle b/app/build.gradle index 6037ac5..c07d1f9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,7 +82,7 @@ dependencies { }) - errorprone 'com.google.errorprone:error_prone_core:2.0.5' + errorprone 'com.google.errorprone:error_prone_core:2.1.1' compile('com.google.guava:guava:23.0-android', { exclude group: 'com.google.code.findbugs' @@ -101,10 +101,12 @@ dependencies { compile 'com.android.support:preference-v7:25.3.1' compile 'com.android.support:support-vector-drawable:25.3.1' testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:2.8.47' + testCompile 'org.mockito:mockito-core:2.10.0' compile 'io.reactivex.rxjava2:rxandroid:2.0.1' // Because RxAndroid releases are few and far between, it is recommended you also // explicitly depend on RxJava's latest version for bug fixes and new features. - compile 'io.reactivex.rxjava2:rxjava:2.1.2' + compile 'io.reactivex.rxjava2:rxjava:2.1.3' + + compile 'io.mikael:urlbuilder:2.0.9' } diff --git a/app/src/main/java/ch/unstable/ost/ConnectionListFragment.java b/app/src/main/java/ch/unstable/ost/ConnectionListFragment.java index 2543d74..6eb5873 100644 --- a/app/src/main/java/ch/unstable/ost/ConnectionListFragment.java +++ b/app/src/main/java/ch/unstable/ost/ConnectionListFragment.java @@ -52,12 +52,14 @@ public class ConnectionListFragment extends Fragment { private View mLoadingIndicator; private RecyclerView mConnectionsList; private ConnectionListAdapter.Listener mOverScrollListener = new ConnectionListAdapter.Listener() { - @Override - public boolean onLoadBelow(ConnectionListAdapter adapter, int pageToLoad) { - if(pageToLoad > connectionAPI.getPageMax() || pageToLoad < connectionAPI.getPageMin()) { + private boolean isLoadablePage(int pageToLoad) { + return pageToLoad <= connectionAPI.getPageMax() && pageToLoad >= connectionAPI.getPageMin(); + } + + private boolean loadPage(int pageToLoad) { + if(!isLoadablePage(pageToLoad)) { return false; } - Log.d(TAG, "on scrolled below"); Message message = backgroundHandler.obtainMessage(MESSAGE_QUERY_CONNECTION_PAGE); message.obj = getConnectionQuery(); message.arg1 = pageToLoad; @@ -65,17 +67,14 @@ public boolean onLoadBelow(ConnectionListAdapter adapter, int pageToLoad) { return true; } + @Override + public boolean onLoadBelow(ConnectionListAdapter adapter, int pageToLoad) { + return loadPage(pageToLoad); + } + @Override public boolean onLoadAbove(ConnectionListAdapter adapter, int pageToLoad) { - if(pageToLoad > connectionAPI.getPageMax() || pageToLoad < connectionAPI.getPageMin()) { - return false; - } - Log.d(TAG, "on scrolled above"); - Message message = backgroundHandler.obtainMessage(MESSAGE_QUERY_CONNECTION_PAGE); - message.obj = getConnectionQuery(); - message.arg1 = pageToLoad; - backgroundHandler.sendMessage(message); - return true; + return loadPage(pageToLoad); } }; private RecyclerView.OnScrollListener mConnectionListScrollListener; diff --git a/app/src/main/java/ch/unstable/ost/api/model/PassingCheckpoint.java b/app/src/main/java/ch/unstable/ost/api/model/PassingCheckpoint.java index 4a7f186..570316e 100644 --- a/app/src/main/java/ch/unstable/ost/api/model/PassingCheckpoint.java +++ b/app/src/main/java/ch/unstable/ost/api/model/PassingCheckpoint.java @@ -3,6 +3,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import com.google.common.base.Objects; @@ -29,7 +30,7 @@ public PassingCheckpoint[] newArray(int size) { private final Date departureTime; private final Date arrivalTime; - public PassingCheckpoint(Date arrivalTime, Date departureTime, Location location, String platform) { + public PassingCheckpoint(Date arrivalTime, Date departureTime, Location location, @Nullable String platform) { super(platform, location); this.arrivalTime = checkNotNull(arrivalTime, "arrivalTime"); this.departureTime = checkNotNull(departureTime, "departureTime"); diff --git a/app/src/main/java/ch/unstable/ost/api/model/Section.java b/app/src/main/java/ch/unstable/ost/api/model/Section.java index 0fa5967..43137b5 100644 --- a/app/src/main/java/ch/unstable/ost/api/model/Section.java +++ b/app/src/main/java/ch/unstable/ost/api/model/Section.java @@ -13,7 +13,10 @@ import static ch.unstable.ost.utils.ParcelUtils.writeNonNullTypedObject; import static com.google.common.base.Preconditions.checkNotNull; - +/** + * + * TODO: check if the stops contain the arrival and departure + */ public class Section implements Parcelable { public static final Creator
CREATOR = new Creator
() { diff --git a/app/src/main/java/ch/unstable/ost/api/search/SearchAPI.java b/app/src/main/java/ch/unstable/ost/api/search/SearchAPI.java index 25da05e..e0d1cf7 100644 --- a/app/src/main/java/ch/unstable/ost/api/search/SearchAPI.java +++ b/app/src/main/java/ch/unstable/ost/api/search/SearchAPI.java @@ -8,26 +8,55 @@ import java.io.IOException; import java.lang.reflect.Type; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; import ch.unstable.ost.api.StationsDAO; import ch.unstable.ost.api.base.BaseHttpJsonAPI; +import ch.unstable.ost.api.model.Connection; +import ch.unstable.ost.api.model.ConnectionQuery; import ch.unstable.ost.api.model.Location; +import ch.unstable.ost.api.model.PassingCheckpoint; +import ch.unstable.ost.api.model.Section; +import ch.unstable.ost.api.search.types.ConnectionDeserializer; import ch.unstable.ost.api.search.types.LocationDeserializer; +import ch.unstable.ost.api.search.types.PassingCheckpointsDeserializer; import ch.unstable.ost.api.search.types.SearchCHIconClassDeserializer; +import ch.unstable.ost.api.search.types.SectionsDeserializer; +import ch.unstable.ost.api.transport.ConnectionAPI; import io.mikael.urlbuilder.UrlBuilder; import static ch.unstable.ost.api.model.Location.StationType; import static com.google.common.base.Preconditions.checkNotNull; -public class SearchAPI extends BaseHttpJsonAPI implements StationsDAO { +public class SearchAPI extends BaseHttpJsonAPI implements StationsDAO, ConnectionAPI { private final static String BASE_URI = "https://timetable.search.ch/api/"; private final static String COMPLETION_URL = BASE_URI + "completion.json"; + private final static String CONNECTIONS_URL = BASE_URI + "route.json"; + private static final TimeZone TIME_ZONE = TimeZone.getTimeZone("Europe/Berlin"); + + private static UrlBuilder addURLDate(UrlBuilder uriBuilder, Date date) { + checkNotNull(uriBuilder, "uriBuilder"); + checkNotNull(date, "date"); + SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm", Locale.ROOT); + timeFormat.setTimeZone(TIME_ZONE); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.ROOT); + dateFormat.setTimeZone(TIME_ZONE); + return uriBuilder + .addParameter("time", timeFormat.format(date)) + .addParameter("date", dateFormat.format(date)); + } @Override protected void onBuildGsonCreated(GsonBuilder gsonBuilder) { - gsonBuilder.registerTypeAdapter(StationType.class, new SearchCHIconClassDeserializer()); + gsonBuilder.registerTypeAdapter(Section[].class, new SectionsDeserializer()); + gsonBuilder.registerTypeAdapter(Connection.class, new ConnectionDeserializer()); + gsonBuilder.registerTypeAdapter(StationType.class, SearchCHIconClassDeserializer.INSTANCE); + gsonBuilder.registerTypeAdapter(PassingCheckpoint.class, PassingCheckpointsDeserializer.INSTANCE); gsonBuilder.registerTypeAdapter(Location.class, LocationDeserializer.INSTANCE); } @@ -52,7 +81,6 @@ public Location[] getStationsByQuery(String query, @Nullable StationType[] types return locationCompletions.toArray(new Location[locationCompletions.size()]); } - private ArrayList filterResults(ArrayList completions, final StationType[] filter) { final int mask = StationType.getMask(filter); ArrayList filtered = new ArrayList<>(completions.size()); @@ -63,4 +91,34 @@ private ArrayList filterResults(ArrayList completions, final } return filtered; } + + @Override + public int getPageMax() { + return 0; + } + + @Override + public int getPageMin() { + return 0; + } + + @Override + public Connection[] getConnections(ConnectionQuery connectionQuery, int page) throws IOException { + UrlBuilder builder = UrlBuilder.fromString(CONNECTIONS_URL) + .addParameter("from", connectionQuery.getFrom()) + .addParameter("to", connectionQuery.getTo()); + + if (connectionQuery.getArrivalTime() != null) { + builder = addURLDate(builder, connectionQuery.getArrivalTime()); + builder = builder.addParameter("time_type", "arrival"); + } else if (connectionQuery.getDepartureTime() != null) { + builder = addURLDate(builder, connectionQuery.getDepartureTime()); + } + ConnectionsList connectionsList = loadJson(builder.toUrl(), ConnectionsList.class); + return connectionsList.connections; + } + + private static class ConnectionsList { + public Connection[] connections; + } } diff --git a/app/src/main/java/ch/unstable/ost/api/search/types/ConnectionDeserializer.java b/app/src/main/java/ch/unstable/ost/api/search/types/ConnectionDeserializer.java new file mode 100644 index 0000000..0cd64d7 --- /dev/null +++ b/app/src/main/java/ch/unstable/ost/api/search/types/ConnectionDeserializer.java @@ -0,0 +1,21 @@ +package ch.unstable.ost.api.search.types; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import java.lang.reflect.Type; + +import ch.unstable.ost.api.model.Connection; +import ch.unstable.ost.api.model.Section; + +public class ConnectionDeserializer implements JsonDeserializer { + @Override + public Connection deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject connectionObj = json.getAsJsonObject(); + Section[] sections = context.deserialize(connectionObj.get("legs"), Section[].class); + return new Connection(sections); + } +} diff --git a/app/src/main/java/ch/unstable/ost/api/search/types/PassingCheckpointsDeserializer.java b/app/src/main/java/ch/unstable/ost/api/search/types/PassingCheckpointsDeserializer.java new file mode 100644 index 0000000..435ec01 --- /dev/null +++ b/app/src/main/java/ch/unstable/ost/api/search/types/PassingCheckpointsDeserializer.java @@ -0,0 +1,82 @@ +package ch.unstable.ost.api.search.types; + +import android.support.annotation.Nullable; +import android.util.Log; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import java.lang.reflect.Type; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; +import java.util.logging.Level; +import java.util.logging.Logger; + +import ch.unstable.ost.api.model.Location; +import ch.unstable.ost.api.model.PassingCheckpoint; +import ch.unstable.ost.utils.LogUtils; + +public enum PassingCheckpointsDeserializer implements JsonDeserializer { + INSTANCE; + + private static final String TAG = "PassingCPDeserializer"; + private static final Logger LOGGER = Logger.getLogger(TAG); + + @Nullable + public static Date getDate(SimpleDateFormat dateFormat, JsonObject object, String name) { + if (!object.has(name)) { + return null; + } + JsonElement dateField = object.get(name); + if (dateField.isJsonNull()) { + return null; + } + try { + return dateFormat.parse(dateField.getAsString()); + } catch (ParseException e) { + throw new JsonParseException("Unable to parse departure date", e); + } + } + + public static SimpleDateFormat getDateFormat() { + SimpleDateFormat departureFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.ROOT); + departureFormat.setTimeZone(TimeZone.getTimeZone("Europe/Berlin")); + return departureFormat; + } + + @Override + public PassingCheckpoint deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + SimpleDateFormat dateFormat = getDateFormat(); + JsonObject object = json.getAsJsonObject(); + String stopId = object.get("stopid").getAsString(); + String name = object.get("name").getAsString(); + Date departure = getDate(dateFormat, object, "departure"); + Date arrival = getDate(dateFormat, object, "arrival"); + + if(departure == null && arrival != null) { + Log.w(TAG, "Departure is null"); + departure = arrival; + } else if(arrival == null && departure != null) { + Log.w(TAG, "Arrival is null"); + arrival = departure; + } else if(arrival == null && departure == null) { + LOGGER.log(Level.WARNING, "neither arrival nor departure is set: " + LogUtils.prettyJson(object)); + Log.w(TAG, "neither arrival nor departure is set: " + LogUtils.prettyJson(object)); + return null; + } + + Location location = new Location(name, Location.StationType.UNKNOWN, stopId); + // TODO: Find out if track is sent + String platform = null; + if(object.has("track")) { + platform = object.get("track").getAsString(); + } + return new PassingCheckpoint(arrival, departure, location, platform); + } +} diff --git a/app/src/main/java/ch/unstable/ost/api/search/types/SearchCHIconClassDeserializer.java b/app/src/main/java/ch/unstable/ost/api/search/types/SearchCHIconClassDeserializer.java index 275f5be..8060d81 100644 --- a/app/src/main/java/ch/unstable/ost/api/search/types/SearchCHIconClassDeserializer.java +++ b/app/src/main/java/ch/unstable/ost/api/search/types/SearchCHIconClassDeserializer.java @@ -12,7 +12,8 @@ import ch.unstable.ost.api.model.Location; -public class SearchCHIconClassDeserializer implements JsonDeserializer { +public enum SearchCHIconClassDeserializer implements JsonDeserializer { + INSTANCE; private static final String TAG = SearchCHIconClassDeserializer.class.getSimpleName(); @Override diff --git a/app/src/main/java/ch/unstable/ost/api/search/types/SectionsDeserializer.java b/app/src/main/java/ch/unstable/ost/api/search/types/SectionsDeserializer.java new file mode 100644 index 0000000..c48002a --- /dev/null +++ b/app/src/main/java/ch/unstable/ost/api/search/types/SectionsDeserializer.java @@ -0,0 +1,102 @@ +package ch.unstable.ost.api.search.types; + +import android.support.annotation.NonNull; +import android.util.Log; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Type; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.logging.Logger; + +import ch.unstable.ost.api.model.ArrivalCheckpoint; +import ch.unstable.ost.api.model.DepartureCheckpoint; +import ch.unstable.ost.api.model.Location; +import ch.unstable.ost.api.model.PassingCheckpoint; +import ch.unstable.ost.api.model.Route; +import ch.unstable.ost.api.model.Section; +import ch.unstable.ost.utils.LogUtils; +import ch.unstable.ost.utils.TimeDateUtils; + +import static ch.unstable.ost.api.transport.types.LocationDeserializer.getNullableString; + +public class SectionsDeserializer implements JsonDeserializer { + private static final String TAG = "SectionsDeserializer"; + private static final Logger LOGGER = Logger.getLogger(TAG); + + @NonNull + private static Location getLocation(JsonObject jsonObject) { + String name = jsonObject.get("sbb_name").getAsString(); + String id = jsonObject.get("stopid").getAsString(); + return new Location(name, Location.StationType.UNKNOWN, id); + } + + private static ArrivalCheckpoint getArrival(SimpleDateFormat dateFormat, JsonObject object) { + JsonObject exit = object.get("exit").getAsJsonObject(); + Date arrivalTime = PassingCheckpointsDeserializer.getDate(dateFormat, exit, "arrival"); + Location arrivalLocation = getLocation(exit); + String arrivalPlatform = getNullableString(exit, "track"); + return new ArrivalCheckpoint(arrivalTime, arrivalPlatform, arrivalLocation); + } + + public static void removeNulls(List list) { + for (Iterator iterator = list.iterator(); iterator.hasNext(); ) { + if(iterator.next() == null) { + iterator.remove(); + } + } + } + + @Override + public Section[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + SimpleDateFormat dateFormat = PassingCheckpointsDeserializer.getDateFormat(); + JsonArray jsonArray = json.getAsJsonArray(); + ArrayList
sections = new ArrayList<>(jsonArray.size()); + for(JsonElement element: jsonArray) { + JsonObject object = element.getAsJsonObject(); + String type = ""; + if(!object.has("type")) { + LOGGER.severe("No type defined in " + LogUtils.prettyJson(object) + " \n Parent: " + LogUtils.prettyJson(json)); + // can be ignored probably + continue; + } else { + type = object.get("type").getAsString(); + } + + if(type.equals("walk")) { + Log.d(TAG, "Walk not handled: " + LogUtils.prettyJson(object)); + LOGGER.fine("Walk not handled: " + LogUtils.prettyJson(object)); + } else { + Log.d(TAG, "Got type " + type + ": " + LogUtils.prettyJson(object)); + LOGGER.fine("Got type " + type + ": " + LogUtils.prettyJson(object)); + String shortname = object.get("line").getAsString(); + String longName = object.get("number").getAsString(); + Type listOfPassingCheckpoints = new TypeToken>(){}.getType(); + List stops = context.deserialize(object.get("stops"), listOfPassingCheckpoints); + removeNulls(stops); + Route route = new Route(shortname, longName, stops.toArray(new PassingCheckpoint[stops.size()])); + + Location departureLocation = getLocation(object); + Date departureTime = PassingCheckpointsDeserializer.getDate(dateFormat, object, "departure"); + String platform = getNullableString(object, "track"); + DepartureCheckpoint departure = new DepartureCheckpoint(departureTime, platform, departureLocation); + + ArrivalCheckpoint arrival = getArrival(dateFormat, object); + String headSign = object.get("terminal").getAsString(); + Section section = new Section(route, departure, arrival, headSign, 0); + sections.add(section); + } + } + return sections.toArray(new Section[sections.size()]); + } +} diff --git a/app/src/main/java/ch/unstable/ost/api/transport/types/LocationDeserializer.java b/app/src/main/java/ch/unstable/ost/api/transport/types/LocationDeserializer.java index d84d321..858bfd5 100644 --- a/app/src/main/java/ch/unstable/ost/api/transport/types/LocationDeserializer.java +++ b/app/src/main/java/ch/unstable/ost/api/transport/types/LocationDeserializer.java @@ -19,6 +19,7 @@ public enum LocationDeserializer implements JsonDeserializer { @Nullable public static String getNullableString(JsonObject parent, String field) { + if(!parent.has(field)) return null; JsonElement value = parent.get(field); if (value.isJsonNull()) { return null; diff --git a/app/src/test/java/ch/unstable/ost/api/search/SearchAPITest.java b/app/src/test/java/ch/unstable/ost/api/search/SearchAPITest.java index 34e9700..ce32e70 100644 --- a/app/src/test/java/ch/unstable/ost/api/search/SearchAPITest.java +++ b/app/src/test/java/ch/unstable/ost/api/search/SearchAPITest.java @@ -1,10 +1,60 @@ package ch.unstable.ost.api.search; +import org.junit.Before; +import org.junit.Test; + +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +import ch.unstable.ost.api.model.Connection; +import ch.unstable.ost.api.model.ConnectionQuery; + +import static ch.unstable.ost.api.transport.TransportAPITest.ONE_HOURS_IN_MILLIES; +import static ch.unstable.ost.api.transport.TransportAPITest.assertConnectionsSortedByDeparture; +import static ch.unstable.ost.api.transport.TransportAPITest.getLast; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.Assert.assertTrue; public class SearchAPITest { + private SearchAPI searchApi; + + @Before + public void setUp() { + searchApi = new SearchAPI(); + } + + private static Date getDate(int year, int month, int day, int hour, int minute) { + Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("Europe/Zurich")); + calendar.set(year, month, day, hour, minute); + return calendar.getTime(); + } + + @Test + public void getConnections() throws Exception { + Date date = getDate(2017,7,7,10,0); + System.out.println(date); + ConnectionQuery query = new ConnectionQuery.Builder() + .setFrom("Bern") + .setTo("Basel SBB") + .addVia("Genf") + .setArrivalTime(date) + .build(); + + Connection[] connections = searchApi.getConnections(query, 0); + assertEquals(connections.length, 4); + assertConnectionsSortedByDeparture(connections); + for(Connection connection: connections) { + System.out.println(connection.getArrivalDate()); + } + + // The last connection should be after the desired arrival date + Connection lastConnection = getLast(connections); + assertTrue(lastConnection.getArrivalDate().getTime() > date.getTime()); + assertTrue(lastConnection.getArrivalDate().getTime() < (date.getTime() + ONE_HOURS_IN_MILLIES)); + } + } \ No newline at end of file diff --git a/app/src/test/java/ch/unstable/ost/api/transport/TransportAPITest.java b/app/src/test/java/ch/unstable/ost/api/transport/TransportAPITest.java index a30495a..5a61028 100644 --- a/app/src/test/java/ch/unstable/ost/api/transport/TransportAPITest.java +++ b/app/src/test/java/ch/unstable/ost/api/transport/TransportAPITest.java @@ -25,21 +25,21 @@ public void setUp() { transportApi = new TransportAPI(); } - private static Date getDate(int year, int month, int day, int hour, int minute) { + public static Date getDate(int year, int month, int day, int hour, int minute) { Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("Europe/Zurich")); calendar.set(year, month, day, hour, minute); return calendar.getTime(); } - private E getFirst(E[] entities) { + public static E getFirst(E[] entities) { return entities[0]; } - private E getLast(E[] entities) { + public static E getLast(E[] entities) { return entities[entities.length - 1]; } - private static void assertConnectionsSortedByDeparture(Connection[] connections) { + public static void assertConnectionsSortedByDeparture(Connection[] connections) { assertIsSorted(connections, new Comparator() { @Override public int compare(Connection o1, Connection o2) {