Jak zarządzać wyjątkami rzucanymi w filtrach na wiosnę?


109

Chcę użyć ogólnego sposobu zarządzania kodami błędów 5xx, powiedzmy konkretnie w przypadku, gdy db nie działa w całej mojej wiosennej aplikacji. Chcę mieć ładny plik JSON o błędzie zamiast śladu stosu.

Dla kontrolerów mam @ControllerAdviceklasę dla różnych wyjątków i to również wychwytuje przypadek, że db zatrzymuje się w środku żądania. Ale to nie wszystko. Zdarza mi się również mieć niestandardowe CorsFilterrozszerzenie OncePerRequestFilteri kiedy dzwonię, doFilterotrzymuję CannotGetJdbcConnectionExceptioni nie będzie zarządzany przez @ControllerAdvice. Przeczytałem kilka rzeczy w Internecie, które tylko wprawiły mnie w zakłopotanie.

Mam więc dużo pytań:

  • Czy muszę zaimplementować niestandardowy filtr? Znalazłem, ExceptionTranslationFilterale to tylko obsługuje AuthenticationExceptionlub AccessDeniedException.
  • Myślałem o wdrożeniu własnego HandlerExceptionResolver, ale to wzbudziło we mnie wątpliwości, nie mam żadnego niestandardowego wyjątku do zarządzania, musi być bardziej oczywisty sposób niż ten. Próbowałem też dodać try / catch i wywołać implementację HandlerExceptionResolver(powinno być wystarczająco dobre, mój wyjątek nie jest niczym specjalnym), ale to nie zwraca niczego w odpowiedzi, otrzymuję status 200 i puste ciało.

Czy jest jakiś dobry sposób, aby sobie z tym poradzić? Dzięki


Możemy zastąpić BasicErrorController Spring Boot. Napisałem o tym na blogu tutaj: naturalprogrammer.com/blog/1685463/…
— Sanjay

Odpowiedzi:


86

Oto co zrobiłem:

Czytałem podstawowe informacje na temat filtrów tutaj i zorientowali się, że trzeba utworzyć własny filtr, który będzie pierwszym w łańcuchu filtrów i będzie mieć zaczep Spróbuj złapać wszystkie wyjątki uruchomieniowe, które mogą tam wystąpić. Następnie muszę ręcznie utworzyć plik json i umieścić go w odpowiedzi.

Oto mój niestandardowy filtr:

public class ExceptionHandlerFilter extends OncePerRequestFilter {

    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);
        } catch (RuntimeException e) {

            // custom error response class used across my project
            ErrorResponse errorResponse = new ErrorResponse(e);

            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.getWriter().write(convertObjectToJson(errorResponse));
    }
}

    public String convertObjectToJson(Object object) throws JsonProcessingException {
        if (object == null) {
            return null;
        }
        ObjectMapper mapper = new ObjectMapper();
        return mapper.writeValueAsString(object);
    }
}

Następnie dodałem go w pliku web.xml przed rozszerzeniem CorsFilter. I to działa!

<filter> 
    <filter-name>exceptionHandlerFilter</filter-name> 
    <filter-class>xx.xxxxxx.xxxxx.api.controllers.filters.ExceptionHandlerFilter</filter-class> 
</filter> 


<filter-mapping> 
    <filter-name>exceptionHandlerFilter</filter-name> 
    <url-pattern>/*</url-pattern> 
</filter-mapping> 

<filter> 
    <filter-name>CorsFilter</filter-name> 
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> 
</filter> 

<filter-mapping>
    <filter-name>CorsFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Czy możesz opublikować swoją klasę ErrorResponse?
— Shiva kumar

1
Klasa @Shivakumar ErrorResponse jest prawdopodobnie prostym DTO z prostymi właściwościami kodu / wiadomości.
— ratijas

23

Chciałem przedstawić rozwiązanie oparte na odpowiedzi @kopelitsa . Główne różnice to:

  1. Ponowne użycie obsługi wyjątków kontrolera przy użyciu HandlerExceptionResolver.
  2. Korzystanie z konfiguracji Java zamiast konfiguracji XML

Najpierw musisz upewnić się, że masz klasę, która obsługuje wyjątki występujące w zwykłym RestController / Controller (klasa z adnotacją @RestControllerAdvicelub @ControllerAdvicei metody z adnotacjami @ExceptionHandler). To obsługuje wyjątki występujące w kontrolerze. Oto przykład użycia RestControllerAdvice:

@RestControllerAdvice
public class ExceptionTranslator {

    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorDTO processRuntimeException(RuntimeException e) {
        return createErrorDTO(HttpStatus.INTERNAL_SERVER_ERROR, "An internal server error occurred.", e);
    }

    private ErrorDTO createErrorDTO(HttpStatus status, String message, Exception e) {
        (...)
    }
}

Aby ponownie użyć tego zachowania w łańcuchu filtrów Spring Security, musisz zdefiniować filtr i podłączyć go do konfiguracji zabezpieczeń. Filtr musi przekierować wyjątek do wyżej zdefiniowanej obsługi wyjątków. Oto przykład:

@Component
public class FilterChainExceptionHandler extends OncePerRequestFilter {

    private final Logger log = LoggerFactory.getLogger(getClass());

    @Autowired
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver resolver;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        try {
            filterChain.doFilter(request, response);
        } catch (Exception e) {
            log.error("Spring Security Filter Chain Exception:", e);
            resolver.resolveException(request, response, null, e);
        }
    }
}

Utworzony filtr należy następnie dodać do SecurityConfiguration. Musisz podłączyć go do łańcucha bardzo wcześnie, ponieważ wszystkie poprzednie wyjątki filtru nie zostaną przechwycone. W moim przypadku rozsądne było dodanie go przed LogoutFilter. Zobacz domyślny łańcuch filtrów i jego kolejność w oficjalnych dokumentach . Oto przykład:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private FilterChainExceptionHandler filterChainExceptionHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(filterChainExceptionHandler, LogoutFilter.class)
            (...)
    }

}

19

Sam natknąłem się na ten problem i wykonałem poniższe kroki, aby ponownie użyć mojego, ExceptionControllerktóry jest oznaczony adnotacją @ControllerAdvisedo Exceptionswrzucenia do zarejestrowanego filtru.

Jest oczywiście wiele sposobów obsługi wyjątków, ale w moim przypadku chciałem, aby wyjątek był obsługiwany przez mój, ExceptionControllerponieważ jestem uparty, a także dlatego, że nie chcę kopiować / wklejać tego samego kodu (tj. Mam trochę przetwarzania / logowania kod w ExceptionController). Chciałbym zwrócić piękną JSONodpowiedź, tak jak pozostałe wyjątki wyrzucone nie z filtra.

{
  "status": 400,
  "message": "some exception thrown when executing the request"
}

W każdym razie udało mi się wykorzystać moje ExceptionHandleri musiałem zrobić trochę więcej, jak pokazano poniżej w krokach:

Kroki


  1. Masz niestandardowy filtr, który może zgłosić wyjątek lub nie
  2. Masz kontroler Spring, który obsługuje wyjątki przy użyciu @ControllerAdvisenp. MyExceptionController

Przykładowy kod

//sample Filter, to be added in web.xml
public MyFilterThatThrowException implements Filter {
   //Spring Controller annotated with @ControllerAdvise which has handlers
   //for exceptions
   private MyExceptionController myExceptionController; 

   @Override
   public void destroy() {
        // TODO Auto-generated method stub
   }

   @Override
   public void init(FilterConfig arg0) throws ServletException {
       //Manually get an instance of MyExceptionController
       ApplicationContext ctx = WebApplicationContextUtils
                  .getRequiredWebApplicationContext(arg0.getServletContext());

       //MyExceptionHanlder is now accessible because I loaded it manually
       this.myExceptionController = ctx.getBean(MyExceptionController.class); 
   }

   @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;

        try {
           //code that throws exception
        } catch(Exception ex) {
          //MyObject is whatever the output of the below method
          MyObject errorDTO = myExceptionController.handleMyException(req, ex); 

          //set the response object
          res.setStatus(errorDTO .getStatus());
          res.setContentType("application/json");

          //pass down the actual obj that exception handler normally send
          ObjectMapper mapper = new ObjectMapper();
          PrintWriter out = res.getWriter(); 
          out.print(mapper.writeValueAsString(errorDTO ));
          out.flush();

          return; 
        }

        //proceed normally otherwise
        chain.doFilter(request, response); 
     }
}

A teraz przykładowy kontroler sprężyny, który obsługuje Exceptionw normalnych przypadkach (tj. Wyjątki, które zwykle nie są zgłaszane na poziomie filtru, ten, którego chcemy użyć do wyjątków rzucanych w filtrze)

//sample SpringController 
@ControllerAdvice
public class ExceptionController extends ResponseEntityExceptionHandler {

    //sample handler
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ExceptionHandler(SQLException.class)
    public @ResponseBody MyObject handleSQLException(HttpServletRequest request,
            Exception ex){
        ErrorDTO response = new ErrorDTO (400, "some exception thrown when "
                + "executing the request."); 
        return response;
    }
    //other handlers
}

Udostępnianie rozwiązania tym, którzy chcą użyć ExceptionControllerdo Exceptionswrzucenia do filtra.


10
Cóż, możesz podzielić się swoim własnym rozwiązaniem, które brzmi tak, jak to zrobić :)
— Raf

1
Jeśli chcesz uniknąć podłączenia kontrolera do twojego filtra (co jest tym, do czego odnosi się @ Bato-BairTsyrenov, jak zakładam), możesz łatwo wyodrębnić logikę, w której tworzysz ErrorDTO do własnej @Componentklasy i użyj jej w filtrze i w Administratora.
— Rüdiger Schulz

1
Nie do końca się z tobą zgadzam, bo to nie jest zbyt czyste wtryskiwanie konkretnego kontrolera do twojego filtra.
— psv

Jak wspomniano w answertym, jest to jeden ze sposobów! Nie twierdziłem, że to najlepszy sposób. Dziękuję za podzielenie się troską @psv Jestem pewien, że społeczność doceni rozwiązanie, o którym myślisz :)
— Raf

12

Oto, co zrobiłem, opierając się na połączeniu powyższych odpowiedzi ... Mieliśmy już GlobalExceptionHandleradnotację @ControllerAdvicei chciałem również znaleźć sposób na ponowne użycie tego kodu do obsługi wyjątków pochodzących z filtrów.

Najprostszym rozwiązaniem, jakie udało mi się znaleźć, było pozostawienie obsługi wyjątków w spokoju i zaimplementowanie kontrolera błędów w następujący sposób:

@Controller
public class ErrorControllerImpl implements ErrorController {
  @RequestMapping("/error")
  public void handleError(HttpServletRequest request) throws Throwable {
    if (request.getAttribute("javax.servlet.error.exception") != null) {
      throw (Throwable) request.getAttribute("javax.servlet.error.exception");
    }
  }
}

Tak więc wszelkie błędy spowodowane przez wyjątki najpierw przechodzą przez program ErrorControlleri są przekierowywane do programu obsługi wyjątków przez ponowne zgłoszenie ich z @Controllerkontekstu, podczas gdy wszelkie inne błędy (nie spowodowane bezpośrednio przez wyjątek) przechodzą ErrorControllerbez modyfikacji.

Jakieś powody, dla których to właściwie zły pomysł?


1
Dzięki teraz testuję to rozwiązanie, ale w moim przypadku działa idealnie.
— Maciej

czysty i prosty dodatek do buta wiosennego 2.0+, który należy dodać @Override public String getErrorPath() { return null; }
— Fma

możesz użyć javax.servlet.RequestDispatcher.ERROR_EXCEPTION zamiast „javax.servlet.error.exception”
— Marx

9

Jeśli chcesz uzyskać ogólny sposób, możesz zdefiniować stronę błędu w web.xml:

<error-page>
  <exception-type>java.lang.Throwable</exception-type>
  <location>/500</location>
</error-page>

I dodaj mapowanie w Spring MVC:

@Controller
public class ErrorController {

    @RequestMapping(value="/500")
    public @ResponseBody String handleException(HttpServletRequest req) {
        // you can get the exception thrown
        Throwable t = (Throwable)req.getAttribute("javax.servlet.error.exception");

        // customize response to what you want
        return "Internal server error.";
    }
}

Ale w reszcie api przekierowanie z lokalizacją nie jest dobrym rozwiązaniem.
— jmattheis

@jmattheis Powyższe nie jest przekierowaniem.
— holmis83

To prawda, widziałem lokalizację i pomyślałem, że ma coś do zrobienia z lokalizacją http. W takim razie tego potrzebuję (:
— jmattheis

Czy możesz dodać odpowiednik konfiguracji Java do pliku web.xml, jeśli taki istnieje?
— k-den

1
@ k-den Myślę, że w obecnej specyfikacji nie ma odpowiednika konfiguracji Java, ale można mieszać konfigurację web.xml i Java.
— holmis83

5

To jest moje rozwiązanie przez nadpisanie domyślnej obsługi Spring Boot / Error

package com.mypackage;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

/**
 * This controller is vital in order to handle exceptions thrown in Filters.
 */
@RestController
@RequestMapping("/error")
public class ErrorController implements org.springframework.boot.autoconfigure.web.ErrorController {

    private final static Logger LOGGER = LoggerFactory.getLogger(ErrorController.class);

    private final ErrorAttributes errorAttributes;

    @Autowired
    public ErrorController(ErrorAttributes errorAttributes) {
        Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
        this.errorAttributes = errorAttributes;
    }

    @Override
    public String getErrorPath() {
        return "/error";
    }

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest aRequest, HttpServletResponse response) {
        RequestAttributes requestAttributes = new ServletRequestAttributes(aRequest);
        Map<String, Object> result =     this.errorAttributes.getErrorAttributes(requestAttributes, false);

        Throwable error = this.errorAttributes.getError(requestAttributes);

        ResponseStatus annotation =     AnnotationUtils.getAnnotation(error.getClass(), ResponseStatus.class);
        HttpStatus statusCode = annotation != null ? annotation.value() : HttpStatus.INTERNAL_SERVER_ERROR;

        result.put("status", statusCode.value());
        result.put("error", statusCode.getReasonPhrase());

        LOGGER.error(result.toString());
        return new ResponseEntity<>(result, statusCode) ;
    }

}

czy ma to wpływ na jakąkolwiek automatyczną konfigurację?
— Samet Baskıcı

Należy zauważyć, że HandlerExceptionResolver niekoniecznie obsługuje wyjątek. Może więc przejść jako HTTP 200. Użycie response.setStatus (..) przed wywołaniem wydaje się bezpieczniejsze.
— ThomasRS

5

Tylko w celu uzupełnienia innych dobrych odpowiedzi, ponieważ zbyt niedawno chciałem pojedynczy składnik obsługi błędów / wyjątków w prostej aplikacji SpringBoot zawierającej filtry, które mogą generować wyjątki, z innymi wyjątkami potencjalnie wyrzucanymi z metod kontrolera.

Na szczęście wydaje się, że nic nie stoi na przeszkodzie, aby połączyć porady dotyczące kontrolera z zastąpieniem domyślnej obsługi błędów Springa w celu zapewnienia spójnych ładunków odpowiedzi, umożliwienia udostępniania logiki, sprawdzania wyjątków z filtrów, pułapek wyjątków zgłaszanych przez usługi itp.

Na przykład


@ControllerAdvice
@RestController
public class GlobalErrorHandler implements ErrorController {

  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(ValidationException.class)
  public Error handleValidationException(
      final ValidationException validationException) {
    return new Error("400", "Incorrect params"); // whatever
  }

  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  @ExceptionHandler(Exception.class)
  public Error handleUnknownException(final Exception exception) {
    return new Error("500", "Unexpected error processing request");
  }

  @RequestMapping("/error")
  public ResponseEntity handleError(final HttpServletRequest request,
      final HttpServletResponse response) {

    Object exception = request.getAttribute("javax.servlet.error.exception");

    // TODO: Logic to inspect exception thrown from Filters...
    return ResponseEntity.badRequest().body(new Error(/* whatever */));
  }

  @Override
  public String getErrorPath() {
    return "/error";
  }

}

3

Jeśli chcesz przetestować stan aplikacji iw przypadku problemu zwrócić błąd HTTP, proponuję filtr. Poniższy filtr obsługuje wszystkie żądania HTTP. Najkrótsze rozwiązanie w Spring Boot z filtrem javax.

W realizacji mogą być różne warunki. W moim przypadku applicationManager sprawdza, czy aplikacja jest gotowa.

import ...ApplicationManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class SystemIsReadyFilter implements Filter {

    @Autowired
    private ApplicationManager applicationManager;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (!applicationManager.isApplicationReady()) {
            ((HttpServletResponse) response).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "The service is booting.");
        } else {
            chain.doFilter(request, response);
        }
    }

    @Override
    public void destroy() {}
}

2

Po zapoznaniu się z różnymi metodami sugerowanymi w powyższych odpowiedziach zdecydowałem się obsłużyć wyjątki uwierzytelniania za pomocą niestandardowego filtru. Byłem w stanie obsłużyć status odpowiedzi i kody za pomocą klasy odpowiedzi na błąd przy użyciu następującej metody.

Utworzyłem niestandardowy filtr i zmodyfikowałem konfigurację zabezpieczeń za pomocą metody addFilterAfter i dodałem po klasie CorsFilter.

@Component
public class AuthFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    //Cast the servlet request and response to HttpServletRequest and HttpServletResponse
    HttpServletResponse httpServletResponse = (HttpServletResponse) response;
    HttpServletRequest httpServletRequest = (HttpServletRequest) request;

    // Grab the exception from the request attribute
    Exception exception = (Exception) request.getAttribute("javax.servlet.error.exception");
    //Set response content type to application/json
    httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);

    //check if exception is not null and determine the instance of the exception to further manipulate the status codes and messages of your exception
    if(exception!=null && exception instanceof AuthorizationParameterNotFoundException){
        ErrorResponse errorResponse = new ErrorResponse(exception.getMessage(),"Authetication Failed!");
        httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        PrintWriter writer = httpServletResponse.getWriter();
        writer.write(convertObjectToJson(errorResponse));
        writer.flush();
        return;
    }
    // If exception instance cannot be determined, then throw a nice exception and desired response code.
    else if(exception!=null){
            ErrorResponse errorResponse = new ErrorResponse(exception.getMessage(),"Authetication Failed!");
            PrintWriter writer = httpServletResponse.getWriter();
            writer.write(convertObjectToJson(errorResponse));
            writer.flush();
            return;
        }
        else {
        // proceed with the initial request if no exception is thrown.
            chain.doFilter(httpServletRequest,httpServletResponse);
        }
    }

public String convertObjectToJson(Object object) throws JsonProcessingException {
    if (object == null) {
        return null;
    }
    ObjectMapper mapper = new ObjectMapper();
    return mapper.writeValueAsString(object);
}
}

SecurityConfig klasa

    @Configuration
    public class JwtSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    AuthFilter authenticationFilter;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterAfter(authenticationFilter, CorsFilter.class).csrf().disable()
                .cors(); //........
        return http;
     }
   }

Klasa ErrorResponse

public class ErrorResponse  {
private final String message;
private final String description;

public ErrorResponse(String description, String message) {
    this.message = message;
    this.description = description;
}

public String getMessage() {
    return message;
}

public String getDescription() {
    return description;
}}

0

Możesz użyć następującej metody wewnątrz bloku catch:

response.sendError(HttpStatus.UNAUTHORIZED.value(), "Invalid token")

Zwróć uwagę, że możesz użyć dowolnego kodu HttpStatus i niestandardowej wiadomości.


-1

To dziwne, ponieważ @ControllerAdvice powinno działać, czy wychwytujesz prawidłowy wyjątek?

@ControllerAdvice
public class GlobalDefaultExceptionHandler {

    @ResponseBody
    @ExceptionHandler(value = DataAccessException.class)
    public String defaultErrorHandler(HttpServletResponse response, DataAccessException e) throws Exception {
       response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
       //Json return
    }
}

Spróbuj też złapać ten wyjątek w CorsFilter i wysłać błąd 500, coś takiego

@ExceptionHandler(DataAccessException.class)
@ResponseBody
public String handleDataException(DataAccessException ex, HttpServletResponse response) {
    response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
    //Json return
}

Obsługa wyjątku w CorsFilter działa, ale nie jest zbyt czysta. Właściwie to, czego naprawdę potrzebuję, to obsługa wyjątku dla wszystkich filtrów
— kopelitsa

35
Rzut wyjątkowy z Filtermoże nie zostać złapany, @ControllerAdviceponieważ może nie sięgać DispatcherServlet.
— Thanh Nguyen Van

-1

Nie musisz w tym celu tworzyć niestandardowego filtru. Rozwiązaliśmy to, tworząc niestandardowe wyjątki, które rozszerzają ServletException (który jest wyrzucany z metody doFilter, pokazanej w deklaracji). Są one następnie przechwytywane i obsługiwane przez naszą globalną procedurę obsługi błędów.

edycja: gramatyka


Czy możesz udostępnić fragment kodu swojego globalnego modułu obsługi błędów?
— Neeraj Vernekar

to nie działa dla mnie. Zrobiłem niestandardowy wyjątek, który rozszerza ServletException, dodałem obsługę tego wyjątku w ExceptionHandler, ale nie został tam przechwycony.
— Marx
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.