dtonhofer

dtonhofer

Functional Programming in Java, Second Edition: Chapter 10, p.168: Retrieving Airport Info, new code!

As for FinanceData on page 91, the code for AirportInfo uses the suspect and soon-to-be-deprecated java.net.URL. Let’s use java.net.URI instead.

Some additional info on the (apparently RESTful web service) we are interrogating here:

This is the Federal Aviation Administration (FAA) Airport Status Web Service"

https://catalog.data.faa.gov/about

I have not found documentation at the FAA but the service is described at “swaggerhub” ( “SwaggerHub is a collaborative platform for defining your APIs using the [OpenAPI Specification], and managing them throughout their lifecycle.”)

More precisely the API element we use is described as follows:

Get airport status based on path parameter provided on the API call. The path parameter is an IATA airport code.

Experimentally, it returns JSON rather than XML.

Apparently this is only for a (restricted?) set of US airports. As lack would have it, I tried IATA code LUX for Findel airport, Luxembourg, and it gives me back information for US airport identified by the FAA identifier (not a IATA code) LUX, so there is at least something not according to specification in there.

If one accesses the webservice, which can only be done via https, the handshake may fail with an IOException (more precisely an SSLHandshakeException). There may be an anti-DOS device at the webserver. But the Java truststore contains the needed trusted root certificate by default, which is:

  s:C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA
   i:C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA
   a:PKEY: rsaEncryption, 2048 (bit); sigalg: RSA-SHA1
   v:NotBefore: Nov 10 00:00:00 2006 GMT; NotAfter: Nov 10 00:00:00 2031 GMT

To test the TLS handshake on Linux:

echo "Q" | openssl s_client -no_ssl3 -connect "soa.smext.faa.gov:443" -prexit -state -showcerts > OUT.txt 2>&1

See man openssl s_client for more.

Instead of a simple string search, we shall also perform a proper JSON parse using com.fasterxml.jackson which is easy to implement.

We need to add the following to the Maven POM and we are good to go:

        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml -->
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-yaml</artifactId>
            <version>2.15.0</version>
        </dependency>

This allows us to use

https://javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/2.9.8/com/fasterxml/jackson/databind/ObjectMapper.html

etc. One may note that this library seems to have traded elegance through proper subclassing for speed.

Chapter10_Airport.java

Here we go. This comes with several test cases and a “simple” monadic approach at handling exceptions in a stream, similar to the one in the book, but the latter is more general.

package chapter10;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.stream.Collectors;

import static chapter10.Exceptional.*;
import static java.util.stream.Collectors.*;

public class Chapter10_Airport {

    // A specific exception thrown by getNameOfAirport()
    // "exceptionhandling/fpij/AirportInfoException.java" p.169

    public static class AirportInfoException extends Exception {
        public AirportInfoException(String message) {
            super(message);
        }
    }

    // The method to perform a web retrieval
    // "exceptionhandling/fpij/AirportInfo.java" p.168 (but unpacked from the surrounding "AirportInfo" class)

    private static String getNameOfAirport(final String iata) throws IOException, AirportInfoException {
        HttpResponse<String> response;
        try {
            final String scheme = "https";
            final String authority = "soa.smext.faa.gov";
            final String path = "/asws/api/airport/status/" + iata;
            final String query = null;
            final String fragment = null;
            final URI uri = new URI(scheme, authority, path, query, fragment);
            System.out.println("Connecting to URI: " + uri);
            HttpClient client = HttpClient.newHttpClient();
            HttpRequest request = HttpRequest.newBuilder().uri(uri).build();
            response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
        } catch (IOException ioex) {
            throw ioex;
        } catch (Exception other) {
            throw new RuntimeException(other);
        }
        System.out.println("Received status code: " + response.statusCode());
        if (response.statusCode() == 200) {
            // The response is JSON, let's parse it with the Jackson library
            ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
            JsonNode jnode = objectMapper.readTree(response.body());
            // Printout of the whole JSON tree (not really needed)
            {
                System.out.println(JsonHelpers.describeNode(jnode));
            }
            // Collect
            if (jnode.getNodeType() != JsonNodeType.OBJECT) {
                throw new AirportInfoException("JSON decode: Topmost node is not an 'object'");
            }
            if (jnode.get("Name") == null) {
                throw new AirportInfoException("JSON decode: No 'name' found at expected place");
            }
            if (jnode.get("Name").getNodeType() != JsonNodeType.STRING) {
                throw new AirportInfoException("JSON decode: The 'name' node is not a string value");
            }
            return jnode.get("Name").asText();
        } else {
            throw new IllegalStateException("Status code was: " + response.statusCode());
        }
    }

    // ---
    // A record carrying the result of a web request.
    // We discriminate using an enum instead of just a boolean for readability.
    // ---

    record ResultOrFailure(String iataCode, String name, Exception ex) {

        public enum What {Result, Failure}

        public ResultOrFailure(String iataCode, String name) {
            this(iataCode, name, null);
        }

        public ResultOrFailure(String iataCode, Exception ex) {
            this(iataCode, null, ex);
        }

        public ResultOrFailure(String iataCode, String name, Exception ex) {
            if (iataCode == null || ((name == null) && (ex == null))) {
                throw new IllegalArgumentException();
            }
            this.iataCode = iataCode;
            this.name = name;
            this.ex = ex;
        }

        public What getWhat() {
            return (this.ex() == null) ? What.Result : What.Failure;
        }

        public boolean isResult() {
            return getWhat() == What.Result;
        }

        public boolean isFailure() {
            return getWhat() == What.Failure;
        }

        public String toString() {
            if (getWhat() == What.Result) {
                return iataCode + " = '" + name + "'";
            } else {
                return iataCode + " =>> [" + ex.getMessage() + "]";
            }
        }

        public ResultOrFailure transform(Function<String, String> transformer) {
            if (getWhat() == What.Result) {
                try {
                    return new ResultOrFailure(this.iataCode, transformer.apply(this.name));
                } catch (Exception ex) {
                    return new ResultOrFailure(this.iataCode, ex);
                }
            } else {
                return this;
            }
        }

    }

    // Printout with some dexterous stream processing!
    // whereby the result is partitioned into "error" and "success" lists.

    private static void printResults(final List<ResultOrFailure> results) {
        final Map<ResultOrFailure.What, List<String>> printy = results.stream()
                .collect(groupingBy(
                        ResultOrFailure::getWhat,
                        mapping(ResultOrFailure::toString,
                                collectingAndThen(Collectors.toList(), Collections::unmodifiableList))));
        final ResultOrFailure.What succ = ResultOrFailure.What.Result;
        final ResultOrFailure.What fail = ResultOrFailure.What.Failure;
        if (printy.containsKey(succ)) {
            System.out.println("Obtained names:\n" + String.join("\n", printy.get(succ)));
        }
        if (printy.containsKey(fail)) {
            System.out.println("Errors:\n" + String.join("\n", printy.get(fail)));
        }
    }

    // "exceptionhandling/fpij/AirportNames.java" p.169

    @Test
    void retrieveSingleAirportName() throws AirportInfoException, IOException {
        final String iataCode = "AUS";
        final String name = getNameOfAirport(iataCode);
        System.out.println("Obtained name of airport with IATA code " + iataCode + ": '" + name + "'");
    }

    // Airports to retrieve. "IHA" is an invalid code, so gives rise to an error during fetch.

    private final List<String> iataCodes = List.of("AUS", "DFW", "HOU", "IHA", "SAT");

    // "exceptionhandling/fpij/AirportNames.java" p.169, slightly modified
    // This version commingles successfully retrieved data and error messages in a single result list.
    // Output:
    // AUSTIN-BERGSTROM INTL
    // DALLAS-FORT WORTH INTL
    // WILLIAM P HOBBY
    // Error: JSON decode: No 'name' found at expected place
    // SAN ANTONIO INTL

    @Test
    void retrieveMultipleAirportNames_imperativeLousy() {
        List<String> names = new ArrayList<>();
        for (var iataCode : iataCodes) {
            try {
                names.add(getNameOfAirport(iataCode).toUpperCase());
            } catch (IOException | AirportInfoException ex) {
                names.add("Error: " + ex.getMessage());
            }
        }
        System.out.println(String.join("\n", names));
    }

    // As above, but we have a more complex result than just "String",
    // namely instances of "ResultOrError", which can be processed for much better output.
    // Output:
    // Obtained names:
    // AUS = 'AUSTIN-BERGSTROM INTL'
    // DFW = 'DALLAS-FORT WORTH INTL'
    // HOU = 'WILLIAM P HOBBY'
    // SAT = 'SAN ANTONIO INTL'
    // Errors:
    // IHA =>> [JSON decode: No 'name' found at expected place]

    @Test
    void retrieveMultipleAirportNames_imperativeBetter() {
        final List<ResultOrFailure> results = new ArrayList<>();
        for (var iataCode : iataCodes) {
            try {
                results.add(new ResultOrFailure(iataCode, getNameOfAirport(iataCode).toUpperCase()));
            } catch (IOException | AirportInfoException ex) {
                results.add(new ResultOrFailure(iataCode, ex));
            }
        }
        printResults(results);
    }

    // Code on p.171
    // Note that unlike retrieveMultipleAirportNames_imperative()
    // this code raises an Exception on first error (instead of
    // printing an error and continuing). So there may be no output at all.

    @Test
    void retrieveMultipleAirportNames_streamPipelineBad() {
        final List<String> names =
                iataCodes.stream()
                        .map(iataCode -> {
                            try {
                                return getNameOfAirport(iataCode);
                            } catch (Exception ex) {
                                // Ripping down everything like this is at the least inelegant
                                throw new RuntimeException(ex);
                            }
                        })
                        .map(String::toUpperCase)
                        .toList();
        System.out.println(String.join("\n", names));
    }

    // "Dealing with it downstream"
    // My own take, with no generality at all.
    // The "ResultOrError" structure is carried along the stream.
    // It can even support additional exceptions inside the "transform()"
    // call, becoming an "error" object with the latest exception
    // carried along.

    @Test
    void retrieveMultipleAirportNames_streamPipelineMonadic() {
        final List<ResultOrFailure> results =
                iataCodes.stream()
                        .map(iataCode -> {
                            try {
                                String name = getNameOfAirport(iataCode);
                                return new ResultOrFailure(iataCode, name);
                            } catch (Exception ex) {
                                // this will still let any "Throwable" up the stack
                                return new ResultOrFailure(iataCode, ex);
                            }
                        })
                        .map(it -> it.transform(String::toUpperCase))
                        .toList();
        printResults(results);
    }

    // If we want to "break" the pipeline at the first exception (but the information
    // about the exception is lost)
    // Output:
    // Obtained names:
    // AUS = 'AUSTIN-BERGSTROM INTL'
    // DFW = 'DALLAS-FORT WORTH INTL'
    // HOU = 'WILLIAM P HOBBY'

    @Test
    void retrieveMultipleAirportNames_streamPipelineMonadicTerminateAsap() {
        final List<ResultOrFailure> results =
                iataCodes.stream()
                        .map(iataCode -> {
                            try {
                                String name = getNameOfAirport(iataCode);
                                return new ResultOrFailure(iataCode, name);
                            } catch (Exception ex) {
                                // this will still let any "Throwable" up the stack
                                return new ResultOrFailure(iataCode, ex);
                            }
                        })
                        .map(it -> it.transform(String::toUpperCase))
                        .takeWhile(ResultOrFailure::isResult)
                        .toList();
        printResults(results);
    }

    // If we want to "break" the pipeline at the first exception (but the information
    // about the exception is kept; if this is run in parallel, several exceptions in
    // sequence may be left through .. I think.
    // Output:
    // Obtained names:
    // AUS = 'AUSTIN-BERGSTROM INTL'
    // DFW = 'DALLAS-FORT WORTH INTL'
    // HOU = 'WILLIAM P HOBBY'
    // SAT = 'SAN ANTONIO INTL'
    // Errors:
    // IHA =>> [JSON decode: No 'name' found at expected place]

    @Test
    void retrieveMultipleAirportNames_streamPipelineMonadicTerminateAsapButKeepFirstException() {
        final AtomicBoolean goodStream = new AtomicBoolean(true);
        final List<ResultOrFailure> results =
                iataCodes.stream()
                        .map(iataCode -> {
                            try {
                                String name = getNameOfAirport(iataCode);
                                return new ResultOrFailure(iataCode, name);
                            } catch (Exception ex) {
                                return new ResultOrFailure(iataCode, ex);
                            }
                        })
                        .map(it -> it.transform(String::toUpperCase))
                        .takeWhile(it -> goodStream.get())
                        .peek(it -> {
                            // interrogate & modify "global state"
                            if (it.isFailure()) {
                                goodStream.set(false);
                            }
                        })
                        .toList();
        printResults(results);
    }
}

JsonHelpers.java

This is going too far for the purpose of the book, but if one wants to print the whole JSON tree received from the FAA, this code is handy:

package chapter10;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

class JsonHelpers {

    private static String buildIndentString(int indent) {
        return IntStream.range(0, indent).mapToObj(it -> " ").collect(Collectors.joining());
    }

    private static void addSubDescToBuf(StringBuilder buf, String subDesc, String heading) {
        String[] lines = subDesc.split("\n");
        assert lines.length > 0;
        buf.append(heading);
        buf.append(lines[0]);
        for (int j = 1; j < lines.length; j++) {
            buf.append("\n");
            buf.append(buildIndentString(heading.length()));
            buf.append(lines[j]);
        }
    }

    private static String getSuffix(int counter) {
        return (counter == 1) ? "" : "s";
    }

    private static int countElements(JsonNode node) {
        int counter = 0;
        for (Iterator<JsonNode> it = node.elements(); it.hasNext(); it.next()) {
            counter++;
        }
        assert counter == node.size();
        return counter;
    }

    private static List<String> collectFieldNames(JsonNode node) {
        assert node.isObject();
        List<String> res = new LinkedList<>();
        for (Iterator<Map.Entry<String, JsonNode>> it = node.fields(); it.hasNext(); ) {
            res.add(it.next().getKey());
        }
        return res;
    }

    private static String describeArray(JsonNode node) {
        StringBuilder buf = new StringBuilder();
        int count = countElements(node);
        buf.append("== Array of size " + count + " ==");
        for (int i = 0; i < count; i++) {
            buf.append("\n");
            String heading = "[" + i + "] ";
            String subDesc = describeNode(node.get(i));
            addSubDescToBuf(buf, subDesc, heading);
        }
        return buf.toString();
    }

    private static String describeObject(JsonNode node) {
        StringBuilder buf = new StringBuilder();
        List<String> fieldNames = collectFieldNames(node);
        int count = fieldNames.size();
        buf.append("== Object of size " + count + " ==");
        for (String fieldName : fieldNames) {
            buf.append("\n");
            String heading = "[" + fieldName + "] ";
            String subDesc = describeNode(node.get(fieldName));
            addSubDescToBuf(buf, subDesc, heading);
        }
        return buf.toString();
    }

    public static String describeNode(JsonNode node) {
        StringBuilder buf = new StringBuilder();
        if (node == null) {
            buf.append("(null)");
        } else if (node.isValueNode()) {
            assert !node.isContainerNode();
            assert !node.isArray();
            assert !node.isObject();
            if (node.getNodeType() == JsonNodeType.STRING) {
                buf.append("'");
                buf.append(node.asText());
                buf.append("'");
            } else {
                buf.append(node.asText());
            }
            buf.append(" (" + node.getNodeType().toString().toLowerCase() + ")");
        } else {
            assert node.isContainerNode();
            assert !node.isValueNode();
            assert node.isArray() == !node.isObject(); // XOR
            if (node.isArray()) {
                buf.append(describeArray(node));
            } else {
                buf.append(describeObject(node));
            }
        }
        return buf.toString();
    }

}

Running it on “AUS”

Connecting to URI: https://soa.smext.faa.gov/asws/api/airport/status/AUS
Received status code: 200
== Object of size 10 ==
[Name] 'Austin-bergstrom Intl' (string)
[City] 'Austin' (string)
[State] 'TX' (string)
[ICAO] 'KAUS' (string)
[IATA] 'AUS' (string)
[SupportedAirport] false (boolean)
[Delay] false (boolean)
[DelayCount] 0 (number)
[Status] == Array of size 1 ==
         [0] == Object of size 1 ==
             [Reason] 'No known delays for this airport' (string)
[Weather] == Object of size 5 ==
          [Weather] == Array of size 1 ==
                    [0] == Object of size 1 ==
                        [Temp] == Array of size 1 ==
                               [0] 'A Few Clouds' (string)
          [Visibility] == Array of size 1 ==
                       [0] 10.0 (number)
          [Meta] == Array of size 1 ==
                 [0] == Object of size 3 ==
                     [Credit] 'NOAA's National Weather Service' (string)
                     [Url] 'https://weather.gov/' (string)
                     [Updated] 'Last Updated on May 18 2023, 5:53 am CDT' (string)
          [Temp] == Array of size 1 ==
                 [0] '62.0 F (16.7 C)' (string)
          [Wind] == Array of size 1 ==
                 [0] 'North at 0.0' (string)
Obtained name of airport with IATA code AUS: 'Austin-bergstrom Intl'

Where Next?

Popular Pragmatic Bookshelf topics Top

iPaul
page 37 ANTLRInputStream input = new ANTLRInputStream(is); as of ANTLR 4 .8 should be: CharStream stream = CharStreams.fromStream(i...
New
rmurray10127
Title: Intuitive Python: docker run… denied error (page 2) Attempted to run the docker command in both CLI and Powershell PS C:\Users\r...
New
patoncrispy
I’m new to Rust and am using this book to learn more as well as to feed my interest in game dev. I’ve just finished the flappy dragon exa...
New
curtosis
Running mix deps.get in the sensor_hub directory fails with the following error: ** (Mix) No SSH public keys found in ~/.ssh. An ssh aut...
New
jgchristopher
“The ProductLive.Index template calls a helper function, live_component/3, that in turn calls on the modal component. ” Excerpt From: Br...
New
digitalbias
Title: Build a Weather Station with Elixir and Nerves: Problem connecting to Postgres with Grafana on (page 64) If you follow the defau...
New
oaklandgit
Hi, I completed chapter 6 but am getting the following error when running: thread 'main' panicked at 'Failed to load texture: IoError(O...
New
AufHe
I’m a newbie to Rails 7 and have hit an issue with the bin/Dev script mentioned on pages 112-113. Iteration A1 - Seeing the list of prod...
New
tkhobbes
After some hassle, I was able to finally run bin/setup, now I have started the rails server but I get this error message right when I vis...
New
redconfetti
Docker-Machine became part of the Docker Toolbox, which was deprecated in 2020, long after Docker Desktop supported Docker Engine nativel...
New

Other popular topics Top

DevotionGeo
I know that these benchmarks might not be the exact picture of real-world scenario, but still I expect a Rust web framework performing a ...
New
PragmaticBookshelf
Design and develop sophisticated 2D games that are as much fun to make as they are to play. From particle effects and pathfinding to soci...
New
DevotionGeo
I know that -t flag is used along with -i flag for getting an interactive shell. But I cannot digest what the man page for docker run com...
New
foxtrottwist
A few weeks ago I started using Warp a terminal written in rust. Though in it’s current state of development there are a few caveats (tab...
New
AstonJ
We’ve talked about his book briefly here but it is quickly becoming obsolete - so he’s decided to create a series of 7 podcasts, the firs...
New
Help
I am trying to crate a game for the Nintendo switch, I wanted to use Java as I am comfortable with that programming language. Can you use...
New
PragmaticBookshelf
Get the comprehensive, insider information you need for Rails 8 with the new edition of this award-winning classic. Sam Ruby @rubys ...
New
PragmaticBookshelf
As digital systems increasingly run the world, mastery of the recurring patterns of software development risk is the key to fast and effe...
New
PragmaticBookshelf
Lint your docs like code: turn any style guide into enforceable rules with Vale and publish clear, consistent content every time. ...
New
PragmaticBookshelf
Quickly turn complex problems into simple, working solutions using functional programming, safe concurrency, and the expressive tools of ...
New

Latest in Functional Programming in Java, Second Edition

Functional Programming in Java, Second Edition Portal

Sub Categories: