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

JohnS
I can’t setup the Rails source code. This happens in a working directory containing multiple (postgres) Rails apps. With: ruby-3.0.0 s...
New
AleksandrKudashkin
On the page xv there is an instruction to run bin/setup from the main folder. I downloaded the source code today (12/03/21) and can’t see...
New
swlaschin
The book has the same “Problem space/Solution space” diagram on page 18 as is on page 17. The correct Problem/Solution space diagrams ar...
New
adamwoolhether
When trying to generate the protobuf .go file, I receive this error: Unknown flag: --go_opt libprotoc 3.12.3 MacOS 11.3.1 Googling ...
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
brunogirin
When installing Cards as an editable package, I get the following error: ERROR: File “setup.py” not found. Directory cannot be installe...
New
hazardco
On page 78 the following code appears: &lt;%= link_to ‘Destroy’, product, class: ‘hover:underline’, method: :delete, data: { confirm...
New
dtonhofer
@parrt In the context of Chapter 4.3, the grammar Java.g4, meant to parse Java 6 compilation units, no longer passes ANTLR (currently 4....
New
New
gorkaio
root_layout: {PentoWeb.LayoutView, :root}, This results in the following following error: no “root” html template defined for PentoWeb...
New

Other popular topics Top

Exadra37
I am thinking in building or buy a desktop computer for programing, both professionally and on my free time, and my choice of OS is Linux...
New
Exadra37
Please tell us what is your preferred monitor setup for programming(not gaming) and why you have chosen it. Does your monitor have eye p...
New
AstonJ
Curious to know which languages and frameworks you’re all thinking about learning next :upside_down_face: Perhaps if there’s enough peop...
New
AstonJ
Inspired by this post from @Carter, which languages, frameworks or other tech or tools do you think is killing it right now? :upside_down...
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
PragmaticBookshelf
Learn different ways of writing concurrent code in Elixir and increase your application's performance, without sacrificing scalability or...
New
PragmaticBookshelf
Use WebRTC to build web applications that stream media and data in real time directly from one user to another, all in the browser. ...
New
AstonJ
Biggest jackpot ever apparently! :upside_down_face: I don’t (usually) gamble/play the lottery, but working on a program to predict the...
New
New
RobertRichards
Hair Salon Games for Girls Fun Girls Hair Saloon game is mainly developed for kids. This game allows users to select virtual avatars to ...
New

Latest in Functional Programming in Java, Second Edition

Functional Programming in Java, Second Edition Portal

Sub Categories: