001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server.mcp.v20260728; 018 019 020import java.util.*; 021 022import org.apache.juneau.bean.mcp.v20260728.*; 023import org.apache.juneau.marshall.marshaller.*; 024import static org.apache.juneau.commons.utils.Shorts.*; 025 026/** 027 * Static helper that parses typed {@link ElicitResult}s out of an {@link McpMrtrResumeContext} (MCP 028 * {@code 2026-07-28} SEP-2322 elicitation, riding the Multi-Round-Trip Requests loop). 029 * 030 * <p> 031 * {@link McpMrtrResumeContext#inputResponses()} values are generic JSON (a {@code JsonMap}/{@code JsonList}/ 032 * boxed primitive/{@code String} after wire deserialization, never a typed bean automatically); this helper 033 * converts each to {@link ElicitResult} via the same {@code Json.to(Json.of(value), type)} technique 034 * {@link McpMrtrResumeContext#continuationAs(Class)} already uses for the continuation side, just applied 035 * per-key across the map instead of to the single continuation value. 036 */ 037public final class ElicitationResponses { 038 039 private ElicitationResponses() {} 040 041 /** 042 * Returns the typed answer for a single question id. 043 * 044 * @param ctx The resume context. Must not be <jk>null</jk>. 045 * @param id The server-assigned id this question was posed under. Must not be <jk>null</jk>. 046 * @return The typed answer, or <jk>null</jk> if {@code id} is absent from {@code ctx}'s responses, or maps 047 * to <jk>null</jk>. 048 * @throws IllegalArgumentException If {@code ctx} or {@code id} is <jk>null</jk>. 049 * @throws RuntimeException If the answer's decoded shape cannot be converted to {@link ElicitResult}. 050 */ 051 public static ElicitResult get(McpMrtrResumeContext ctx, String id) { 052 reqnn("ctx", ctx); 053 reqnn("id", id); 054 var value = ctx.inputResponses().get(id); 055 if (value == null) 056 return null; 057 return Json.to(Json.of(value), ElicitResult.class); 058 } 059 060 /** 061 * Returns every answer in {@code ctx}, converted to {@link ElicitResult}. 062 * 063 * @param ctx The resume context. Must not be <jk>null</jk>. 064 * @return A keyed map of typed answers. Never <jk>null</jk> (empty if {@code ctx} carried none). Values 065 * may be <jk>null</jk> when the corresponding raw entry is absent/<jk>null</jk> (see {@link #get}). 066 * @throws IllegalArgumentException If {@code ctx} is <jk>null</jk>. 067 * @throws RuntimeException If any answer's decoded shape cannot be converted to {@link ElicitResult}. 068 */ 069 public static Map<String,ElicitResult> all(McpMrtrResumeContext ctx) { 070 reqnn("ctx", ctx); 071 Map<String,ElicitResult> out = new LinkedHashMap<>(); 072 ctx.inputResponses().keySet().forEach(id -> out.put(id, get(ctx, id))); 073 return out; 074 } 075 076 /** 077 * Returns a boolean field's value from a single question's answer, decline-safe. 078 * 079 * <p> 080 * Treats a missing id, a missing field, or a non-{@link ElicitAction#ACCEPT} action (declined, cancelled) 081 * as <jk>false</jk> rather than requiring the caller to null-check {@link #get} and its content map by 082 * hand — the safe default for a confirmation question is to never proceed on ambiguity. 083 * 084 * @param ctx The resume context. Must not be <jk>null</jk>. 085 * @param id The server-assigned id this question was posed under. Must not be <jk>null</jk>. 086 * @param field The field name within the answer's content map. Must not be <jk>null</jk>. 087 * @return <jk>true</jk> only if {@code id} is present, the action is {@link ElicitAction#ACCEPT}, and 088 * {@code field} is present in the content map with value {@link Boolean#TRUE}. <jk>false</jk> in every 089 * other case. 090 * @throws IllegalArgumentException If {@code ctx}, {@code id}, or {@code field} is <jk>null</jk>. 091 * @throws RuntimeException If the answer's decoded shape cannot be converted to {@link ElicitResult}. 092 */ 093 public static boolean getBoolean(McpMrtrResumeContext ctx, String id, String field) { 094 reqnn("ctx", ctx); 095 reqnn("id", id); 096 reqnn("field", field); 097 var answer = get(ctx, id); 098 if (answer == null || answer.getAction() != ElicitAction.ACCEPT) 099 return false; 100 var content = answer.getContent(); 101 return content != null && Boolean.TRUE.equals(content.get(field)); 102 } 103 104 /** 105 * Returns a string field's value from a single question's answer, decline-safe. 106 * 107 * <p> 108 * Treats a missing id, a missing field, or a non-{@link ElicitAction#ACCEPT} action (declined, cancelled) 109 * as <jk>null</jk> rather than requiring the caller to null-check {@link #get} and its content map by hand. 110 * 111 * @param ctx The resume context. Must not be <jk>null</jk>. 112 * @param id The server-assigned id this question was posed under. Must not be <jk>null</jk>. 113 * @param field The field name within the answer's content map. Must not be <jk>null</jk>. 114 * @return The field's value as a {@link String}, or <jk>null</jk> if {@code id} is absent, the action is 115 * not {@link ElicitAction#ACCEPT}, {@code field} is absent, or its value is not a {@link String}. 116 * @throws IllegalArgumentException If {@code ctx}, {@code id}, or {@code field} is <jk>null</jk>. 117 * @throws RuntimeException If the answer's decoded shape cannot be converted to {@link ElicitResult}. 118 */ 119 public static String getString(McpMrtrResumeContext ctx, String id, String field) { 120 reqnn("ctx", ctx); 121 reqnn("id", id); 122 reqnn("field", field); 123 var answer = get(ctx, id); 124 if (answer == null || answer.getAction() != ElicitAction.ACCEPT) 125 return null; 126 var content = answer.getContent(); 127 if (content == null) 128 return null; 129 return content.get(field) instanceof String s ? s : null; 130 } 131}