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

yulkin
your book suggests to use Image.toByteData() to convert image to bytes, however I get the following error: "the getter ‘toByteData’ isn’t...
New
jdufour
Hello! On page xix of the preface, it says there is a community forum "… for help if your’re stuck on one of the exercises in this book… ...
New
jeremyhuiskamp
Title: Web Development with Clojure, Third Edition, vB17.0 (p9) The create table guestbook syntax suggested doesn’t seem to be accepted ...
New
leonW
I ran this command after installing the sample application: $ cards add do something --owner Brian And got a file not found error: Fil...
New
jskubick
I’m under the impression that when the reader gets to page 136 (“View Data with the Database Inspector”), the code SHOULD be able to buil...
New
jskubick
I found an issue in Chapter 7 regarding android:backgroundTint vs app:backgroundTint. How to replicate: load chapter-7 from zipfile i...
New
Charles
In general, the book isn’t yet updated for Phoenix version 1.6. On page 18 of the book, the authors indicate that an auto generated of ro...
New
brunogirin
When trying to run tox in parallel as explained on page 151, I got the following error: tox: error: argument -p/–parallel: expected one...
New
akraut
The markup used to display the uploaded image results in a Phoenix.LiveView.HTMLTokenizer.ParseError error. lib/pento_web/live/product_l...
New
mcpierce
@mfazio23 I’ve applied the changes from Chapter 5 of the book and everything builds correctly and runs. But, when I try to start a game,...
New

Other popular topics Top

PragmaticBookshelf
Machine learning can be intimidating, with its reliance on math and algorithms that most programmers don't encounter in their regular wor...
New
ohm
Which, if any, games do you play? On what platform? I just bought (and completed) Minecraft Dungeons for my Nintendo Switch. Other than ...
New
siddhant3030
I’m thinking of buying a monitor that I can rotate to use as a vertical monitor? Also, I want to know if someone is using it for program...
New
New
AstonJ
I ended up cancelling my Moonlander order as I think it’s just going to be a bit too bulky for me. I think the Planck and the Preonic (o...
New
DevotionGeo
The V Programming Language Simple language for building maintainable programs V is already mentioned couple of times in the forum, but I...
New
PragmaticBookshelf
Create efficient, elegant software tests in pytest, Python's most powerful testing framework. Brian Okken @brianokken Edited by Kat...
New
hilfordjames
There appears to have been an update that has changed the terminology for what has previously been known as the Taskbar Overflow - this h...
New
AnfaengerAlex
Hello, I’m a beginner in Android development and I’m facing an issue with my project setup. In my build.gradle.kts file, I have the foll...
New
Fl4m3Ph03n1x
Background Lately I am in a quest to find a good quality TTS ai generation tool to run locally in order to create audio for some videos I...
New

Latest in Functional Programming in Java, Second Edition

Functional Programming in Java, Second Edition Portal

Sub Categories: