// Drop-in Java client library for the KI BMS HTTP API. // // Save this file alongside your code as `AtsClient.java` and import the // AtsClient class: // // import ats_client.AtsClient; // // AtsClient c = new AtsClient("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 AtsClient. 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 ats_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 KI BMS. Configuration is set on the * constructor; subsequent calls reuse the underlying HttpClient. */ public class AtsClient { public static final String APP_SLUG = "ats"; public static final String APP_NAME = "KI BMS"; public static final String MODULE_NAME = "ats_client"; public static final String CLIENT_VERSION = "0.3.13"; public static final String LANGUAGE = "java"; private static final String DEFAULT_BASE = "https://www.ki-bewerber-management.de"; /** * 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 = "{\"application\":{\"ops\":[\"list\",\"read\",\"create\",\"update\",\"delete\"],\"create_fields\":[\"job_id\",\"candidate_id\",\"stage\",\"previous_stage\",\"position\",\"applied_at\",\"last_stage_at\",\"source_id\",\"source_label\",\"cover_letter\",\"cv_blob_id\",\"cv_url\",\"answers\",\"fit_score\",\"fit_reasoning\",\"fit_flags\",\"fit_computed_at\",\"rejected_reason\",\"rejected_note\",\"tags\"],\"update_fields\":[\"stage\",\"previous_stage\",\"position\",\"applied_at\",\"last_stage_at\",\"source_id\",\"source_label\",\"cover_letter\",\"cv_blob_id\",\"cv_url\",\"answers\",\"fit_score\",\"fit_reasoning\",\"fit_flags\",\"fit_computed_at\",\"rejected_reason\",\"rejected_note\",\"tags\"],\"allowed_filters\":[\"data__job_id\",\"data__candidate_id\",\"data__stage\",\"data__source_id\",\"data__rejected_reason\",\"data__is_archived\",\"status\",\"is_archived\",\"owned_by\",\"created_by\"],\"allowed_sorts\":[\"created_at\",\"updated_at\",\"data__applied_at\",\"data__fit_score\",\"data__position\",\"data__last_stage_at\"],\"default_sort\":\"data__position\",\"max_limit\":500,\"fields\":[{\"name\":\"tags\",\"type\":\"tags\"},{\"name\":\"stage\",\"type\":\"enum\",\"values\":[\"new\",\"review\",\"screening\",\"interview\",\"offer\",\"hired\",\"rejected\",\"talent_pool\"]},{\"name\":\"cv_url\",\"type\":\"url\",\"max_len\":2048},{\"name\":\"job_id\",\"type\":\"string\",\"max_len\":64,\"ref\":{\"type\":\"job\",\"owned\":false,\"optional\":false}},{\"name\":\"answers\",\"type\":\"list\"},{\"name\":\"position\",\"type\":\"number\"},{\"name\":\"fit_flags\",\"type\":\"tags\"},{\"name\":\"fit_score\",\"type\":\"number\"},{\"name\":\"source_id\",\"type\":\"string\",\"max_len\":64,\"ref\":{\"type\":\"source\",\"owned\":false,\"optional\":false}},{\"name\":\"applied_at\",\"type\":\"string\",\"max_len\":32},{\"name\":\"cv_blob_id\",\"type\":\"string\",\"max_len\":64},{\"name\":\"candidate_id\",\"type\":\"string\",\"max_len\":64,\"ref\":{\"type\":\"candidate\",\"owned\":false,\"optional\":false}},{\"name\":\"cover_letter\",\"type\":\"string\",\"max_len\":16000},{\"name\":\"source_label\",\"type\":\"string\",\"max_len\":200},{\"name\":\"fit_reasoning\",\"type\":\"string\",\"max_len\":4000},{\"name\":\"last_stage_at\",\"type\":\"string\",\"max_len\":32},{\"name\":\"rejected_note\",\"type\":\"string\",\"max_len\":2000},{\"name\":\"previous_stage\",\"type\":\"string\",\"max_len\":32},{\"name\":\"fit_computed_at\",\"type\":\"string\",\"max_len\":32},{\"name\":\"rejected_reason\",\"type\":\"enum\",\"values\":[\"not_qualified\",\"salary_mismatch\",\"location_mismatch\",\"culture_mismatch\",\"withdrew\",\"ghosted\",\"filled_internally\",\"duplicate\",\"other\"]}]},\"application_note\":{\"ops\":[\"list\",\"read\",\"create\",\"update\",\"delete\"],\"create_fields\":[\"body\",\"pinned\",\"private\",\"parent_kind\",\"parent_id\"],\"update_fields\":[\"body\",\"pinned\",\"private\"],\"allowed_filters\":[\"data__parent_id\",\"data__parent_kind\",\"data__pinned\",\"data__private\",\"status\",\"is_archived\",\"owned_by\",\"created_by\"],\"allowed_sorts\":[\"created_at\",\"updated_at\"],\"default_sort\":\"created_at\",\"max_limit\":200,\"fields\":[{\"name\":\"body\",\"type\":\"string\",\"max_len\":8000},{\"name\":\"pinned\",\"type\":\"bool\"},{\"name\":\"private\",\"type\":\"bool\"},{\"name\":\"parent_id\",\"type\":\"string\",\"max_len\":64},{\"name\":\"parent_kind\",\"type\":\"enum\",\"values\":[\"candidate\",\"application\",\"job\"]}]},\"candidate\":{\"ops\":[\"list\",\"read\",\"create\",\"update\",\"delete\"],\"create_fields\":[\"name\",\"first_name\",\"last_name\",\"salutation\",\"pronouns\",\"email\",\"phone\",\"city\",\"country\",\"current_company\",\"current_role\",\"years_experience\",\"available_from\",\"salary_expectation\",\"currency\",\"linkedin\",\"github\",\"portfolio\",\"cv_url\",\"cv_blob_id\",\"avatar_blob_id\",\"summary\",\"skills\",\"languages\",\"tags\",\"source_id\",\"source_label\",\"pool_status\",\"gdpr_consent\",\"gdpr_consent_at\",\"gdpr_retention_until\",\"preferred_locale\",\"last_touched_at\",\"color\"],\"update_fields\":[\"name\",\"first_name\",\"last_name\",\"salutation\",\"pronouns\",\"email\",\"phone\",\"city\",\"country\",\"current_company\",\"current_role\",\"years_experience\",\"available_from\",\"salary_expectation\",\"currency\",\"linkedin\",\"github\",\"portfolio\",\"cv_url\",\"cv_blob_id\",\"avatar_blob_id\",\"summary\",\"skills\",\"languages\",\"tags\",\"source_id\",\"source_label\",\"pool_status\",\"gdpr_consent\",\"gdpr_consent_at\",\"gdpr_retention_until\",\"preferred_locale\",\"last_touched_at\",\"color\"],\"allowed_filters\":[\"data__email\",\"data__name\",\"data__location\",\"data__country\",\"data__source_id\",\"data__tags\",\"data__skills\",\"data__pool_status\",\"data__gdpr_consent\",\"status\",\"is_archived\",\"owned_by\",\"created_by\"],\"allowed_sorts\":[\"created_at\",\"updated_at\",\"data__name\",\"data__last_touched_at\"],\"default_sort\":\"created_at\",\"max_limit\":200,\"fields\":[{\"name\":\"city\",\"type\":\"string\",\"max_len\":120},{\"name\":\"name\",\"type\":\"string\",\"max_len\":200},{\"name\":\"tags\",\"type\":\"tags\"},{\"name\":\"color\",\"type\":\"string\",\"max_len\":24},{\"name\":\"email\",\"type\":\"string\",\"max_len\":320},{\"name\":\"phone\",\"type\":\"string\",\"max_len\":64},{\"name\":\"cv_url\",\"type\":\"url\",\"max_len\":2048},{\"name\":\"github\",\"type\":\"url\",\"max_len\":2048},{\"name\":\"skills\",\"type\":\"tags\"},{\"name\":\"country\",\"type\":\"string\",\"max_len\":120},{\"name\":\"summary\",\"type\":\"string\",\"max_len\":4000},{\"name\":\"currency\",\"type\":\"string\",\"max_len\":8},{\"name\":\"linkedin\",\"type\":\"url\",\"max_len\":2048},{\"name\":\"pronouns\",\"type\":\"string\",\"max_len\":32},{\"name\":\"languages\",\"type\":\"tags\"},{\"name\":\"last_name\",\"type\":\"string\",\"max_len\":120},{\"name\":\"portfolio\",\"type\":\"url\",\"max_len\":2048},{\"name\":\"source_id\",\"type\":\"string\",\"max_len\":64,\"ref\":{\"type\":\"source\",\"owned\":false,\"optional\":false}},{\"name\":\"cv_blob_id\",\"type\":\"string\",\"max_len\":64},{\"name\":\"first_name\",\"type\":\"string\",\"max_len\":120},{\"name\":\"salutation\",\"type\":\"enum\",\"values\":[\"herr\",\"frau\",\"divers\",\"neutral\"]},{\"name\":\"pool_status\",\"type\":\"enum\",\"values\":[\"active\",\"talent_pool\",\"blocked\",\"withdrawn\"]},{\"name\":\"current_role\",\"type\":\"string\",\"max_len\":200},{\"name\":\"gdpr_consent\",\"type\":\"bool\"},{\"name\":\"source_label\",\"type\":\"string\",\"max_len\":200},{\"name\":\"available_from\",\"type\":\"string\",\"max_len\":32},{\"name\":\"avatar_blob_id\",\"type\":\"string\",\"max_len\":64},{\"name\":\"current_company\",\"type\":\"string\",\"max_len\":200},{\"name\":\"gdpr_consent_at\",\"type\":\"string\",\"max_len\":32},{\"name\":\"last_touched_at\",\"type\":\"string\",\"max_len\":32},{\"name\":\"preferred_locale\",\"type\":\"string\",\"max_len\":16},{\"name\":\"years_experience\",\"type\":\"number\"},{\"name\":\"salary_expectation\",\"type\":\"number\"},{\"name\":\"gdpr_retention_until\",\"type\":\"string\",\"max_len\":32}]},\"email_template\":{\"ops\":[\"list\",\"read\",\"create\",\"update\",\"delete\"],\"create_fields\":[\"name\",\"category\",\"subject\",\"body\",\"language\",\"stage_trigger\",\"auto_send\",\"active\",\"variables_doc\"],\"update_fields\":[\"name\",\"category\",\"subject\",\"body\",\"language\",\"stage_trigger\",\"auto_send\",\"active\",\"variables_doc\"],\"allowed_filters\":[\"data__name\",\"data__category\",\"data__stage_trigger\",\"data__active\",\"status\",\"is_archived\",\"owned_by\",\"created_by\"],\"allowed_sorts\":[\"created_at\",\"data__name\"],\"default_sort\":\"data__name\",\"max_limit\":100,\"fields\":[{\"name\":\"body\",\"type\":\"string\",\"max_len\":16000},{\"name\":\"name\",\"type\":\"string\",\"max_len\":200},{\"name\":\"active\",\"type\":\"bool\"},{\"name\":\"subject\",\"type\":\"string\",\"max_len\":400},{\"name\":\"category\",\"type\":\"enum\",\"values\":[\"acknowledge\",\"screening_invite\",\"interview_invite\",\"rejection\",\"offer\",\"talent_pool\",\"other\"]},{\"name\":\"language\",\"type\":\"string\",\"max_len\":16},{\"name\":\"auto_send\",\"type\":\"bool\"},{\"name\":\"stage_trigger\",\"type\":\"enum\",\"values\":[\"\",\"new\",\"review\",\"screening\",\"interview\",\"offer\",\"hired\",\"rejected\",\"talent_pool\"]},{\"name\":\"variables_doc\",\"type\":\"string\",\"max_len\":2000}]},\"evaluation\":{\"ops\":[\"list\",\"read\",\"create\",\"update\",\"delete\"],\"create_fields\":[\"application_id\",\"interview_id\",\"interviewer_id\",\"skills_score\",\"culture_score\",\"communication_score\",\"potential_score\",\"overall_score\",\"recommendation\",\"highlights\",\"concerns\",\"summary\"],\"update_fields\":[\"skills_score\",\"culture_score\",\"communication_score\",\"potential_score\",\"overall_score\",\"recommendation\",\"highlights\",\"concerns\",\"summary\"],\"allowed_filters\":[\"data__application_id\",\"data__interview_id\",\"data__interviewer_id\",\"data__recommendation\",\"status\",\"is_archived\",\"owned_by\",\"created_by\"],\"allowed_sorts\":[\"created_at\",\"data__overall_score\"],\"default_sort\":\"created_at\",\"max_limit\":200,\"fields\":[{\"name\":\"summary\",\"type\":\"string\",\"max_len\":4000},{\"name\":\"concerns\",\"type\":\"string\",\"max_len\":4000},{\"name\":\"highlights\",\"type\":\"string\",\"max_len\":4000},{\"name\":\"interview_id\",\"type\":\"string\",\"max_len\":64,\"ref\":{\"type\":\"interview\",\"owned\":false,\"optional\":false}},{\"name\":\"skills_score\",\"type\":\"number\"},{\"name\":\"culture_score\",\"type\":\"number\"},{\"name\":\"overall_score\",\"type\":\"number\"},{\"name\":\"application_id\",\"type\":\"string\",\"max_len\":64,\"ref\":{\"type\":\"application\",\"owned\":false,\"optional\":false}},{\"name\":\"interviewer_id\",\"type\":\"string\",\"max_len\":64},{\"name\":\"recommendation\",\"type\":\"enum\",\"values\":[\"strong_yes\",\"yes\",\"neutral\",\"no\",\"strong_no\"]},{\"name\":\"potential_score\",\"type\":\"number\"},{\"name\":\"communication_score\",\"type\":\"number\"}]},\"interview\":{\"ops\":[\"list\",\"read\",\"create\",\"update\",\"delete\"],\"create_fields\":[\"application_id\",\"candidate_id\",\"job_id\",\"kind\",\"status\",\"title\",\"scheduled_at\",\"duration_minutes\",\"location\",\"meeting_url\",\"interviewer_id\",\"interviewer_ids\",\"agenda\",\"notes\",\"send_invite\"],\"update_fields\":[\"kind\",\"status\",\"title\",\"scheduled_at\",\"duration_minutes\",\"location\",\"meeting_url\",\"interviewer_id\",\"interviewer_ids\",\"agenda\",\"notes\",\"send_invite\"],\"allowed_filters\":[\"data__application_id\",\"data__candidate_id\",\"data__job_id\",\"data__kind\",\"data__status\",\"data__interviewer_id\",\"status\",\"is_archived\",\"owned_by\",\"created_by\"],\"allowed_sorts\":[\"data__scheduled_at\",\"created_at\",\"updated_at\"],\"default_sort\":\"data__scheduled_at\",\"max_limit\":200,\"fields\":[{\"name\":\"kind\",\"type\":\"enum\",\"values\":[\"phone\",\"video\",\"onsite\",\"take_home\",\"panel\",\"trial_day\"]},{\"name\":\"notes\",\"type\":\"string\",\"max_len\":8000},{\"name\":\"title\",\"type\":\"string\",\"max_len\":200},{\"name\":\"agenda\",\"type\":\"string\",\"max_len\":4000},{\"name\":\"job_id\",\"type\":\"string\",\"max_len\":64},{\"name\":\"status\",\"type\":\"enum\",\"values\":[\"scheduled\",\"completed\",\"no_show\",\"cancelled\",\"rescheduled\"]},{\"name\":\"location\",\"type\":\"string\",\"max_len\":200},{\"name\":\"meeting_url\",\"type\":\"url\",\"max_len\":2048},{\"name\":\"send_invite\",\"type\":\"bool\"},{\"name\":\"candidate_id\",\"type\":\"string\",\"max_len\":64},{\"name\":\"scheduled_at\",\"type\":\"string\",\"max_len\":32},{\"name\":\"application_id\",\"type\":\"string\",\"max_len\":64,\"ref\":{\"type\":\"application\",\"owned\":false,\"optional\":false}},{\"name\":\"interviewer_id\",\"type\":\"string\",\"max_len\":64},{\"name\":\"interviewer_ids\",\"type\":\"list\"},{\"name\":\"duration_minutes\",\"type\":\"number\"}]},\"job\":{\"ops\":[\"list\",\"read\",\"create\",\"update\",\"delete\"],\"create_fields\":[\"title\",\"slug\",\"department\",\"location\",\"country\",\"remote\",\"employment_type\",\"seniority\",\"headcount\",\"salary_min\",\"salary_max\",\"currency\",\"salary_visibility\",\"summary\",\"description\",\"responsibilities\",\"requirements\",\"nice_to_have\",\"benefits\",\"language\",\"status\",\"public\",\"ai_screen_enabled\",\"ai_screen_prompt\",\"knockout_questions\",\"screening_questions\",\"tags\",\"hiring_manager_id\",\"team_ids\",\"opened_at\",\"target_close_date\",\"closed_at\",\"external_apply_url\",\"color\"],\"update_fields\":[\"title\",\"slug\",\"department\",\"location\",\"country\",\"remote\",\"employment_type\",\"seniority\",\"headcount\",\"salary_min\",\"salary_max\",\"currency\",\"salary_visibility\",\"summary\",\"description\",\"responsibilities\",\"requirements\",\"nice_to_have\",\"benefits\",\"language\",\"status\",\"public\",\"ai_screen_enabled\",\"ai_screen_prompt\",\"knockout_questions\",\"screening_questions\",\"tags\",\"hiring_manager_id\",\"team_ids\",\"opened_at\",\"target_close_date\",\"closed_at\",\"external_apply_url\",\"color\"],\"allowed_filters\":[\"data__title\",\"data__department\",\"data__location\",\"data__employment_type\",\"data__seniority\",\"data__remote\",\"data__status\",\"data__public\",\"data__hiring_manager_id\",\"data__tags\",\"status\",\"is_archived\",\"owned_by\",\"created_by\"],\"allowed_sorts\":[\"created_at\",\"updated_at\",\"data__title\",\"data__opened_at\",\"data__target_close_date\"],\"default_sort\":\"created_at\",\"max_limit\":200,\"fields\":[{\"name\":\"slug\",\"type\":\"string\",\"max_len\":120},{\"name\":\"tags\",\"type\":\"tags\"},{\"name\":\"color\",\"type\":\"string\",\"max_len\":24},{\"name\":\"title\",\"type\":\"string\",\"max_len\":200},{\"name\":\"public\",\"type\":\"bool\"},{\"name\":\"remote\",\"type\":\"enum\",\"values\":[\"onsite\",\"hybrid\",\"remote\"]},{\"name\":\"status\",\"type\":\"enum\",\"values\":[\"draft\",\"open\",\"paused\",\"closed\",\"filled\"]},{\"name\":\"country\",\"type\":\"string\",\"max_len\":80},{\"name\":\"summary\",\"type\":\"string\",\"max_len\":600},{\"name\":\"benefits\",\"type\":\"string\",\"max_len\":4000},{\"name\":\"currency\",\"type\":\"string\",\"max_len\":8},{\"name\":\"language\",\"type\":\"string\",\"max_len\":16},{\"name\":\"location\",\"type\":\"string\",\"max_len\":120},{\"name\":\"team_ids\",\"type\":\"list\"},{\"name\":\"closed_at\",\"type\":\"string\",\"max_len\":32},{\"name\":\"headcount\",\"type\":\"number\"},{\"name\":\"opened_at\",\"type\":\"string\",\"max_len\":32},{\"name\":\"seniority\",\"type\":\"enum\",\"values\":[\"junior\",\"mid\",\"senior\",\"lead\",\"principal\"]},{\"name\":\"department\",\"type\":\"string\",\"max_len\":120},{\"name\":\"salary_max\",\"type\":\"number\"},{\"name\":\"salary_min\",\"type\":\"number\"},{\"name\":\"description\",\"type\":\"string\",\"max_len\":16000},{\"name\":\"nice_to_have\",\"type\":\"string\",\"max_len\":4000},{\"name\":\"requirements\",\"type\":\"string\",\"max_len\":8000},{\"name\":\"employment_type\",\"type\":\"enum\",\"values\":[\"full_time\",\"part_time\",\"internship\",\"working_student\",\"freelance\",\"contract\"]},{\"name\":\"ai_screen_prompt\",\"type\":\"string\",\"max_len\":4000},{\"name\":\"responsibilities\",\"type\":\"string\",\"max_len\":8000},{\"name\":\"ai_screen_enabled\",\"type\":\"bool\"},{\"name\":\"hiring_manager_id\",\"type\":\"string\",\"max_len\":64},{\"name\":\"salary_visibility\",\"type\":\"enum\",\"values\":[\"public\",\"team\",\"private\"]},{\"name\":\"target_close_date\",\"type\":\"string\",\"max_len\":32},{\"name\":\"external_apply_url\",\"type\":\"url\",\"max_len\":2048},{\"name\":\"knockout_questions\",\"type\":\"list\"},{\"name\":\"screening_questions\",\"type\":\"list\"}]},\"message\":{\"ops\":[\"list\",\"read\",\"create\",\"update\",\"delete\"],\"create_fields\":[\"candidate_id\",\"application_id\",\"channel\",\"direction\",\"subject\",\"body\",\"status\",\"sent_at\",\"delivered_at\",\"read_at\",\"template_id\",\"from_address\",\"to_address\",\"cc_addresses\",\"thread_id\",\"error\"],\"update_fields\":[\"subject\",\"body\",\"status\",\"sent_at\",\"delivered_at\",\"read_at\",\"thread_id\",\"error\"],\"allowed_filters\":[\"data__candidate_id\",\"data__application_id\",\"data__channel\",\"data__status\",\"data__template_id\",\"status\",\"is_archived\",\"owned_by\",\"created_by\"],\"allowed_sorts\":[\"created_at\",\"data__sent_at\"],\"default_sort\":\"-data__sent_at\",\"max_limit\":200,\"fields\":[{\"name\":\"body\",\"type\":\"string\",\"max_len\":16000},{\"name\":\"error\",\"type\":\"string\",\"max_len\":600},{\"name\":\"status\",\"type\":\"enum\",\"values\":[\"draft\",\"queued\",\"sent\",\"delivered\",\"failed\",\"bounced\"]},{\"name\":\"channel\",\"type\":\"enum\",\"values\":[\"email\",\"note\"]},{\"name\":\"read_at\",\"type\":\"string\",\"max_len\":32},{\"name\":\"sent_at\",\"type\":\"string\",\"max_len\":32},{\"name\":\"subject\",\"type\":\"string\",\"max_len\":400},{\"name\":\"direction\",\"type\":\"enum\",\"values\":[\"outbound\",\"inbound\"]},{\"name\":\"thread_id\",\"type\":\"string\",\"max_len\":200},{\"name\":\"to_address\",\"type\":\"string\",\"max_len\":320},{\"name\":\"template_id\",\"type\":\"string\",\"max_len\":64,\"ref\":{\"type\":\"email_template\",\"owned\":false,\"optional\":false}},{\"name\":\"candidate_id\",\"type\":\"string\",\"max_len\":64},{\"name\":\"cc_addresses\",\"type\":\"list\"},{\"name\":\"delivered_at\",\"type\":\"string\",\"max_len\":32},{\"name\":\"from_address\",\"type\":\"string\",\"max_len\":320},{\"name\":\"application_id\",\"type\":\"string\",\"max_len\":64}]},\"offer\":{\"ops\":[\"list\",\"read\",\"create\",\"update\",\"delete\"],\"create_fields\":[\"application_id\",\"candidate_id\",\"job_id\",\"salary_gross\",\"salary_period\",\"currency\",\"bonus\",\"bonus_note\",\"vacation_days\",\"start_date\",\"expires_at\",\"term\",\"term_until\",\"weekly_hours\",\"remote_policy\",\"status\",\"sent_at\",\"decided_at\",\"letter_body\",\"letter_blob_id\",\"decline_reason\"],\"update_fields\":[\"salary_gross\",\"salary_period\",\"currency\",\"bonus\",\"bonus_note\",\"vacation_days\",\"start_date\",\"expires_at\",\"term\",\"term_until\",\"weekly_hours\",\"remote_policy\",\"status\",\"sent_at\",\"decided_at\",\"letter_body\",\"letter_blob_id\",\"decline_reason\"],\"allowed_filters\":[\"data__application_id\",\"data__candidate_id\",\"data__job_id\",\"data__status\",\"data__currency\",\"status\",\"is_archived\",\"owned_by\",\"created_by\"],\"allowed_sorts\":[\"created_at\",\"data__start_date\",\"data__sent_at\"],\"default_sort\":\"created_at\",\"max_limit\":100,\"fields\":[{\"name\":\"term\",\"type\":\"enum\",\"values\":[\"permanent\",\"fixed_term\",\"trial\",\"intern\",\"freelance\"]},{\"name\":\"bonus\",\"type\":\"number\"},{\"name\":\"job_id\",\"type\":\"string\",\"max_len\":64},{\"name\":\"status\",\"type\":\"enum\",\"values\":[\"draft\",\"sent\",\"accepted\",\"declined\",\"withdrawn\",\"expired\"]},{\"name\":\"sent_at\",\"type\":\"string\",\"max_len\":32},{\"name\":\"currency\",\"type\":\"string\",\"max_len\":8},{\"name\":\"bonus_note\",\"type\":\"string\",\"max_len\":600},{\"name\":\"decided_at\",\"type\":\"string\",\"max_len\":32},{\"name\":\"expires_at\",\"type\":\"string\",\"max_len\":32},{\"name\":\"start_date\",\"type\":\"string\",\"max_len\":32},{\"name\":\"term_until\",\"type\":\"string\",\"max_len\":32},{\"name\":\"letter_body\",\"type\":\"string\",\"max_len\":16000},{\"name\":\"candidate_id\",\"type\":\"string\",\"max_len\":64},{\"name\":\"salary_gross\",\"type\":\"number\"},{\"name\":\"weekly_hours\",\"type\":\"number\"},{\"name\":\"remote_policy\",\"type\":\"string\",\"max_len\":200},{\"name\":\"salary_period\",\"type\":\"enum\",\"values\":[\"yearly\",\"monthly\",\"daily\",\"hourly\"]},{\"name\":\"vacation_days\",\"type\":\"number\"},{\"name\":\"application_id\",\"type\":\"string\",\"max_len\":64,\"ref\":{\"type\":\"application\",\"owned\":false,\"optional\":false}},{\"name\":\"decline_reason\",\"type\":\"string\",\"max_len\":600},{\"name\":\"letter_blob_id\",\"type\":\"string\",\"max_len\":64}]},\"source\":{\"ops\":[\"list\",\"read\",\"create\",\"update\",\"delete\"],\"create_fields\":[\"name\",\"kind\",\"url\",\"active\",\"notes\"],\"update_fields\":[\"name\",\"kind\",\"url\",\"active\",\"notes\"],\"allowed_filters\":[\"data__name\",\"data__kind\",\"data__active\",\"status\",\"is_archived\",\"owned_by\",\"created_by\"],\"allowed_sorts\":[\"created_at\",\"data__name\"],\"default_sort\":\"data__name\",\"max_limit\":100,\"fields\":[{\"name\":\"url\",\"type\":\"url\",\"max_len\":2048},{\"name\":\"kind\",\"type\":\"enum\",\"values\":[\"linkedin\",\"indeed\",\"stepstone\",\"xing\",\"honeypot\",\"kununu\",\"careers_page\",\"referral\",\"active_sourcing\",\"agency\",\"event\",\"other\"]},{\"name\":\"name\",\"type\":\"string\",\"max_len\":200},{\"name\":\"notes\",\"type\":\"string\",\"max_len\":2000},{\"name\":\"active\",\"type\":\"bool\"}]},\"task\":{\"ops\":[\"list\",\"read\",\"create\",\"update\",\"delete\"],\"create_fields\":[\"title\",\"description\",\"due_date\",\"completed\",\"completed_at\",\"priority\",\"assigned_to\",\"parent_kind\",\"parent_id\"],\"update_fields\":[\"title\",\"description\",\"due_date\",\"completed\",\"completed_at\",\"priority\",\"assigned_to\"],\"allowed_filters\":[\"data__parent_id\",\"data__parent_kind\",\"data__assigned_to\",\"data__completed\",\"data__priority\",\"status\",\"is_archived\",\"owned_by\",\"created_by\"],\"allowed_sorts\":[\"data__due_date\",\"created_at\",\"data__priority\"],\"default_sort\":\"data__due_date\",\"max_limit\":200,\"fields\":[{\"name\":\"title\",\"type\":\"string\",\"max_len\":200},{\"name\":\"due_date\",\"type\":\"string\",\"max_len\":32},{\"name\":\"priority\",\"type\":\"enum\",\"values\":[\"low\",\"normal\",\"high\",\"urgent\"]},{\"name\":\"completed\",\"type\":\"bool\"},{\"name\":\"parent_id\",\"type\":\"string\",\"max_len\":64},{\"name\":\"assigned_to\",\"type\":\"string\",\"max_len\":64},{\"name\":\"description\",\"type\":\"string\",\"max_len\":4000},{\"name\":\"parent_kind\",\"type\":\"enum\",\"values\":[\"candidate\",\"application\",\"job\"]},{\"name\":\"completed_at\",\"type\":\"string\",\"max_len\":32}]}}"; 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 AtsClient(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/challenge")) .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 applicationList(Map opts) throws ApiException { return requestList("/xapi2/data/application", opts); } public Map applicationGet(String id) throws ApiException { return requestJSON("GET", "/xapi2/data/application/" + id, null); } public Map applicationCreate(Map data) throws ApiException { return requestJSON("POST", "/xapi2/data/application", data); } public Map applicationUpdate(String id, Map data) throws ApiException { return requestJSON("PATCH", "/xapi2/data/application/" + id, data); } public boolean applicationDelete(String id) throws ApiException { requestJSON("DELETE", "/xapi2/data/application/" + id, null); return true; } public Map applicationNoteList(Map opts) throws ApiException { return requestList("/xapi2/data/application_note", opts); } public Map applicationNoteGet(String id) throws ApiException { return requestJSON("GET", "/xapi2/data/application_note/" + id, null); } public Map applicationNoteCreate(Map data) throws ApiException { return requestJSON("POST", "/xapi2/data/application_note", data); } public Map applicationNoteUpdate(String id, Map data) throws ApiException { return requestJSON("PATCH", "/xapi2/data/application_note/" + id, data); } public boolean applicationNoteDelete(String id) throws ApiException { requestJSON("DELETE", "/xapi2/data/application_note/" + id, null); return true; } public Map candidateList(Map opts) throws ApiException { return requestList("/xapi2/data/candidate", opts); } public Map candidateGet(String id) throws ApiException { return requestJSON("GET", "/xapi2/data/candidate/" + id, null); } public Map candidateCreate(Map data) throws ApiException { return requestJSON("POST", "/xapi2/data/candidate", data); } public Map candidateUpdate(String id, Map data) throws ApiException { return requestJSON("PATCH", "/xapi2/data/candidate/" + id, data); } public boolean candidateDelete(String id) throws ApiException { requestJSON("DELETE", "/xapi2/data/candidate/" + id, null); return true; } public Map emailTemplateList(Map opts) throws ApiException { return requestList("/xapi2/data/email_template", opts); } public Map emailTemplateGet(String id) throws ApiException { return requestJSON("GET", "/xapi2/data/email_template/" + id, null); } public Map emailTemplateCreate(Map data) throws ApiException { return requestJSON("POST", "/xapi2/data/email_template", data); } public Map emailTemplateUpdate(String id, Map data) throws ApiException { return requestJSON("PATCH", "/xapi2/data/email_template/" + id, data); } public boolean emailTemplateDelete(String id) throws ApiException { requestJSON("DELETE", "/xapi2/data/email_template/" + id, null); return true; } public Map evaluationList(Map opts) throws ApiException { return requestList("/xapi2/data/evaluation", opts); } public Map evaluationGet(String id) throws ApiException { return requestJSON("GET", "/xapi2/data/evaluation/" + id, null); } public Map evaluationCreate(Map data) throws ApiException { return requestJSON("POST", "/xapi2/data/evaluation", data); } public Map evaluationUpdate(String id, Map data) throws ApiException { return requestJSON("PATCH", "/xapi2/data/evaluation/" + id, data); } public boolean evaluationDelete(String id) throws ApiException { requestJSON("DELETE", "/xapi2/data/evaluation/" + id, null); return true; } public Map interviewList(Map opts) throws ApiException { return requestList("/xapi2/data/interview", opts); } public Map interviewGet(String id) throws ApiException { return requestJSON("GET", "/xapi2/data/interview/" + id, null); } public Map interviewCreate(Map data) throws ApiException { return requestJSON("POST", "/xapi2/data/interview", data); } public Map interviewUpdate(String id, Map data) throws ApiException { return requestJSON("PATCH", "/xapi2/data/interview/" + id, data); } public boolean interviewDelete(String id) throws ApiException { requestJSON("DELETE", "/xapi2/data/interview/" + id, null); return true; } public Map jobList(Map opts) throws ApiException { return requestList("/xapi2/data/job", opts); } public Map jobGet(String id) throws ApiException { return requestJSON("GET", "/xapi2/data/job/" + id, null); } public Map jobCreate(Map data) throws ApiException { return requestJSON("POST", "/xapi2/data/job", data); } public Map jobUpdate(String id, Map data) throws ApiException { return requestJSON("PATCH", "/xapi2/data/job/" + id, data); } public boolean jobDelete(String id) throws ApiException { requestJSON("DELETE", "/xapi2/data/job/" + id, null); return true; } public Map messageList(Map opts) throws ApiException { return requestList("/xapi2/data/message", opts); } public Map messageGet(String id) throws ApiException { return requestJSON("GET", "/xapi2/data/message/" + id, null); } public Map messageCreate(Map data) throws ApiException { return requestJSON("POST", "/xapi2/data/message", data); } public Map messageUpdate(String id, Map data) throws ApiException { return requestJSON("PATCH", "/xapi2/data/message/" + id, data); } public boolean messageDelete(String id) throws ApiException { requestJSON("DELETE", "/xapi2/data/message/" + id, null); return true; } public Map offerList(Map opts) throws ApiException { return requestList("/xapi2/data/offer", opts); } public Map offerGet(String id) throws ApiException { return requestJSON("GET", "/xapi2/data/offer/" + id, null); } public Map offerCreate(Map data) throws ApiException { return requestJSON("POST", "/xapi2/data/offer", data); } public Map offerUpdate(String id, Map data) throws ApiException { return requestJSON("PATCH", "/xapi2/data/offer/" + id, data); } public boolean offerDelete(String id) throws ApiException { requestJSON("DELETE", "/xapi2/data/offer/" + id, null); return true; } public Map sourceList(Map opts) throws ApiException { return requestList("/xapi2/data/source", opts); } public Map sourceGet(String id) throws ApiException { return requestJSON("GET", "/xapi2/data/source/" + id, null); } public Map sourceCreate(Map data) throws ApiException { return requestJSON("POST", "/xapi2/data/source", data); } public Map sourceUpdate(String id, Map data) throws ApiException { return requestJSON("PATCH", "/xapi2/data/source/" + id, data); } public boolean sourceDelete(String id) throws ApiException { requestJSON("DELETE", "/xapi2/data/source/" + id, null); return true; } public Map taskList(Map opts) throws ApiException { return requestList("/xapi2/data/task", opts); } public Map taskGet(String id) throws ApiException { return requestJSON("GET", "/xapi2/data/task/" + id, null); } public Map taskCreate(Map data) throws ApiException { return requestJSON("POST", "/xapi2/data/task", data); } public Map taskUpdate(String id, Map data) throws ApiException { return requestJSON("PATCH", "/xapi2/data/task/" + id, data); } public boolean taskDelete(String id) throws ApiException { requestJSON("DELETE", "/xapi2/data/task/" + id, null); return true; } }