test




import java.util.*;
        import java.util.concurrent.ConcurrentHashMap;

public class ApiPlatformRegistry {

    // Ключ: "ClassName#methodName", например "PersonController#read"
    // Значение: список аннотаций с этого метода
    private static final Map<String, List<PlatformApiResponse>> REGISTRY = new ConcurrentHashMap<>();

    public static void register(String key, List<PlatformApiResponse> responses) {
        REGISTRY.put(key, responses);
    }

    public static Map<String, List<PlatformApiResponse>> getAll() {
        return Collections.unmodifiableMap(REGISTRY);
    }
}

// -----

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import io.quarkus.runtime.StartupEvent;

import java.lang.reflect.Method;
import java.util.Arrays;

@ApplicationScoped
public class ApiPlatformScanner {

    // Сюда добавляете свои контроллеры
    private static final Class<?>[] CONTROLLERS = {
            // com.banank.platform.services.profile.controller.PersonController.class
    };

    public void onStart(@Observes StartupEvent event) {
        for (Class<?> controller : CONTROLLERS) {
            for (Method method : controller.getDeclaredMethods()) {

                String key = controller.getSimpleName() + "#" + method.getName();

                // Случай 1: несколько аннотаций → Quarkus оборачивает их в контейнер
                PlatformApiResponses container = method.getAnnotation(PlatformApiResponses.class);
                if (container != null) {
                    ApiPlatformRegistry.register(key, Arrays.asList(container.value()));
                    continue;
                }

                // Случай 2: одна аннотация без контейнера
                PlatformApiResponse single = method.getAnnotation(PlatformApiResponse.class);
                if (single != null) {
                    ApiPlatformRegistry.register(key, java.util.List.of(single));
                }
            }
        }
    }
}

//--
package com.banank.platform.services.profile.exception.utils;
import jakarta.ws.rs.container.ResourceInfo;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.jboss.resteasy.reactive.server.ServerExceptionMapper;

import java.lang.reflect.Method;

@Slf4j
public class GlobalExceptionHandler {

    @Context
    ResourceInfo resourceInfo;


    @ServerExceptionMapper
    public Response handleException(RuntimeException ex) {
        Method method = resourceInfo.getResourceMethod();
        if (method != null) {
            ExceptionMapping mapping = findMappingFromAnnotations(method, ex.getClass());
            if (mapping != null) {
                return createResponse(mapping, ex);
            }

            Class<?> resourceClass = resourceInfo.getResourceClass();
            mapping = findMappingFromAnnotations(resourceClass, ex.getClass());
            if (mapping != null) {
                return createResponse(mapping, ex);
            }
        }

        log.error("Unexpected error: {}", ex.getMessage(), ex);
        return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                .entity(new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred"))
                .build();
    }

    private ExceptionMapping findMappingFromAnnotations(Method method, Class<? extends Exception> exceptionClass) {
        PlatformApiResponses apiErrorResponses = method.getAnnotation(PlatformApiResponses.class);
        if (apiErrorResponses != null) {
            for (PlatformApiResponse apiErrorResponse : apiErrorResponses.value()) {
                if (apiErrorResponse.exception().isAssignableFrom(exceptionClass)) {
                    return new ExceptionMapping(
                            Integer.parseInt(apiErrorResponse.responseCode()),
                            apiErrorResponse.responseCode(),
                            apiErrorResponse.description()
                    );
                }
            }
        }

        PlatformApiResponse apiErrorResponse = method.getAnnotation(PlatformApiResponse.class);
        if (apiErrorResponse != null && apiErrorResponse.exception().isAssignableFrom(exceptionClass)) {
            return new ExceptionMapping(
                    Integer.parseInt(apiErrorResponse.responseCode()),
                    apiErrorResponse.responseCode(),
                    apiErrorResponse.description()
            );
        }

        return null;
    }

    private ExceptionMapping findMappingFromAnnotations(Class<?> clazz, Class<? extends Exception> exceptionClass) {
        PlatformApiResponses apiErrorResponses = clazz.getAnnotation(PlatformApiResponses.class);
        if (apiErrorResponses != null) {
            for (PlatformApiResponse apiErrorResponse : apiErrorResponses.value()) {
                if (apiErrorResponse.exception().isAssignableFrom(exceptionClass)) {
                    return new ExceptionMapping(
                            Integer.parseInt(apiErrorResponse.responseCode()),
                            apiErrorResponse.responseCode(),
                            apiErrorResponse.description()
                    );
                }
            }
        }

        PlatformApiResponse apiErrorResponse = clazz.getAnnotation(PlatformApiResponse.class);
        if (apiErrorResponse != null && apiErrorResponse.exception().isAssignableFrom(exceptionClass)) {
            return new ExceptionMapping(
                    Integer.parseInt(apiErrorResponse.responseCode()),
                    apiErrorResponse.responseCode(),
                    apiErrorResponse.description()
            );
        }

        return null;
    }

    private Response createResponse(ExceptionMapping mapping, Exception ex) {
        Response.Status status = Response.Status.fromStatusCode(mapping.status);

        // Приоритет: 1) ex.getMessage(), 2) defaultMessage, 3) description
        String message = determineMessage(ex, mapping);

        if (status.getFamily() == Response.Status.Family.SERVER_ERROR) {
            log.error("{}: {}", mapping.code, message, ex);
        } else {
            log.warn("{}: {}", mapping.code, message);
        }

        return Response.status(status)
                .entity(new ErrorResponse(mapping.code, message))
                .build();
    }

    private String determineMessage(Exception ex, ExceptionMapping mapping) {
        // 1. Пытаемся взять сообщение из исключения
        String exceptionMessage = ex.getMessage();
        if (exceptionMessage != null && !exceptionMessage.isBlank()) {
            return exceptionMessage;
        }


        // 3. Fallback на description
        return mapping.description;
    }

    public record ErrorResponse(String code, String message) {}

    private record ExceptionMapping(int status, String code, String description) {}
}

// -- package com.banank.platform.services.profile.exception.utils;

import org.eclipse.microprofile.openapi.OASFactory;
import org.eclipse.microprofile.openapi.OASFilter;
import org.eclipse.microprofile.openapi.models.Operation;
import org.eclipse.microprofile.openapi.models.media.Content;
import org.eclipse.microprofile.openapi.models.responses.APIResponse;
import org.eclipse.microprofile.openapi.models.responses.APIResponses;

import java.util.List;
import java.util.Map;

public class PlatformApiOASFilter implements OASFilter {

    @Override
    public Operation filterOperation(Operation operation) {
        if (operation == null || operation.getOperationId() == null) {
            return operation;
        }

        String operationId = operation.getOperationId();

        for (Map.Entry<String, List<PlatformApiResponse>> entry : ApiPlatformRegistry.getAll().entrySet()) {
            String[] parts = entry.getKey().split("#");
            String className = parts[0];
            String methodName = parts[1];

            if (!operationId.contains(className) || !operationId.contains(methodName)) {
                continue;
            }

            APIResponses responses = operation.getResponses();
            if (responses == null) {
                responses = OASFactory.createAPIResponses();
                operation.setResponses(responses);
            }

            for (PlatformApiResponse error : entry.getValue()) {
                APIResponse apiResponse = OASFactory.createAPIResponse()
                        .description(error.description());

                if (error.content().length > 0) {
                    Content content = OASFactory.createContent();
                    for (var c : error.content()) {
                        content.addMediaType(c.mediaType(), OASFactory.createMediaType());
                    }
                    apiResponse.setContent(content);
                }

                responses.addAPIResponse(error.responseCode(), apiResponse);
            }
        }

        return operation;
    }
}

// --
/**
 * Copyright (c) 2017 Contributors to the Eclipse Foundation
 * Copyright 2017 SmartBear Software
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.banank.platform.services.profile.exception.utils;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
import org.eclipse.microprofile.openapi.annotations.headers.Header;
import org.eclipse.microprofile.openapi.annotations.links.Link;
import org.eclipse.microprofile.openapi.annotations.media.Content;

/**
 * The APIResponse annotation corresponds to the OpenAPI Response model object which describes a single response from an
 * API Operation, including design-time, static links to operations based on the response.
 * <p>
 * When this annotation is applied to a Jakarta REST method the response is added to the responses defined in the
 * corresponding OpenAPI operation. If the operation already has a response with the specified responseCode the
 * annotation on the method is ignored.
 *
 * <pre>
 * &#64;APIResponse(responseCode = "200", description = "Calculate load size", content = {
 *         &#64;Content(mediaType = "application/json", Schema = &#64;Schema(type = "integer"))})
 * &#64;GET
 * public getLuggageWeight(Flight id) {
 *     return getBagWeight(id) + getCargoWeight(id);
 * }
 * </pre>
 * <p>
 * When this annotation is applied to a Jakarta REST resource class, the response is added to the responses defined in
 * all OpenAPI operations which correspond to a method on that class. If an operation already has a response with the
 * specified responseCode the response is not added to that operation.
 *
 * <p>
 * When this annotation is applied to an <code>ExceptionMapper</code> class or <code>toResponse</code> method, it allows
 * developers to describe the API response that will be added to a generated OpenAPI operation based on a Jakarta REST
 * method that declares an <code>Exception</code> of the type handled by the <code>ExceptionMapper</code>.
 *
 * <pre>
 * &#64;Provider
 * public class NotFoundExceptionMapper implements ExceptionMapper&lt;NotFoundException&gt; {
 *     &#64;Override
 *     &#64;APIResponse(responseCode = "404", description = "Not Found")
 *     public Response toResponse(NotFoundException t) {
 *         return Response.status(404)
 *                 .type(MediaType.TEXT_PLAIN)
 *                 .entity("Not found")
 *                 .build();
 *     }
 * }
 * </pre>
 *
 * @see <a href="https://spec.openapis.org/oas/v3.1.0.html#response-object">OpenAPI Specification Response Object</a>
 *
 **/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Repeatable(PlatformApiResponses.class)
public @interface PlatformApiResponse {

    Class<? extends Exception> exception();

    /**
     * A short description of the response. It is a REQUIRED property unless this is only a reference to a response
     * instance.
     *
     * @return description of the response.
     **/
    String description() default "";

    /**
     * The HTTP response code, or 'default', for the supplied response. May only have 1 default entry.
     *
     * @return HTTP response code for this response instance or default
     **/
    String responseCode() default "default";

    /**
     * An array of response headers. Allows additional information to be included with response.
     * <p>
     * RFC7230 states header names are case insensitive. If a response header is defined with the name "Content-Type",
     * it SHALL be ignored.
     *
     * @return array of headers for this response instance
     **/
    Header[] headers() default {};

    /**
     * An array of operation links that can be followed from the response.
     *
     * @return array of operation links for this response instance
     **/
    Link[] links() default {};

    /**
     * An array containing descriptions of potential response payloads for different media types.
     *
     * @return content of this response instance
     **/
    Content[] content() default {};

    /**
     * The unique name to identify this response. Only REQUIRED when the response is defined within
     * {@link org.eclipse.microprofile.openapi.annotations.Components}. The name will be used as the key to add this
     * response to the 'responses' map for reuse.
     *
     * @return this response's name
     **/
    String name() default "";

    /**
     * Reference value to a Response object.
     * <p>
     * This property provides a reference to an object defined elsewhere. This property may be used with
     * {@link #description()} but is mutually exclusive with all other properties. If properties other than
     * {@code description} are defined in addition to the {@code ref} property then the result is undefined.
     *
     * @return reference to a response
     **/
    String ref() default "";

    /**
     * List of extensions to be added to the {@link org.eclipse.microprofile.openapi.models.responses.APIResponse
     * APIResponse} model corresponding to the containing annotation.
     *
     * @return array of extensions
     *
     * @since 3.1
     */
    Extension[] extensions() default {};
}

//--
package com.banank.platform.services.profile.exception.utils;

import java.lang.annotation.*;

// Контейнер для @Repeatable — без него несколько @ApiErrorResponse на метод не заработает
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface PlatformApiResponses {
    PlatformApiResponse[] value();
}
// --

@Slf4j
@Path("/profile/person")
@RequireUserId
@Tags({
        @Tag(name = "banank-identification"),
        @Tag(name = "Profile Person", description = "Profile Person CRUD")
})
public class PersonResource {

    @Inject
    UserContext userContext;


    @Inject
    PersonService personService;

    @PUT
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "Set profile person", description = "Receives person data and save it")
    @Parameters({
        @Parameter(name = "X-User-Id", description = "User ID required", in = ParameterIn.HEADER, required = true, schema = @Schema(type = SchemaType.STRING))
    })
    @APIResponses({
            @APIResponse(responseCode = "204", description = "Person successfully created"),
            @APIResponse(responseCode = "400", description = "Invalid person payload")
    })
    public Response update(@Valid CreatePersonRequestDto request) {
        log.info("Received request: {}", request);
        String userId = userContext.getUserId();

        var person = new Person();
        person.setName(request.name());
        person.setLastName(request.lastName());
        person.setDateOfBirth(request.dateOfBirth());

        try {
            personService.putIfKycNotPassed(userId, person);
        } catch (IllegalStateException e) {
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
        }

        return Response.ok().build();
    }



    @GET
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "Get profile person", description = "Get profile person data")
    @Parameters({
            @Parameter(name = "X-User-Id", description = "User ID required", in = ParameterIn.HEADER, required = true, schema = @Schema(type = SchemaType.STRING))
    })
 /*   @APIResponses({
            @APIResponse(responseCode = "204", description = "Successfully initialized", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = GetPersonResponseDto.class))),
            @APIResponse(responseCode = "400", description = "Invalid request"),
    })
*/
    @PlatformApiResponse(
            exception = PersonGetFailedException.class,
            responseCode = "409",
            description = "Person Read Failed"
    )
    public Response read() {
        String userId = userContext.getUserId();
        Optional<Person> maybePerson = personService.get(userId);

        if (maybePerson.isEmpty()) {
            return Response.status(Response.Status.NOT_FOUND).build();
        }

        Person person = maybePerson.get();

        GetPersonResponseDto response = new GetPersonResponseDto(person.getName(), person.getLastName(), person.getDateOfBirth());
        return Response.ok().entity(response).build();
    }


}
все ли тут верно если я хочу отображать PlatformApiResponse в swagger?