JAX-RS - Як разом повернути код статусу JSON та HTTP?


248

Я пишу веб-додаток REST (NetBeans 6.9, JAX-RS, TopLink Essentials) і намагаюся повернути код статусу JSON та HTTP. У мене готовий і працює код, який повертає JSON, коли від клієнта викликається метод HTTP GET. По суті:

@Path("get/id")
@GET
@Produces("application/json")
public M_機械 getMachineToUpdate(@PathParam("id") String id) {

    // some code to return JSON ...

    return myJson;
}

Але я також хочу повернути код статусу HTTP (500, 200, 204 тощо) разом із даними JSON.

Я намагався використовувати HttpServletResponse:

response.sendError("error message", 500);

Але це змусило браузер вважати, що це "справжні" 500, тому вихідна веб-сторінка була звичайною сторінкою помилок HTTP 500.

Я хочу повернути код статусу HTTP, щоб мій клієнтський JavaScript міг обробляти певну логіку залежно від нього (наприклад, відобразити код помилки та повідомлення на сторінці HTML). Чи можливо це чи не слід використовувати коди статусу HTTP для подібних дій?


2
Яка різниця між 500, які ви хочете (нереально? :)) і справжніми 500?
бритва

@razor Тут справжня 500 означає сторінку помилки HTML замість відповіді JSON
Nupur

Веб-браузер розроблений не для роботи з JSON, а з HTML-сторінками, тому якщо ви відповідаєте на 500 (і навіть на деякий корпус повідомлення), браузер може показати вам лише повідомлення про помилку (насправді це залежить від реалізації браузера), просто тому, що це корисно для нормальний користувач.
бритва

Відповіді:


347

Ось приклад:

@GET
@Path("retrieve/{uuid}")
public Response retrieveSomething(@PathParam("uuid") String uuid) {
    if(uuid == null || uuid.trim().length() == 0) {
        return Response.serverError().entity("UUID cannot be blank").build();
    }
    Entity entity = service.getById(uuid);
    if(entity == null) {
        return Response.status(Response.Status.NOT_FOUND).entity("Entity not found for UUID: " + uuid).build();
    }
    String json = //convert entity to json
    return Response.ok(json, MediaType.APPLICATION_JSON).build();
}

Погляньте на клас Відповідь .

Зауважте, що ви завжди повинні вказувати тип вмісту, особливо якщо ви передаєте кілька типів вмісту, але якщо кожне повідомлення буде представлено як JSON, ви можете просто анотувати метод за допомогою @Produces("application/json")


12
Це працює, але те, що мені не подобається у значенні повернення Response - це, на мою думку, це забруднює ваш код, особливо стосовно будь-якого клієнта, який намагається його використовувати. Якщо ви надаєте інтерфейс, який повертає Відповідь третій стороні, він не знає, який тип ви справді повертаєте. Весна робить це більш зрозумілим з анотацією, дуже корисно, якщо ви завжди повертаєте код статусу (тобто HTTP 204)
Guido

19
Зробити цей клас загальним (Response <T>) було б цікавим покращенням для jax-rs, щоб мати переваги обох альтернатив.
Гвідо

41
Немає необхідності якось перетворювати сутність у json. return Response.status(Response.Status.Forbidden).entity(myPOJO).build();Роботи можна робити так, як хочете return myPOJO;, але з додатковим налаштуванням коду HTTP-статусу.
kratenko

1
Я думаю, що відокремлення бізнес-логіки на окремий клас обслуговування працює добре. Кінцева точка використовує Response як тип повернення, і це методи, як правило, лише дзвінки до сервісних методів плюс анотації шляху та парами. Це чітко відокремлює логіку від відображення типу URL-вмісту / вмісту (де гума потрапляє на дорогу, так би мовити).
Штійн де Вітт

насправді можна просто повернути об'єкт, який не загортається, у відповідь.
SES

192

Існує кілька випадків використання для встановлення кодів статусу HTTP у веб-службі REST, і принаймні один не був достатньо задокументований у існуючих відповідях (тобто, коли ви використовуєте автоматичну магічну серіалізацію JSON / XML за допомогою JAXB, і ви хочете повернути об'єкт, який підлягає серіалізації, але також код статусу, відмінний від типового 200).

Тому дозвольте спробувати перерахувати різні випадки використання та рішення для кожного з них:

1. Код помилки (500, 404, ...)

Найпоширеніший випадок використання, коли потрібно повернути код статусу, інший, ніж 200 OKколи виникає помилка.

Наприклад:

  • сутність запитується, але вона не існує (404)
  • запит семантично невірний (400)
  • користувач не має права (401)
  • є проблема з підключенням до бази даних (500)
  • тощо.

а) Киньте виняток

У цьому випадку я думаю, що найчистіший спосіб вирішити проблему - це викинути виняток. Цей виняток буде оброблятися тим ExceptionMapper, що перетворить виняток у відповідь з відповідним кодом помилки.

Ви можете використовувати за замовчуванням, ExceptionMapperякий поставляється заздалегідь налаштований з Джерсі (і, мабуть, це те саме з іншими реалізаціями), і кинути будь-який із існуючих підкласів javax.ws.rs.WebApplicationException. Це заздалегідь визначені типи винятків, які попередньо відображені у різних кодах помилок, наприклад:

  • BadRequestException (400)
  • InternalServerErrorException (500)
  • NotFoundException (404)

І т.д. Ви можете знайти тут список: API

Можна також визначити власні винятки та ExceptionMapperкласи та додати ці картографи до Джерсі за допомогою @Providerанотації ( джерело цього прикладу ):

public class MyApplicationException extends Exception implements Serializable
{
    private static final long serialVersionUID = 1L;
    public MyApplicationException() {
        super();
    }
    public MyApplicationException(String msg)   {
        super(msg);
    }
    public MyApplicationException(String msg, Exception e)  {
        super(msg, e);
    }
}

Постачальник:

    @Provider
    public class MyApplicationExceptionHandler implements ExceptionMapper<MyApplicationException> 
    {
        @Override
        public Response toResponse(MyApplicationException exception) 
        {
            return Response.status(Status.BAD_REQUEST).entity(exception.getMessage()).build();  
        }
    }

Примітка. Ви також можете написати ExceptionMappers для існуючих типів винятків, які ви використовуєте.

b) Використовуйте конструктор відповідей

Ще один спосіб встановлення коду статусу - це використання Responseконструктора для побудови відповіді з призначеним кодом.

У такому випадку має бути тип повернення вашого методу javax.ws.rs.core.Response. Це описується в різних інших відповідях, таких як прийнята відповідь його прокляття і виглядає так:

@GET
@Path("myresource({id}")
public Response retrieveSomething(@PathParam("id") String id) {
    ...
    Entity entity = service.getById(uuid);
    if(entity == null) {
        return Response.status(Response.Status.NOT_FOUND).entity("Resource not found for ID: " + uuid).build();
    }
    ...
}

2. Успіх, але не 200

Інший випадок, коли ви хочете встановити статус повернення, - це коли операція пройшла успішно, але ви хочете повернути код успіху, відмінний від 200, разом із вмістом, який ви повертаєте в тілі.

Частий випадок використання - це коли ви створюєте нову сутність ( POSTзапит) і хочете повернути інформацію про цю нову сутність або, можливо, про саму сутність разом із 201 Createdкодом статусу.

Один із підходів полягає в тому, щоб використовувати об’єкт відповіді так, як описано вище, і самостійно встановити тіло запиту. Однак цим ви втрачаєте можливість використовувати автоматичну серіалізацію до XML або JSON, надану JAXB.

Це оригінальний метод, що повертає об'єкт сутності, який буде серіалізований в JSON JAXB:

@Path("/")
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public User addUser(User user){
    User newuser = ... do something like DB insert ...
    return newuser;
}

Це поверне JSON-представлення новоствореного користувача, але статус повернення буде 200, а не 201.

Тепер проблема полягає в тому, що якщо я хочу використовувати Responseбудівельник для встановлення коду повернення, я повинен повернути Responseоб'єкт у своєму методі. Як мені все-таки повернути Userоб'єкт, який підлягає серіалізації?

а) Встановіть код на відповідь сервлета

Один із підходів до вирішення цього питання - отримати об’єкт запиту сервлетів і встановити код відповіді вручну самостійно, як це було продемонстровано у відповіді Гаретта Вілсона:

@Path("/")
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public User addUser(User user, @Context final HttpServletResponse response){

    User newUser = ...

    //set HTTP code to "201 Created"
    response.setStatus(HttpServletResponse.SC_CREATED);
    try {
        response.flushBuffer();
    }catch(Exception e){}

    return newUser;
}

Метод як і раніше повертає об'єкт сутності, і код стану буде 201.

Зауважте, що для того, щоб це спрацювало, мені довелося стерти відповідь. Це неприємне відродження низькорівневого коду API сервлетів у нашому прекрасному ресурсі JAX_RS, і що ще гірше, це призводить до того, що заголовки після цього неможливо змінити, оскільки вони вже були відправлені на провід.

b) Використовуйте об'єкт відповіді з сутністю

Найкращим рішенням у цьому випадку є використання об’єкта Response та встановлення сутності для серіалізації на цьому об'єкті відповіді. Було б непогано зробити об’єкт Response узагальненим, щоб вказати тип сукупності корисного навантаження в цьому випадку, але наразі це не так.

@Path("/")
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public Response addUser(User user){

    User newUser = ...

    return Response.created(hateoas.buildLinkUri(newUser, "entity")).entity(restResponse).build();
}

У цьому випадку ми використовуємо створений метод класу Builder Response для того, щоб встановити код статусу на 201. Ми передаємо об'єкт (користувач) сутності у відповідь за допомогою методу entity ().

Результат полягає в тому, що HTTP-код - 401, як ми хотіли, а тіло відповіді - це точно такий же JSON, як і раніше, коли ми щойно повертали об'єкт User. До нього також додається заголовок місцезнаходження.

Клас відповідей має ряд методів побудови для різних статусів (stati?), Таких як:

Response.accepted () Response.ok () Response.noContent () Response.notAcceptable ()

Примітка: об’єкт hateoas - це клас помічників, який я розробив для створення URI ресурсів. Тут вам потрібно буде придумати свій власний механізм;)

Ось про це.

Я сподіваюся, що ця тривала відповідь комусь допоможе :)


Цікаво, чи існує чистий спосіб повернути сам об’єкт даних замість відповіді. flushДійсно брудна.
AlikElzin-kilaka

1
Лише моя домашня тварина: 401 не означає, що користувач не має права. Це означає, що клієнт не має права, оскільки сервер не знає, хто ви. Якщо зареєстрованому / розпізнаному користувачеві користувачеві заборонено робити певні дії, правильний код відповіді - 403 Заборонено.
Demonblack

69

Відповідь hisdrewness буде спрацьовувати, але вона змінює весь підхід до того, щоб дозволити такому постачальнику, як Jackson + JAXB, автоматично перетворити ваш повернутий об'єкт у якийсь вихідний формат, наприклад, JSON. Натхненний публікацією Apache CXF (для якої використовується специфічний для CXF клас) я знайшов один із способів встановити код відповіді, який повинен працювати в будь-якій реалізації JAX-RS: ввести контекст HttpServletResponse і вручну встановити код відповіді. Наприклад, ось як встановити код CREATEDвідповіді, коли це доречно.

@Path("/foos/{fooId}")
@PUT
@Consumes("application/json")
@Produces("application/json")
public Foo setFoo(@PathParam("fooID") final String fooID, final Foo foo, @Context final HttpServletResponse response)
{
  //TODO store foo in persistent storage
  if(itemDidNotExistBefore) //return 201 only if new object; TODO app-specific logic
  {
    response.setStatus(Response.Status.CREATED.getStatusCode());
  }
  return foo;  //TODO get latest foo from storage if needed
}

Вдосконалення: Після пошуку іншої відповідної відповіді я дізнався, що можна вводити HttpServletResponseзмінну як члена, навіть для одиночного класу обслуговування (принаймні в RESTEasy) !! Це набагато кращий підхід, ніж забруднення API деталями впровадження. Це виглядатиме так:

@Context  //injected response proxy supporting multiple threads
private HttpServletResponse response;

@Path("/foos/{fooId}")
@PUT
@Consumes("application/json")
@Produces("application/json")
public Foo setFoo(@PathParam("fooID") final String fooID, final Foo foo)
{
  //TODO store foo in persistent storage
  if(itemDidNotExistBefore) //return 201 only if new object; TODO app-specific logic
  {
    response.setStatus(Response.Status.CREATED.getStatusCode());
  }
  return foo;  //TODO get latest foo from storage if needed
}

Ви можете фактично поєднувати підходи: анотувати метод з @Produces, а не вказувати тип медіа у фіналі Response.ok, і ви отримаєте ваш об'єкт повернення правильно JAXB-серіалізований у відповідний тип носія, щоб відповідати запиту. (Я щойно спробував це за допомогою одного методу, який міг би повернути або XML, або JSON: сам метод також не потрібно згадувати, за винятком @Producesанотації.)
Royston Shufflebotham,

Ви маєте рацію Гаррет. Мій приклад був більше ілюстрацією акценту на наданні змістового типу. Наші підходи схожі, але ідея використання MessageBodyWriterі Providerдозволяє неявного змісту переговорів, хоча, здається , що ваш приклад не вистачає якоїсь - то код. Ось ще одна відповідь , який я при умови , що ілюструє це: stackoverflow.com/questions/5161466 / ...
hisdrewness

8
Я не можу змінити код статусу в response.setStatus(). Єдиний спосіб надіслати, наприклад, відповідь 404 Not Found - встановити код статусу відповіді, response.setStatus(404)а потім закрити вихідний потік, response.getOutputStream().close()щоб JAX-RS не міг скинути мій статус.
Роб Юрлінк

2
Мені вдалося використати такий підхід для встановлення коду 201, але мені довелося додати блок пробного входу response.flushBuffer(), щоб уникнути зміни рамки коду відповіді. Не дуже чистий.
П’єр Генрі

1
@RobJuurlink, якщо ви хочете спеціально повернути а 404 Not Found, це може бути простіше просто використовувати throw new NotFoundException().
Гаррет Вілсон

34

Якщо ви хочете зберегти ваш ресурсний шар чистим від Responseоб'єктів, то я рекомендую вам використовувати @NameBindingта прив'язувати до реалізації ContainerResponseFilter.

Ось м'ясо анотації:

package my.webservice.annotations.status;

import javax.ws.rs.NameBinding;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@NameBinding
@Retention(RetentionPolicy.RUNTIME)
public @interface Status {
  int CREATED = 201;
  int value();
}

Ось м'ясо фільтра:

package my.webservice.interceptors.status;

import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.ext.Provider;
import java.io.IOException;

@Provider
public class StatusFilter implements ContainerResponseFilter {

  @Override
  public void filter(ContainerRequestContext containerRequestContext, ContainerResponseContext containerResponseContext) throws IOException {
    if (containerResponseContext.getStatus() == 200) {
      for (Annotation annotation : containerResponseContext.getEntityAnnotations()) {
        if(annotation instanceof Status){
          containerResponseContext.setStatus(((Status) annotation).value());
          break;
        }
      }
    }
  }
}

І тоді реалізація на вашому ресурсі просто стає:

package my.webservice.resources;

import my.webservice.annotations.status.StatusCreated;
import javax.ws.rs.*;

@Path("/my-resource-path")
public class MyResource{
  @POST
  @Status(Status.CREATED)
  public boolean create(){
    return true;
  }
}

Зберігає API чистим, приємна відповідь. Чи можна параметризувати анотацію, як-от @Status (код = 205), і перехоплювач замінить код на все, що ви вказали? Я думаю, що це в основному дасть вам примітку про переосмислення кодів, коли вам потрібно.
користувач2800708

@ user2800708, я вже робив це для свого місцевого коду, оновив відповідь, як ви запропонували.
Nthalk

Приємно, дякую. Завдяки цьому та кільком іншим хитрощам, я в основному тепер в змозі очистити API REST у своєму коді, щоб він відповідав простому інтерфейсу Java без згадки про REST; це просто ще один механізм RMI.
користувач2800708

6
Замість того, щоб циклувати анотації у StatusFilter, ви можете анотувати фільтр за допомогою @ Status на додаток до @ Provider. Тоді фільтр буде викликатись лише на ресурсах, котрі позначаються @ Status. Це мета @ NameBinding
треворизм

1
Гарний опис @trevorism. Там в один не дуже приємний побічний ефект аннотирования StatusFilterз @Status: вам необхідно або поставити за замовчуванням для анотування в valueполе, або оголосити один , коли анотування класу (наприклад: @Status(200)). Це, очевидно, не ідеально.
Філ

6

Якщо ви хочете змінити код статусу через виняток, за допомогою JAX-RS 2.0 ви можете реалізувати подібний ExceptionMapper. Це обробляє такий виняток для всього додатка.

@Provider
public class UnauthorizedExceptionMapper implements ExceptionMapper<EJBAccessException> {

    @Override
    public Response toResponse(EJBAccessException exception) {
        return Response.status(Response.Status.UNAUTHORIZED.getStatusCode()).build();
    }

}

6

Якщо ваш WS-RS потребує помилки, чому б не просто використовувати WebApplicationException?

@GET
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Path("{id}")
public MyEntity getFoo(@PathParam("id") long id,  @QueryParam("lang")long idLanguage) {

if (idLanguage== 0){
    // No URL parameter idLanguage was sent
    ResponseBuilder builder = Response.status(Response.Status.BAD_REQUEST);
    builder.entity("Missing idLanguage parameter on request");
    Response response = builder.build();
    throw new WebApplicationException(response);
    }
... //other stuff to return my entity
return myEntity;
}

4
На мою думку, WebApplicationExceptions не підходить для помилок клієнта, оскільки вони кидають великі сліди стека. Клієнтські помилки не повинні викидати сліди стека на стороні сервера та забруднювати журнали за допомогою нього.
Роб Юрлінк

5

JAX-RS підтримує стандартні / власні коди HTTP. Див., Наприклад, ResponseBuilder та ResponseStatus:

http://jackson.codehaus.org/javadoc/jax-rs/1.0/javax/ws/rs/core/Response.ResponseBuilder.html#status%28javax.ws.rs.core.Response.Status%29

Майте на увазі, що інформація JSON - це більше про дані, пов'язані з ресурсом / програмою. HTTP-коди детальніше про стан запиту на операцію CRUD. (принаймні, так це має бути в системах REST-ful)


посилання порушено
Умпа

5

Мені було дуже корисно створювати також повідомлення json з повторним кодом, як це:

@POST
@Consumes("application/json")
@Produces("application/json")
public Response authUser(JsonObject authData) {
    String email = authData.getString("email");
    String password = authData.getString("password");
    JSONObject json = new JSONObject();
    if (email.equalsIgnoreCase(user.getEmail()) && password.equalsIgnoreCase(user.getPassword())) {
        json.put("status", "success");
        json.put("code", Response.Status.OK.getStatusCode());
        json.put("message", "User " + authData.getString("email") + " authenticated.");
        return Response.ok(json.toString()).build();
    } else {
        json.put("status", "error");
        json.put("code", Response.Status.NOT_FOUND.getStatusCode());
        json.put("message", "User " + authData.getString("email") + " not found.");
        return Response.status(Response.Status.NOT_FOUND).entity(json.toString()).build();
    }
}

4

Подивіться приклад тут, він найкраще ілюструє проблему та її вирішення в останній (2.3.1) версії Джерсі.

https://jersey.java.net/documentation/latest/representations.html#d0e3586

Це в основному передбачає визначення спеціального винятку та збереження типу повернення як сутності. Коли виникає помилка, виняток викидається, інакше ви повертаєте POJO.


Я хотів би додати, що приклад інтересу - це той, де вони визначають власний клас виключень і будують Responseу ньому. Просто шукайте CustomNotFoundExceptionклас і, можливо, скопіюйте його у свій пост.
JBert

Я використовую такий підхід для помилок і мені це подобається. Але він не застосовується до кодів успіху (різних, ніж 200), таких як "201 створено".
П’єр Генрі

3

Я не використовую JAX-RS, але у мене є аналогічний сценарій, коли я використовую:

response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());

Це для мене використовується Spring MVC, але є простий спосіб це дізнатися!
Кап

1

Також зауважте, що за замовчуванням Джерсі замінить орган відповіді у випадку http-коду 400 або більше.

Щоб отримати вказану сутність як орган відповіді, спробуйте додати наступний init-param до вашої Джерсі у файл конфігурації web.xml:

    <init-param>
        <!-- used to overwrite default 4xx state pages -->
        <param-name>jersey.config.server.response.setStatusOverSendError</param-name>
        <param-value>true</param-value>
    </init-param>

0

Наступний код працював для мене. Впорскування messageContext через анотований сетер та встановлення коду статусу в моєму методі "додати".

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;

import org.apache.cxf.jaxrs.ext.MessageContext;

public class FlightReservationService {

    MessageContext messageContext;

    private final Map<Long, FlightReservation> flightReservations = new HashMap<>();

    @Context
    public void setMessageContext(MessageContext messageContext) {
        this.messageContext = messageContext;
    }

    @Override
    public Collection<FlightReservation> list() {
        return flightReservations.values();
    }

    @Path("/{id}")
    @Produces("application/json")
    @GET
    public FlightReservation get(Long id) {
        return flightReservations.get(id);
    }

    @Path("/")
    @Consumes("application/json")
    @Produces("application/json")
    @POST
    public void add(FlightReservation booking) {
        messageContext.getHttpServletResponse().setStatus(Response.Status.CREATED.getStatusCode());
        flightReservations.put(booking.getId(), booking);
    }

    @Path("/")
    @Consumes("application/json")
    @PUT
    public void update(FlightReservation booking) {
        flightReservations.remove(booking.getId());
        flightReservations.put(booking.getId(), booking);
    }

    @Path("/{id}")
    @DELETE
    public void remove(Long id) {
        flightReservations.remove(id);
    }
}

0

Розвиваючи відповідь на Nthalk з мікропрофілю OpenAPI можна вирівняти код повернення з документацією , використовуючи @APIResponse анотацію.

Це дозволяє позначити метод JAX-RS типу

@GET
@APIResponse(responseCode = "204")
public Resource getResource(ResourceRequest request) 

Ви можете проаналізувати цю стандартизовану примітку за допомогою ContainerResponseFilter

@Provider
public class StatusFilter implements ContainerResponseFilter {

    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
        if (responseContext.getStatus() == 200) {
            for (final var annotation : responseContext.getEntityAnnotations()) {
                if (annotation instanceof APIResponse response) {
                    final var rawCode = response.responseCode();
                    final var statusCode = Integer.parseInt(rawCode);

                    responseContext.setStatus(statusCode);
                }
            }
        }
    }

}

Застереження виникає, коли ви ставите кілька анотацій на ваш метод, як

@APIResponse(responseCode = "201", description = "first use case")
@APIResponse(responseCode = "204", description = "because you can")
public Resource getResource(ResourceRequest request) 

-1

Я використовую трикотаж 2.0 з читачами та авторами повідомлень. Я мав свій тип повернення методу як специфічну сутність, яку також використовували при впровадженні програми написання тіла повідомлень, і я повертав той же pojo, SkuListDTO. @GET @Consumes ({"application / xml", "application / json"}) @Produces ({"application / xml", "application / json"}) @Path ("/ skuResync")

public SkuResultListDTO getSkuData()
    ....
return SkuResultListDTO;

все, що я змінив, було це, я залишив реалізацію письменника в спокої, і вона все ще працювала.

public Response getSkuData()
...
return Response.status(Response.Status.FORBIDDEN).entity(dfCoreResultListDTO).build();
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.