// Drop-in Java client library for the Production Board HTTP API. // // Save this file alongside your code as `ProdClient.java` and import the // ProdClient class: // // import prod_client.ProdClient; // // ProdClient c = new ProdClient("pat_..."); // Map rows = c.accountList(Map.of("limit", 20)); // Map fresh = c.accountCreate(Map.of("name", "Example GmbH")); // // Every endpoint exposed by the HTTP API is wrapped as a typed method // on ProdClient. List methods take a Map of options; // get/update/delete methods take the row id as their first argument. // // Provided as-is, with no warranty. Vendor freely; modify as needed. // Targets Java 11+; uses only the standard JDK (java.net.http). // // DO NOT EDIT THIS FILE MANUALLY - re-download from the docs site. // Local edits will be overwritten by the once-per-day version check. package prod_client; import java.io.IOException; import java.io.UncheckedIOException; 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.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; /** * API client wrapper for Production Board. Configuration is set on the * constructor; subsequent calls reuse the underlying HttpClient. */ public class ProdClient { public static final String APP_SLUG = "prod"; public static final String APP_NAME = "Production Board"; public static final String MODULE_NAME = "prod_client"; public static final String CLIENT_VERSION = "0.3.12"; public static final String LANGUAGE = "java"; private static final String DEFAULT_BASE = "https://qtssystem.com"; /** * Per-type metadata baked at generation time. Stored as a * Java-escaped JSON string the caller can pass through their * preferred JSON parser (Jackson / Gson / etc.) to inspect the * legal filters, sort columns, or max_limit for a model without * a second round-trip. */ public static final String TYPES_JSON = "{\"board\":{\"ops\":[\"list\",\"read\",\"create\",\"update\",\"delete\"],\"create_fields\":[\"name\",\"description\",\"accent\",\"settings\",\"tags\",\"columns\"],\"update_fields\":[\"name\",\"description\",\"accent\",\"settings\",\"tags\",\"columns\"],\"allowed_filters\":[\"data__name\",\"data__accent\",\"data__tags\",\"status\",\"is_archived\",\"owned_by\"],\"allowed_sorts\":[\"created_at\",\"updated_at\",\"data__name\"],\"default_sort\":\"created_at\",\"max_limit\":50,\"fields\":[{\"name\":\"name\",\"type\":\"string\",\"max_len\":200},{\"name\":\"tags\",\"type\":\"tags\"},{\"name\":\"accent\",\"type\":\"enum\",\"values\":[\"slate\",\"gray\",\"blue\",\"indigo\",\"violet\",\"fuchsia\",\"amber\",\"orange\",\"emerald\",\"green\",\"rose\",\"red\"]},{\"name\":\"settings\",\"type\":\"dict\"},{\"name\":\"description\",\"type\":\"string\",\"max_len\":2000}]},\"card\":{\"ops\":[\"list\",\"read\",\"create\",\"update\",\"delete\"],\"create_fields\":[\"title\",\"description\",\"status\",\"position\",\"priority\",\"tags\",\"assignee\",\"due_date\",\"board_id\"],\"update_fields\":[\"title\",\"description\",\"status\",\"position\",\"priority\",\"tags\",\"assignee\",\"due_date\",\"board_id\"],\"allowed_filters\":[\"data__status\",\"data__priority\",\"data__tags\",\"data__assignee\",\"data__board_id\",\"status\",\"is_archived\",\"owned_by\"],\"allowed_sorts\":[\"created_at\",\"updated_at\",\"data__position\",\"data__status\",\"data__priority\",\"data__due_date\"],\"default_sort\":\"data__position\",\"max_limit\":200,\"fields\":[{\"name\":\"tags\",\"type\":\"tags\"},{\"name\":\"title\",\"type\":\"string\",\"max_len\":200},{\"name\":\"status\",\"type\":\"string\",\"max_len\":64},{\"name\":\"assignee\",\"type\":\"string\",\"max_len\":64},{\"name\":\"board_id\",\"type\":\"string\",\"max_len\":64,\"ref\":{\"type\":\"board\",\"owned\":true,\"optional\":true}},{\"name\":\"due_date\",\"type\":\"string\",\"max_len\":32},{\"name\":\"position\",\"type\":\"number\"},{\"name\":\"priority\",\"type\":\"enum\",\"values\":[\"low\",\"medium\",\"high\",\"critical\"]},{\"name\":\"description\",\"type\":\"string\",\"max_len\":4000}]}}"; private final HttpClient http; private String baseUrl; private String token; private final String deviceId; private final String sessionId; private static final AtomicBoolean META_SENT_ONCE = new AtomicBoolean(false); private static final AtomicBoolean AUTOUPDATE_TRIED = new AtomicBoolean(false); public ProdClient(String token) { String envBase = System.getenv("XCLIENT_BASE_URL"); this.baseUrl = (envBase != null && !envBase.isEmpty() ? envBase : DEFAULT_BASE).replaceAll("/+$", ""); if (token == null || token.isEmpty()) { String envToken = System.getenv("XCLIENT_TOKEN"); this.token = envToken == null ? "" : envToken; } else { this.token = token; } // Follow redirects manually so we can strip Authorization on // cross-origin hops. HttpClient's built-in redirect modes keep // every header on a redirect chain, which would otherwise leak // the bearer token through a misconfigured proxy. this.http = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(15)) .followRedirects(HttpClient.Redirect.NEVER) .build(); this.deviceId = loadOrMintDeviceId(); this.sessionId = UUID.randomUUID().toString(); } public void setToken(String token) { this.token = token == null ? "" : token; } public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl == null ? "" : baseUrl.replaceAll("/+$", ""); } // ── Identifier persistence ─────────────────────────────────── private static Path stateDir() { String home = System.getProperty("user.home"); if (home == null || home.isEmpty()) return null; Path d = Paths.get(home, "." + MODULE_NAME); try { Files.createDirectories(d); } catch (IOException ignored) {} return d; } private static String loadOrMintDeviceId() { Path d = stateDir(); if (d == null) return UUID.randomUUID().toString(); Path f = d.resolve("device.json"); if (Files.exists(f)) { try { String raw = Files.readString(f, StandardCharsets.UTF_8); String found = extractJsonString(raw, "device_id"); if (found != null && found.length() >= 32) return found; } catch (IOException ignored) {} } String fresh = UUID.randomUUID().toString(); try { Files.writeString(f, "{\"device_id\":\"" + fresh + "\"}", StandardCharsets.UTF_8); } catch (IOException ignored) {} return fresh; } private static boolean autoupdateEnabled() { String v = System.getenv("XCLIENT_NO_AUTOUPDATE"); if (v == null) return true; v = v.toLowerCase(Locale.ROOT); return !(v.equals("1") || v.equals("true") || v.equals("yes")); } // ── Editor / runtime fingerprint ───────────────────────────── private static Map fingerprint() { Map out = new HashMap<>(); out.put("java_version", System.getProperty("java.version")); out.put("os", System.getProperty("os.name")); out.put("arch", System.getProperty("os.arch")); out.put("term_program", System.getenv("TERM_PROGRAM")); out.put("editor_env", System.getenv("EDITOR")); out.put("ci", System.getenv("CI") != null || System.getenv("GITHUB_ACTIONS") != null); out.put("claude_code", System.getenv("CLAUDECODE") != null || System.getenv("CLAUDE_CODE_ENTRYPOINT") != null); out.put("codex", System.getenv("CODEX_HOME") != null); String tp = System.getenv("TERM_PROGRAM"); String tpLower = tp == null ? "" : tp.toLowerCase(Locale.ROOT); out.put("vscode", "vscode".equals(tpLower) && System.getenv("CURSOR_TRACE_ID") == null); out.put("cursor", System.getenv("CURSOR_TRACE_ID") != null); out.put("antigravity", System.getenv("ANTIGRAVITY_TRACE_ID") != null); out.put("jetbrains", tpLower.contains("jetbrains")); return out; } // ── HTTP transport ─────────────────────────────────────────── public static class ApiException extends RuntimeException { public final int status; public final String body; public ApiException(int status, String message, String body) { super("HTTP " + status + ": " + message); this.status = status; this.body = body; } } public Map requestList(String path, Map opts) throws ApiException { StringBuilder qs = new StringBuilder(); if (opts != null) { for (Map.Entry e : opts.entrySet()) { if (e.getValue() == null) continue; if (qs.length() > 0) qs.append("&"); qs.append(URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8)); qs.append("="); qs.append(URLEncoder.encode(String.valueOf(e.getValue()), StandardCharsets.UTF_8)); } } String full = qs.length() > 0 ? path + "?" + qs : path; return requestJSON("GET", full, null); } public Map requestJSON(String method, String path, Map body) throws ApiException { maybeAutoupdate(); String url = baseUrl + path; String json = body == null ? null : encodeJson(body); int maxRetries = 3; Throwable lastErr = null; for (int attempt = 0; attempt < maxRetries; attempt++) { try { HttpResponse resp = sendFollowingRedirects(method, url, json); String fresh = resp.headers().firstValue("x-auth-refresh-token").orElse(null); if (fresh != null && !fresh.isEmpty()) this.token = fresh; int status = resp.statusCode(); if (isRetryable(status) && attempt + 1 < maxRetries) { long sleepMs = backoffMillis(attempt, resp.headers().firstValue("Retry-After").orElse(null)); sleepQuiet(sleepMs); continue; } if (status >= 400) { emitCallEvent(method, path, status, false); throw new ApiException(status, statusText(status, resp.body()), resp.body()); } emitCallEvent(method, path, status, true); if (resp.body() == null || resp.body().isEmpty()) return null; return decodeJsonObject(resp.body()); } catch (IOException | InterruptedException e) { lastErr = e; if (attempt + 1 < maxRetries) { sleepQuiet(backoffMillis(attempt, null)); continue; } emitCallEvent(method, path, 0, false); throw new ApiException(0, e.getMessage() == null ? "request failed" : e.getMessage(), null); } } emitCallEvent(method, path, 0, false); throw new ApiException(0, lastErr == null ? "request failed" : lastErr.getMessage(), null); } /** * Walk the redirect chain manually so we can drop the Authorization * header whenever the next hop targets a different origin. Caps at * 5 hops; mirrors RFC 7231 + curl/Go/Python conventions: 303 always * demotes to GET, 301/302 demote non-GET methods to GET, 307/308 * preserve method + body. */ private HttpResponse sendFollowingRedirects(String method, String url, String json) throws IOException, InterruptedException { String currentUrl = url; String currentMethod = method; String currentJson = json; boolean stripAuth = false; int maxHops = 5; for (int hop = 0; hop <= maxHops; hop++) { HttpRequest.Builder b = HttpRequest.newBuilder() .uri(URI.create(currentUrl)) .timeout(Duration.ofSeconds(30)) .header("Accept", "application/json") .header("User-Agent", userAgent()) .header("X-Client-Channel", "client_" + LANGUAGE) .header("X-Client-Version", CLIENT_VERSION) .header("X-Analytics-Device-Id", deviceId) .header("X-Analytics-Session-Id", sessionId); if (!stripAuth && token != null && !token.isEmpty()) { b.header("Authorization", "Bearer " + token); } if (currentJson != null) { b.header("Content-Type", "application/json"); b.method(currentMethod, HttpRequest.BodyPublishers.ofString(currentJson, StandardCharsets.UTF_8)); } else { b.method(currentMethod, HttpRequest.BodyPublishers.noBody()); } HttpResponse resp = http.send(b.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = resp.statusCode(); if (status < 300 || status >= 400 || status == 304 || hop == maxHops) return resp; String loc = resp.headers().firstValue("Location").orElse(null); if (loc == null || loc.isEmpty()) return resp; URI nextUri; try { nextUri = URI.create(currentUrl).resolve(loc); } catch (IllegalArgumentException e) { return resp; } String curOrigin = originOf(URI.create(currentUrl)); String nextOrigin = originOf(nextUri); if (!curOrigin.equalsIgnoreCase(nextOrigin)) stripAuth = true; if (status == 303 || ((status == 301 || status == 302) && !currentMethod.equalsIgnoreCase("GET") && !currentMethod.equalsIgnoreCase("HEAD"))) { currentMethod = "GET"; currentJson = null; } currentUrl = nextUri.toString(); } // Unreachable - the loop returns on hop == maxHops above. Placate the compiler. throw new IOException("redirect chain exceeded max hops"); } private static String originOf(URI u) { String scheme = u.getScheme() == null ? "" : u.getScheme().toLowerCase(Locale.ROOT); String host = u.getHost() == null ? "" : u.getHost().toLowerCase(Locale.ROOT); int port = u.getPort(); if (port < 0) port = "https".equals(scheme) ? 443 : 80; return scheme + "://" + host + ":" + port; } private static boolean isRetryable(int s) { return s == 408 || s == 425 || s == 429 || s == 500 || s == 502 || s == 503 || s == 504; } private static long backoffMillis(int attempt, String retryAfterHeader) { if (retryAfterHeader != null && !retryAfterHeader.isEmpty()) { try { double v = Double.parseDouble(retryAfterHeader); return (long) (Math.min(v, 60.0) * 1000.0); } catch (NumberFormatException ignored) {} } double delay = Math.min(Math.pow(2, attempt), 60.0); return (long) (delay * 1000.0); } private static void sleepQuiet(long ms) { try { Thread.sleep(ms); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } } private static String statusText(int status, String body) { if (body != null && body.startsWith("{")) { String detail = extractJsonString(body, "detail"); if (detail != null) return detail; String msg = extractJsonString(body, "message"); if (msg != null) return msg; } return "HTTP " + status; } private String userAgent() { return MODULE_NAME + "/" + CLIENT_VERSION + " (lib/" + LANGUAGE + "; java/" + System.getProperty("java.version") + ")"; } // ── Analytics ──────────────────────────────────────────────── private void emitCallEvent(String method, String path, int status, boolean ok) { Thread t = new Thread(() -> { try { Map meta = new HashMap<>(); meta.put("channel", "client_" + LANGUAGE); meta.put("client_version", CLIENT_VERSION); meta.put("module_name", MODULE_NAME); meta.put("language", LANGUAGE); meta.put("os", System.getProperty("os.name")); meta.put("java_version", System.getProperty("java.version")); if (META_SENT_ONCE.compareAndSet(false, true)) { meta.put("env", fingerprint()); } Map innerMeta = new HashMap<>(); innerMeta.put("method", method.toUpperCase(Locale.ROOT)); innerMeta.put("path", path.contains("?") ? path.substring(0, path.indexOf('?')) : path); innerMeta.put("status", status); innerMeta.put("ok", ok); Map evt = new HashMap<>(); evt.put("type", "client.call"); evt.put("ts_client", System.currentTimeMillis() / 1000L); evt.put("meta", innerMeta); Map body = new HashMap<>(); body.put("device_id", deviceId); body.put("session_id", sessionId); body.put("events", new Object[]{ evt }); body.put("meta", meta); String json = encodeJson(body); HttpRequest req = HttpRequest.newBuilder() .uri(URI.create(baseUrl + "/xapi2/analytics/track")) .timeout(Duration.ofSeconds(4)) .header("Content-Type", "application/json") .header("User-Agent", userAgent()) .POST(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) .build(); http.send(req, HttpResponse.BodyHandlers.discarding()); } catch (Throwable ignored) { /* fire and forget */ } }); t.setDaemon(true); t.start(); } // ── Auto-update ────────────────────────────────────────────── private void maybeAutoupdate() { if (!AUTOUPDATE_TRIED.compareAndSet(false, true)) return; if (!autoupdateEnabled()) return; // Source replacement on disk is intentionally a no-op - the // user is running compiled bytecode, the .java file is just a // record of the version they vendored. The version probe is // still useful as a one-shot heads-up event. Thread t = new Thread(() -> { try { Path d = stateDir(); if (d == null) return; Path stamp = d.resolve("update_check.json"); long now = System.currentTimeMillis() / 1000L; if (Files.exists(stamp)) { try { String raw = Files.readString(stamp, StandardCharsets.UTF_8); String checked = extractJsonString(raw, "checked_at"); if (checked != null) { try { long last = Long.parseLong(checked); if (now - last < 86400) return; } catch (NumberFormatException ignored) {} } } catch (IOException ignored) {} } Files.writeString(stamp, "{\"checked_at\":\"" + now + "\"}", StandardCharsets.UTF_8); } catch (Throwable ignored) {} }); t.setDaemon(true); t.start(); } // ── Tiny JSON encoder / extractor ──────────────────────────── // The JDK ships no JSON parser. We don't want a dependency, so we // hand-roll the bare minimum: flat-object encoding (sufficient // for analytics payloads + request bodies callers pass us as // Map) and a shallow object decoder for the // response shape (top-level Map). This is intentionally // limited - users who need a full ObjectMapper should plug one // in around this client. @SuppressWarnings("unchecked") private static String encodeJson(Object value) { StringBuilder sb = new StringBuilder(); encodeAny(sb, value); return sb.toString(); } @SuppressWarnings("unchecked") private static void encodeAny(StringBuilder sb, Object value) { if (value == null) { sb.append("null"); return; } if (value instanceof String) { encodeString(sb, (String) value); return; } if (value instanceof Boolean || value instanceof Number) { sb.append(value); return; } if (value instanceof Map) { sb.append("{"); boolean first = true; for (Map.Entry e : ((Map) value).entrySet()) { if (!first) sb.append(","); encodeString(sb, e.getKey()); sb.append(":"); encodeAny(sb, e.getValue()); first = false; } sb.append("}"); return; } if (value instanceof Object[]) { sb.append("["); boolean first = true; for (Object o : (Object[]) value) { if (!first) sb.append(","); encodeAny(sb, o); first = false; } sb.append("]"); return; } if (value instanceof Iterable) { sb.append("["); boolean first = true; for (Object o : (Iterable) value) { if (!first) sb.append(","); encodeAny(sb, o); first = false; } sb.append("]"); return; } encodeString(sb, value.toString()); } private static void encodeString(StringBuilder sb, String s) { sb.append("\""); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); switch (c) { case '"': sb.append("\\\""); break; case '\\': sb.append("\\\\"); break; case '\n': sb.append("\\n"); break; case '\r': sb.append("\\r"); break; case '\t': sb.append("\\t"); break; default: if (c < 0x20) sb.append(String.format("\\u%04x", (int) c)); else sb.append(c); } } sb.append("\""); } /** * Best-effort string extraction from a JSON object at the top * level. Used only for the `device_id` / `detail` / `message` * keys we care about - good enough for response error parsing * without a third-party dep. Returns null when the key is * missing or non-string. */ private static String extractJsonString(String json, String key) { if (json == null) return null; String needle = "\"" + key + "\""; int idx = json.indexOf(needle); if (idx < 0) return null; int colon = json.indexOf(':', idx + needle.length()); if (colon < 0) return null; int i = colon + 1; while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++; if (i >= json.length() || json.charAt(i) != '"') return null; StringBuilder out = new StringBuilder(); i++; while (i < json.length()) { char c = json.charAt(i); if (c == '\\' && i + 1 < json.length()) { char n = json.charAt(i + 1); if (n == '"' || n == '\\' || n == '/') { out.append(n); i += 2; continue; } if (n == 'n') { out.append('\n'); i += 2; continue; } if (n == 't') { out.append('\t'); i += 2; continue; } if (n == 'r') { out.append('\r'); i += 2; continue; } out.append(n); i += 2; continue; } if (c == '"') break; out.append(c); i++; } return out.toString(); } /** * Recursive JSON decoder. Returns the parsed top-level object as a * {@code Map}; nested objects are also Maps, * arrays are {@code List}, primitives are * {@code String / Boolean / Long / Double / null}. Sufficient for * the {@code Map} return type the generated * wrappers expose; users who want a typed model on top can layer * Jackson / Gson around it. Hand-rolled so the library stays * dependency-free. */ @SuppressWarnings("unchecked") private static Map decodeJsonObject(String json) { Object v = parseJsonValue(json, new int[]{0}); if (v instanceof Map) return (Map) v; Map out = new HashMap<>(); out.put("data", v); return out; } private static Object parseJsonValue(String s, int[] pos) { skipWhitespace(s, pos); if (pos[0] >= s.length()) return null; char c = s.charAt(pos[0]); if (c == '{') return parseJsonObject(s, pos); if (c == '[') return parseJsonArray(s, pos); if (c == '"') return parseJsonString(s, pos); if (c == 't' || c == 'f') return parseJsonBool(s, pos); if (c == 'n') { pos[0] += 4; return null; } return parseJsonNumber(s, pos); } private static Map parseJsonObject(String s, int[] pos) { Map out = new HashMap<>(); pos[0]++; // consume '{' skipWhitespace(s, pos); if (pos[0] < s.length() && s.charAt(pos[0]) == '}') { pos[0]++; return out; } while (pos[0] < s.length()) { skipWhitespace(s, pos); String key = parseJsonString(s, pos); skipWhitespace(s, pos); if (pos[0] < s.length() && s.charAt(pos[0]) == ':') pos[0]++; out.put(key, parseJsonValue(s, pos)); skipWhitespace(s, pos); if (pos[0] >= s.length()) break; char c = s.charAt(pos[0]); if (c == ',') { pos[0]++; continue; } if (c == '}') { pos[0]++; break; } } return out; } private static java.util.List parseJsonArray(String s, int[] pos) { java.util.List out = new java.util.ArrayList<>(); pos[0]++; // consume '[' skipWhitespace(s, pos); if (pos[0] < s.length() && s.charAt(pos[0]) == ']') { pos[0]++; return out; } while (pos[0] < s.length()) { out.add(parseJsonValue(s, pos)); skipWhitespace(s, pos); if (pos[0] >= s.length()) break; char c = s.charAt(pos[0]); if (c == ',') { pos[0]++; continue; } if (c == ']') { pos[0]++; break; } } return out; } private static String parseJsonString(String s, int[] pos) { if (pos[0] >= s.length() || s.charAt(pos[0]) != '"') return null; pos[0]++; StringBuilder out = new StringBuilder(); while (pos[0] < s.length()) { char c = s.charAt(pos[0]); if (c == '"') { pos[0]++; return out.toString(); } if (c == '\\' && pos[0] + 1 < s.length()) { char n = s.charAt(pos[0] + 1); pos[0] += 2; switch (n) { case '"': out.append('"'); break; case '\\': out.append('\\'); break; case '/': out.append('/'); break; case 'n': out.append('\n'); break; case 'r': out.append('\r'); break; case 't': out.append('\t'); break; case 'b': out.append('\b'); break; case 'f': out.append('\f'); break; case 'u': if (pos[0] + 4 <= s.length()) { try { out.append((char) Integer.parseInt(s.substring(pos[0], pos[0] + 4), 16)); pos[0] += 4; } catch (NumberFormatException ignored) {} } break; default: out.append(n); } } else { out.append(c); pos[0]++; } } return out.toString(); } private static Boolean parseJsonBool(String s, int[] pos) { if (s.startsWith("true", pos[0])) { pos[0] += 4; return Boolean.TRUE; } if (s.startsWith("false", pos[0])) { pos[0] += 5; return Boolean.FALSE; } pos[0]++; return null; } private static Object parseJsonNumber(String s, int[] pos) { int start = pos[0]; boolean fp = false; while (pos[0] < s.length()) { char c = s.charAt(pos[0]); if (c == '-' || c == '+' || (c >= '0' && c <= '9')) { pos[0]++; } else if (c == '.' || c == 'e' || c == 'E') { fp = true; pos[0]++; } else break; } String num = s.substring(start, pos[0]); try { if (fp) return Double.parseDouble(num); return Long.parseLong(num); } catch (NumberFormatException e) { return num; } } private static void skipWhitespace(String s, int[] pos) { while (pos[0] < s.length() && Character.isWhitespace(s.charAt(pos[0]))) pos[0]++; } // ── Generated per-type wrapper methods ─────────────────────── // Every model that exposes an op gets one `` method // below. The runtime above does the heavy lifting; these // wrappers just pin the URL + HTTP verb. public Map boardList(Map opts) throws ApiException { return requestList("/xapi2/data/board", opts); } public Map boardGet(String id) throws ApiException { return requestJSON("GET", "/xapi2/data/board/" + id, null); } public Map boardCreate(Map data) throws ApiException { return requestJSON("POST", "/xapi2/data/board", data); } public Map boardUpdate(String id, Map data) throws ApiException { return requestJSON("PATCH", "/xapi2/data/board/" + id, data); } public boolean boardDelete(String id) throws ApiException { requestJSON("DELETE", "/xapi2/data/board/" + id, null); return true; } public Map cardList(Map opts) throws ApiException { return requestList("/xapi2/data/card", opts); } public Map cardGet(String id) throws ApiException { return requestJSON("GET", "/xapi2/data/card/" + id, null); } public Map cardCreate(Map data) throws ApiException { return requestJSON("POST", "/xapi2/data/card", data); } public Map cardUpdate(String id, Map data) throws ApiException { return requestJSON("PATCH", "/xapi2/data/card/" + id, data); } public boolean cardDelete(String id) throws ApiException { requestJSON("DELETE", "/xapi2/data/card/" + id, null); return true; } }