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

johnp
Running the examples in chapter 5 c under pytest 5.4.1 causes an AttributeError: ‘module’ object has no attribute ‘config’. In particula...
New
GilWright
Working through the steps (checking that the Info,plist matches exactly), run the demo game and what appears is grey but does not fill th...
New
mikecargal
Title: Hands-On Rust (Chap 8 (Adding a Heads Up Display) It looks like ​.with_simple_console_no_bg​(SCREEN_WIDTH*2, SCREEN_HEIGHT*2...
New
herminiotorres
Hi @Margaret , On page VII the book tells us the example and snippets will be all using Elixir version 1.11 But on page 3 almost the en...
New
gilesdotcodes
In case this helps anyone, I’ve had issues setting up the rails source code. Here were the solutions: In Gemfile, change gem 'rails' t...
New
leba0495
Hello! Thanks for the great book. I was attempting the Trie (chap 17) exercises and for number 4 the solution provided for the autocorre...
New
fynn
This is as much a suggestion as a question, as a note for others. Locally the SGP30 wasn’t available, so I ordered a SGP40. On page 53, ...
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
EdBorn
Title: Agile Web Development with Rails 7: (page 70) I am running windows 11 pro with rails 7.0.3 and ruby 3.1.2p20 (2022-04-12 revision...
New
SlowburnAZ
Getting an error when installing the dependencies at the start of this chapter: could not compile dependency :exla, "mix compile" failed...
New

Other popular topics Top

PragmaticBookshelf
Andy and Dave wrote this influential, classic book to help their clients create better software and rediscover the joy of coding. Almost ...
New
dasdom
No chair. I have a standing desk. This post was split into a dedicated thread from our thread about chairs :slight_smile:
New
AstonJ
You might be thinking we should just ask who’s not using VSCode :joy: however there are some new additions in the space that might give V...
New
Rainer
My first contact with Erlang was about 2 years ago when I used RabbitMQ, which is written in Erlang, for my job. This made me curious and...
New
Exadra37
Oh just spent so much time on this to discover now that RancherOS is in end of life but Rancher is refusing to mark the Github repo as su...
New
Maartz
Hi folks, I don’t know if I saw this here but, here’s a new programming language, called Roc Reminds me a bit of Elm and thus Haskell. ...
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
PragmaticBookshelf
25 puzzles that will make you a better C programmer by challenging your knowledge of the language, and explaining the technical details o...
New
PragmaticBookshelf
Escape callback hell and ship fast, clean code that reads as smoothly as it runs. Squash bugs and stamp out memory leaks with an intuitiv...
New
mindriot
Ok, well here are some thoughts and opinions on some of the ergonomic keyboards I have, I guess like mini review of each that I use enoug...
New

Latest in Functional Programming in Java, Second Edition

Functional Programming in Java, Second Edition Portal ❯

Sub Categories: