“I cannot access www.example.com. Can you check whether it is available?”
In Part 1, we built Pixel, a local Java chatbot using Ollama and Qwen3 0.6B. Pixel could discuss a problem. Now, let’s enable it to investigate.
We will call Google Public DNS’s external JSON API, then contact the website over HTTPS. There is no additional service to build, host, or run. The existing Ollama container still supplies the model.
The model interprets the request. Java validates and executes the action. External systems supply the evidence.
What we are building
Qwen proposes a website check as structured JSON. Java validates that proposal, queries public DNS, and checks the HTTPS homepage. The immediate result is formatted in Java without a second model call.
Google’s API reports public DNS results. The HTTPS request originates from the computer running Pixel and uses that computer’s DNS resolution. Neither proves worldwide availability.
Step 1: Prepare the project
Continue in the pixel-cli project from Part 1. Keep JDK 17, the Gradle Wrapper, the existing Jackson dependency, build.gradle, and settings.gradle. Keep the existing application main class as Pixel.
Start Ollama and verify the model:
BASH · SCROLL TO READ
docker start pixel-ollama
docker exec pixel-ollama ollama list
If needed:
BASH · SCROLL TO READ
docker exec -it pixel-ollama ollama pull qwen3:0.6b
The model setup from Part 1 remains unchanged. On a CPU, model interpretation can take longer than the network checks.
Use public websites for this exercise. The hostname is sent to Google Public DNS and the website is contacted from your computer. Hostname syntax validation does not prevent a hostname from resolving to an internal address. This is a local CLI tutorial; exposing it as a shared server would require enforced destination and outbound network restrictions.
Step 2: Try the external API
Windows PowerShell:
POWERSHELL · SCROLL TO READ
Invoke-RestMethod -Uri "https://dns.google/resolve?name=www.example.com&type=A"
macOS/Linux:
BASH · SCROLL TO READ
curl "https://dns.google/resolve?name=www.example.com&type=A"
Status is a DNS response code, not an HTTP status. A value of zero means the DNS query completed without a DNS error; inspect Answer to determine whether it contains the requested address records. We query A records for IPv4 and AAAA records for IPv6.
Step 3: Give each class one clear responsibility
Rather than placing the conversation, model integration, and network operations in one class, we will use seven small classes. Java 17 records provide immutable values for model decisions and DNS answers.
| File | Responsibility |
|---|
Pixel.java | Entry point; constructs and connects objects |
Conversation.java | Terminal input, history, and action dispatch |
ModelDecision.java | Validates the model’s proposed action and arguments |
OllamaClient.java | Requests structured decisions from Ollama |
WebsiteChecker.java | Validates the hostname and coordinates the checks |
GoogleDnsClient.java | Calls the external public DNS API |
HttpsChecker.java | Checks the homepage from the local network |
Create all seven files directly inside src/main/java. They intentionally use the default package to stay compatible with Part 1’s Pixel main class. Replace the old Pixel.java; do not append a second class of the same name. No Spring application, local HTTP server, or additional framework is required.
The JavaDocs describe each class’s contract. Inline comments explain decisions such as retaining the real result in history, validating JSON field types, disabling redirects, and closing response streams.
Pixel.java
JAVA · SCROLL TO READ
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.time.Duration;
import java.util.Scanner;
/**
* Entry point and composition root: creates the application objects and connects them.
* No model interpretation or website-checking logic belongs in this class.
*/
public final class Pixel {
private Pixel() { }
/**
* Starts the interactive CLI using the selected Ollama model.
* @param args optional first argument: model name; defaults to qwen3:0.6b
*/
public static void main(String[] args) {
String model = args.length == 0 ? "qwen3:0.6b" : args[0];
String base = System.getenv().getOrDefault(
"OLLAMA_URL", "http://127.0.0.1:11434").replaceAll("/+$", "");
// A configured ObjectMapper and HttpClient can be reused across requests.
ObjectMapper json = new ObjectMapper();
HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.followRedirects(HttpClient.Redirect.NEVER)
.build();
OllamaClient ollama = new OllamaClient(
http, json, URI.create(base + "/api/chat"), model);
WebsiteChecker checker = new WebsiteChecker(
new GoogleDnsClient(http, json), new HttpsChecker(http));
System.out.println("Pixel | model: " + model);
System.out.println("Describe a public website-access problem.");
System.out.println("Commands: /reset, /exit");
try (Scanner input = new Scanner(System.in)) {
new Conversation(ollama, checker).run(input);
}
}
}
Conversation.java
JAVA · SCROLL TO READ
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
/** Owns terminal interaction, bounded conversation history, and action dispatch. */
public final class Conversation {
private final OllamaClient ollama;
private final WebsiteChecker checker;
private final List<Map<String, String>> history = new ArrayList<>();
/**
* Creates a conversation with its model and permitted capability.
* @param ollama structured model client
* @param checker implementation of the single supported external action
*/
public Conversation(OllamaClient ollama, WebsiteChecker checker) {
this.ollama = ollama;
this.checker = checker;
}
/**
* Reads messages until /exit, end of input, or interruption.
* A failed turn is reported without adding a fabricated result to history.
* @param input terminal input; the caller owns and closes this Scanner
*/
public void run(Scanner input) {
while (true) {
System.out.print("\nYou: ");
if (!input.hasNextLine()) break;
String text = input.nextLine().trim();
if (text.equalsIgnoreCase("/exit")) break;
if (text.equalsIgnoreCase("/reset")) {
history.clear();
System.out.println("Pixel: Conversation cleared.");
continue;
}
if (text.isBlank()) continue;
if (text.length() > 1500) {
System.out.println("Please use 1,500 characters or fewer.");
continue;
}
try {
System.out.println("Pixel is preparing a reply...");
ModelDecision decision = ollama.decide(history, text);
// Only this predefined action can reach the checking capability.
String answer = decision.action().equals("chat")
? decision.reply() : checker.check(decision.hostname());
System.out.println("\nPixel: " + answer);
history.add(Map.of("role", "user", "content", text));
history.add(Map.of("role", "assistant", "content", answer));
// Retain four complete turns, including the actual observed result.
while (history.size() > 8) history.subList(0, 2).clear();
} catch (InterruptedException exception) {
Thread.currentThread().interrupt(); // Preserve cancellation intent.
break;
} catch (IOException | IllegalArgumentException exception) {
System.out.println("Pixel: I could not complete that request. "
+ exception.getMessage());
}
}
System.out.println("Goodbye!");
}
}
ModelDecision.java
JAVA · SCROLL TO READ
import com.fasterxml.jackson.databind.JsonNode;
/**
* Validated proposal from the language model. A proposal is data, never executable code.
* @param action either chat or check_website
* @param hostname hostname for a check; empty for chat
* @param reply conversational response; empty for a check
*/
public record ModelDecision(String action, String hostname, String reply) {
/**
* Enforces the field combinations supported by this application.
* The JSON schema helps generation, but Java remains the validation boundary.
* @throws IllegalArgumentException if fields are missing or inconsistent
*/
public ModelDecision {
if (action == null || hostname == null || reply == null) {
throw new IllegalArgumentException("Missing decision fields.");
}
boolean chat = action.equals("chat") && hostname.isEmpty() && !reply.isBlank();
boolean check = action.equals("check_website") && !hostname.isBlank()
&& hostname.length() <= 253 && reply.isEmpty();
if (!chat && !check) {
throw new IllegalArgumentException("Unsupported action or arguments.");
}
}
/**
* Converts JSON into a decision only after checking its structure and field types.
* @param node parsed model response
* @return a validated immutable proposal
* @throws IllegalArgumentException for an unexpected response shape
*/
public static ModelDecision fromJson(JsonNode node) {
if (node == null || !node.isObject() || node.size() != 3) {
throw new IllegalArgumentException("Invalid model response.");
}
return new ModelDecision(text(node, "action"), text(node, "hostname"),
text(node, "reply"));
}
/** Reads a string without coercing a number, boolean, or missing value into text. */
private static String text(JsonNode node, String field) {
JsonNode value = node.get(field);
if (value == null || !value.isTextual()) {
throw new IllegalArgumentException("Missing or invalid field: " + field);
}
return value.asText();
}
}
OllamaClient.java
JAVA · SCROLL TO READ
import com.fasterxml.jackson.databind.ObjectMapper;
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.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Calls Ollama to translate conversation into a structured proposal.
* This class cannot perform website checks; it only returns a ModelDecision.
*/
public final class OllamaClient {
private static final String SYSTEM_PROMPT = """
You are Pixel, a friendly IT help-desk assistant.
Return only JSON matching the supplied schema.
If the user cannot access a specific website, asks whether it is
accessible, or asks you to check it:
action = "check_website"
hostname = hostname only, without scheme, path, or port
reply = ""
Use recent conversation context when the reference is clear.
If no hostname is known, ask for it using the chat action.
Otherwise:
action = "chat"
hostname = ""
reply = a concise helpful response
Never invent website-check results or claim to execute commands.
Never ask for passwords or recovery codes.
""";
private static final String SCHEMA = """
{
"type":"object",
"properties":{
"action":{"type":"string","enum":["chat","check_website"]},
"hostname":{"type":"string"},
"reply":{"type":"string"}
},
"required":["action","hostname","reply"],
"additionalProperties":false
}
""";
private final HttpClient http;
private final ObjectMapper json;
private final URI endpoint;
private final String model;
/**
* Receives dependencies explicitly, avoiding hidden network configuration.
* @param http reusable HTTP client
* @param json JSON serializer and parser
* @param endpoint Ollama chat endpoint, owned by application configuration
* @param model installed Ollama model name
*/
public OllamaClient(HttpClient http, ObjectMapper json, URI endpoint, String model) {
this.http = http;
this.json = json;
this.endpoint = endpoint;
this.model = model;
}
/**
* Requests and validates one model decision without mutating conversation history.
* @param history completed user/assistant turns
* @param input current user message
* @return validated proposal for the application to dispatch
* @throws IOException on transport errors or invalid JSON
* @throws InterruptedException if the calling thread is interrupted
* @throws IllegalArgumentException if the generated decision is invalid
*/
public ModelDecision decide(List<Map<String, String>> history, String input)
throws IOException, InterruptedException {
List<Map<String, String>> messages = new ArrayList<>();
messages.add(Map.of("role", "system", "content",
SYSTEM_PROMPT + "\nSchema:\n" + SCHEMA));
messages.addAll(history);
messages.add(Map.of("role", "user", "content", input));
// Supplying the schema as format constrains output structure, not intent.
String body = json.writeValueAsString(Map.of(
"model", model, "messages", messages,
"format", json.readTree(SCHEMA), "stream", false, "think", false,
"options", Map.of("temperature", 0, "num_ctx", 4096,
"num_predict", 300)));
HttpRequest request = HttpRequest.newBuilder(endpoint)
.timeout(Duration.ofMinutes(3))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body)).build();
var response = http.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("Ollama returned HTTP " + response.statusCode());
}
var envelope = json.readTree(response.body());
if (envelope == null) {
throw new IOException("Ollama returned an empty JSON response.");
}
String content = envelope.path("message").path("content").asText("");
// The envelope is Ollama's response; content contains the model's JSON.
return ModelDecision.fromJson(json.readTree(content));
}
}
WebsiteChecker.java
JAVA · SCROLL TO READ
import java.io.IOException;
import java.util.Locale;
/**
* Coordinates hostname validation, public DNS lookups, and an HTTPS request.
* This capability has no dependency on Ollama or terminal conversation history,
* making it a natural candidate for later exposure through an MCP adapter.
*/
public final class WebsiteChecker {
private final GoogleDnsClient dns;
private final HttpsChecker https;
/**
* @param dns external DNS API client
* @param https HTTPS homepage checker
*/
public WebsiteChecker(GoogleDnsClient dns, HttpsChecker https) {
this.dns = dns;
this.https = https;
}
/**
* Performs a bounded sequence of checks and formats the immediate evidence.
* Both record types are queried because a website can support IPv4, IPv6, or both.
* An API transport failure aborts the turn; it is not evidence of a website outage.
* @param value hostname proposed by the model
* @return DNS findings and, when an address is found, the HTTPS result
* @throws IllegalArgumentException for unsupported hostname syntax
* @throws IOException if the DNS API request or response fails
* @throws InterruptedException if either external operation is interrupted
*/
public String check(String value) throws IOException, InterruptedException {
String hostname = validateHostname(value);
System.out.println("[Action] Public DNS lookup: " + hostname);
GoogleDnsClient.Answer ipv4 = dns.lookup(hostname, "A");
GoogleDnsClient.Answer ipv6 = dns.lookup(hostname, "AAAA");
String statuses = "DNS status codes: A=" + ipv4.status()
+ ", AAAA=" + ipv6.status() + ".";
if (!ipv4.hasAddress() && !ipv6.hasAddress()) {
return "Google Public DNS returned no usable A or AAAA address for "
+ hostname + ". " + statuses
+ " This does not establish domain-registration status.";
}
System.out.println("[Action] HTTPS request: " + hostname);
return "Google Public DNS returned an address. " + statuses + "\n"
+ https.check(hostname);
}
/**
* Accepts ASCII DNS hostnames, excluding schemes, paths, ports, and IP literals.
* This validates syntax only: a valid name can still resolve to a private address.
* It is not a public-destination filter or a defence against DNS rebinding.
* @param value hostname supplied by the model
* @return trimmed, lowercase hostname
* @throws IllegalArgumentException if the hostname syntax is unsupported
*/
private static String validateHostname(String value) {
if (value == null) throw new IllegalArgumentException("Provide a hostname.");
String hostname = value.trim().toLowerCase(Locale.ROOT);
if (hostname.length() > 253 || !hostname.matches(
"(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}")) {
throw new IllegalArgumentException(
"Use a hostname such as www.example.com, without a scheme, path, or port.");
}
return hostname;
}
}
GoogleDnsClient.java
JAVA · SCROLL TO READ
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
/** Calls Google Public DNS's external JSON API for address-record evidence. */
public final class GoogleDnsClient {
private final HttpClient http;
private final ObjectMapper json;
/**
* @param http reusable HTTP client with redirects disabled
* @param json JSON response parser
*/
public GoogleDnsClient(HttpClient http, ObjectMapper json) {
this.http = http;
this.json = json;
}
/**
* Captures both DNS status and whether a requested address record was returned.
* @param status DNS response code, distinct from an HTTP status code
* @param hasAddress true if a successful response contains the requested record
*/
public record Answer(int status, boolean hasAddress) { }
/**
* Looks up A or AAAA records without treating an empty answer as non-registration.
* @param hostname hostname already validated by WebsiteChecker
* @param type A for IPv4 or AAAA for IPv6
* @return DNS status and address-record presence
* @throws IOException if the API cannot be reached or its response is invalid
* @throws InterruptedException if interrupted during the request
* @throws IllegalArgumentException if an unsupported record type is requested
*/
public Answer lookup(String hostname, String type)
throws IOException, InterruptedException {
int recordType = switch (type) {
case "A" -> 1;
case "AAAA" -> 28;
default -> throw new IllegalArgumentException("Use A or AAAA.");
};
String encoded = URLEncoder.encode(hostname, StandardCharsets.UTF_8);
URI uri = URI.create("https://dns.google/resolve?name=" + encoded + "&type=" + type);
HttpRequest request = HttpRequest.newBuilder(uri)
.timeout(Duration.ofSeconds(10))
.header("Accept", "application/json").GET().build();
var response = http.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("Google DNS API returned HTTP " + response.statusCode());
}
JsonNode result = json.readTree(response.body());
if (result == null || !result.path("Status").isIntegralNumber()) {
throw new IOException("Unexpected DNS API response.");
}
int status = result.path("Status").intValue();
if (status == 0) {
// CNAME records may also appear; count only the requested address type.
for (JsonNode answer : result.path("Answer")) {
if (answer.path("type").asInt() == recordType
&& !answer.path("data").asText("").isBlank()) {
return new Answer(status, true);
}
}
}
return new Answer(status, false);
}
}
HttpsChecker.java
JAVA · SCROLL TO READ
import javax.net.ssl.SSLException;
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.net.http.HttpTimeoutException;
import java.time.Duration;
/**
* Checks the HTTPS homepage from the computer running Pixel.
* Uses local DNS resolution; Google DNS answers are not used as connection targets.
*/
public final class HttpsChecker {
private final HttpClient http;
/**
* Requires redirects to be disabled so every reported status describes this host.
* @param http reusable client with Redirect.NEVER
* @throws IllegalArgumentException if the client follows redirects
*/
public HttpsChecker(HttpClient http) {
if (http.followRedirects() != HttpClient.Redirect.NEVER) {
throw new IllegalArgumentException("The checker must not follow redirects.");
}
this.http = http;
}
/**
* Sends a GET request and reports the observed response or connection failure.
* @param hostname hostname validated by WebsiteChecker
* @return factual result; does not claim global availability
* @throws InterruptedException if the request is interrupted
*/
public String check(String hostname) throws InterruptedException {
HttpRequest request = HttpRequest.newBuilder(URI.create("https://" + hostname + "/"))
.timeout(Duration.ofSeconds(10))
.header("User-Agent", "PixelWebsiteChecker/1.0").GET().build();
try {
var response = http.send(request, HttpResponse.BodyHandlers.ofInputStream());
// Do not parse HTML or feed website content into the language model.
// Closing the stream avoids retaining/downloading the complete page;
// some body bytes may already have arrived with the response headers.
try (var body = response.body()) {
int code = response.statusCode();
String explanation;
if (code >= 200 && code < 300) {
explanation = "The homepage responded successfully from this computer's network.";
} else if (code >= 300 && code < 400) {
explanation = "The website redirected. The destination was not checked.";
} else if (code == 401 || code == 403 || code == 429) {
explanation = "The website restricted the request. Browser access may differ.";
} else if (code >= 500) {
explanation = "The website responded with a server error.";
} else {
explanation = "The website responded, but its homepage returned an HTTP error.";
}
return hostname + " returned HTTP " + code + ". " + explanation;
}
} catch (HttpTimeoutException exception) {
return hostname + ": the HTTPS request timed out. This does not establish a global outage.";
} catch (SSLException exception) {
return hostname + ": a trusted HTTPS connection could not be established.";
} catch (IOException exception) {
return hostname + ": public DNS returned an address, but the HTTPS request failed "
+ "from this computer. Local DNS or network conditions may differ.";
}
}
}
Step 4: Build and launch
From the existing pixel-cli project root, run:
Windows PowerShell:
POWERSHELL · SCROLL TO READ
.\gradlew.bat installDist
.\build\install\pixel-cli\bin\pixel-cli.bat
macOS/Linux:
BASH · SCROLL TO READ
./gradlew installDist
./build/install/pixel-cli/bin/pixel-cli
Only the Java CLI and the existing Ollama container need to run. There is no website-checking server to start.
Step 5: Ask Pixel to investigate
TEXT · SCROLL TO READ
You: I cannot access www.example.com. Can you check it?
Pixel is preparing a reply...
[Action] Public DNS lookup: www.example.com
[Action] HTTPS request: www.example.com
Pixel then prints the observed result. A successful response might resemble:
TEXT · SCROLL TO READ
Pixel: Google Public DNS returned an address. DNS status codes: A=0, AAAA=0.
www.example.com returned HTTP 200. The homepage responded successfully from this computer's network.
This is illustrative output; actual DNS results and HTTP responses vary.
The model proposes a decision resembling:
JSON · SCROLL TO READ
{"action":"check_website","hostname":"www.example.com","reply":""}
ModelDecision validates the field combination. Conversation dispatches the supported action. WebsiteChecker coordinates the checks. No model-generated endpoint, HTTP method, or command is executed.
Now test clarification:
TEXT · SCROLL TO READ
/reset
I cannot access a website.
Pixel should ask for the hostname. Reply:
TEXT · SCROLL TO READ
The website is www.example.com.
History provides the context for that answer. The CLI retains four completed turns, including the actual check result. Follow-up explanations are model-generated, so they can still be mistaken. A small model may also select the wrong action; structured JSON constrains the response shape, not the correctness of its interpretation.
Step 6: Check a name that should not resolve
TEXT · SCROLL TO READ
Please check pixel-demo.invalid.
The .invalid namespace is reserved for invalid names. Under normal public DNS behaviour, no address should be returned. Pixel displays the DNS statuses without claiming to have checked domain registration.
If the DNS API fails to respond, Pixel reports a failed request. An API transport error is not evidence that the target website is down. In this example the A and AAAA lookups run sequentially; an API failure aborts the turn rather than silently ignoring incomplete evidence.
Understanding the evidence
| Observation | What it establishes |
|---|
| Public DNS returns an A or AAAA record | Google’s resolver supplied an address record |
| No address records returned | No usable address was found by these queries; inspect DNS statuses |
| HTTP 2xx | The requested homepage responded successfully |
| HTTP 3xx | A redirect was returned; its destination remains unchecked |
| HTTP 401/403/429 | The website restricted this request |
| HTTP 5xx | The website returned a server error |
| Timeout or connection failure | This computer could not complete the HTTPS check |
| TLS failure | Java could not establish a trusted HTTPS connection |
DNS resolution, domain registration, and application availability are different questions. A functioning homepage does not establish that login, search, or checkout works. A website may block Java while allowing a browser. Corporate proxy settings and TLS trust can also differ between clients.
The example checks HTTPS port 443 and the homepage only. It does not follow redirects, execute JavaScript, authenticate, or inspect HTML. Timeouts apply to the HTTP requests; they are not a strict overall wall-clock limit for the complete turn or the platform’s local DNS resolution.
Step 7: Stop the demo
Exit Pixel:
TEXT · SCROLL TO READ
/exit
Then stop Ollama:
BASH · SCROLL TO READ
docker stop pixel-ollama
What Have We Established?
Pixel now connects a natural-language request to an external API and a live
website check. The model interprets the request, Java controls execution,
and external systems supply the evidence.
Now imagine applying this approach across an ecosystem of
data-accessor services. One service retrieves inventory,
another provides order details, and a third reports delivery status.
Pixel could become a conversational entry point across these capabilities.
A customer-support colleague might ask:
“Why has order 12345 not arrived, and is a replacement available?”
An extended Pixel could retrieve the order, check its shipment, query
replacement inventory, and assemble an answer grounded in those results.
Building that workflow would require additional orchestration: selecting
multiple tools, passing identifiers between calls, and handling incomplete
or conflicting responses.
The same idea applies to an IT service desk:
“I cannot access the employee portal. Check whether it responds, and log a
ticket if the problem persists.”
Pixel could call a website-checking service, ask for the missing details,
and invoke a ticket-management microservice. That service would create the
ticket and return its reference number. Because creating a ticket changes
a system, the workflow should verify the user’s identity, confirm the
submission details, and prevent duplicate tickets if a request is retried.
Other possibilities are close to everyday work:
-
Travel operations: Retrieve booking details, supplier
status, and cancellation rules to help investigate a disrupted itinerary.
-
Engineering support: Correlate deployment history,
service health, and incident records to investigate a reported failure.
-
Employee assistance: Query approved knowledge sources,
check an existing request, and submit a new service request when needed.
The architectural opportunity is to
make existing capabilities accessible through conversation.
Each service continues to own its data, business rules, and authorization.
Pixel helps users discover and combine those capabilities, while the
application controls what can be called and what information can be returned.
We have implemented one small example. The broader possibility is a
conversational application that can retrieve evidence, coordinate services,
and help complete useful work.
In Part 3, we will expose our website-checking capability through MCP - and
explore how selected methods on ordinary Java objects can become
discoverable tools through a reusable abstraction.
References