001/* 002 * Copyright (c) 2026 Singular 003 * SPDX-License-Identifier: MIT 004 */ 005package ai.singlr.runtime; 006 007import io.helidon.common.GenericType; 008import io.helidon.common.media.type.MediaTypes; 009import io.helidon.http.Headers; 010import io.helidon.http.HttpMediaType; 011import io.helidon.http.WritableHeaders; 012import io.helidon.http.media.EntityReader; 013import io.helidon.http.media.EntityWriter; 014import io.helidon.http.media.MediaSupport; 015import tools.jackson.databind.ObjectMapper; 016import tools.jackson.databind.json.JsonMapper; 017 018/** 019 * Helidon {@link MediaSupport} backed by Jackson 3.x. Wire via {@code WebServer.builder() 020 * .mediaContext(it -> it.addMediaSupport(JacksonSupport.create(mapper)))}. 021 */ 022public final class JacksonSupport implements MediaSupport { 023 024 private final ObjectMapper objectMapper; 025 private final JacksonReader<?> reader; 026 private final JacksonWriter<?> writer; 027 028 private JacksonSupport(ObjectMapper objectMapper) { 029 this.objectMapper = objectMapper; 030 this.reader = new JacksonReader<>(objectMapper); 031 this.writer = new JacksonWriter<>(objectMapper); 032 } 033 034 /** 035 * Create a {@code JacksonSupport} backed by a default {@link JsonMapper}. 036 * 037 * @return a fresh instance 038 */ 039 public static JacksonSupport create() { 040 return new JacksonSupport(JsonMapper.builder().build()); 041 } 042 043 /** 044 * Create a {@code JacksonSupport} backed by the caller's {@link ObjectMapper}. 045 * 046 * @param objectMapper non-null mapper 047 * @return a fresh instance 048 */ 049 public static JacksonSupport create(ObjectMapper objectMapper) { 050 return new JacksonSupport(objectMapper); 051 } 052 053 /** 054 * The wrapped mapper. Useful for handlers that need to (de)serialize directly without going 055 * through Helidon's media context. 056 * 057 * @return the mapper this support owns 058 */ 059 public ObjectMapper objectMapper() { 060 return objectMapper; 061 } 062 063 @Override 064 public String name() { 065 return "jackson"; 066 } 067 068 @Override 069 public String type() { 070 return "jackson"; 071 } 072 073 @SuppressWarnings("unchecked") 074 @Override 075 public <T> ReaderResponse<T> reader(GenericType<T> type, Headers headers) { 076 if (isJsonContentType(headers)) { 077 return new ReaderResponse<>(SupportLevel.SUPPORTED, () -> (EntityReader<T>) reader); 078 } 079 if (isSupportedType(type)) { 080 return new ReaderResponse<>(SupportLevel.COMPATIBLE, () -> (EntityReader<T>) reader); 081 } 082 return ReaderResponse.unsupported(); 083 } 084 085 @SuppressWarnings("unchecked") 086 @Override 087 public <T> WriterResponse<T> writer( 088 GenericType<T> type, Headers requestHeaders, WritableHeaders<?> responseHeaders) { 089 if (isJsonAccepted(requestHeaders)) { 090 return new WriterResponse<>(SupportLevel.SUPPORTED, () -> (EntityWriter<T>) writer); 091 } 092 if (isSupportedType(type)) { 093 return new WriterResponse<>(SupportLevel.COMPATIBLE, () -> (EntityWriter<T>) writer); 094 } 095 return WriterResponse.unsupported(); 096 } 097 098 @SuppressWarnings("unchecked") 099 @Override 100 public <T> ReaderResponse<T> reader( 101 GenericType<T> type, Headers requestHeaders, Headers responseHeaders) { 102 if (isJsonContentType(responseHeaders)) { 103 return new ReaderResponse<>(SupportLevel.SUPPORTED, () -> (EntityReader<T>) reader); 104 } 105 if (isSupportedType(type)) { 106 return new ReaderResponse<>(SupportLevel.COMPATIBLE, () -> (EntityReader<T>) reader); 107 } 108 return ReaderResponse.unsupported(); 109 } 110 111 @SuppressWarnings("unchecked") 112 @Override 113 public <T> WriterResponse<T> writer(GenericType<T> type, WritableHeaders<?> requestHeaders) { 114 if (isSupportedType(type)) { 115 return new WriterResponse<>(SupportLevel.COMPATIBLE, () -> (EntityWriter<T>) writer); 116 } 117 return WriterResponse.unsupported(); 118 } 119 120 private boolean isJsonContentType(Headers headers) { 121 return headers.contentType().map(ct -> ct.test(MediaTypes.APPLICATION_JSON)).orElse(false); 122 } 123 124 private boolean isJsonAccepted(Headers headers) { 125 for (HttpMediaType acceptedType : headers.acceptedTypes()) { 126 if (acceptedType.test(MediaTypes.APPLICATION_JSON)) { 127 return true; 128 } 129 } 130 return false; 131 } 132 133 private <T> boolean isSupportedType(GenericType<T> type) { 134 var rawType = type.rawType(); 135 return !rawType.equals(String.class) && !rawType.equals(byte[].class) && !rawType.isArray(); 136 } 137}