LinkHeaderUtil.java

package se.jobtechdev.personaldatagateway.api.util;

import org.springframework.data.domain.Page;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import org.springframework.http.HttpHeaders;
import org.springframework.web.util.UriComponentsBuilder;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;

public class LinkHeaderUtil {
  private LinkHeaderUtil() {}

  public static String format(String rel, String value) {
    return String.format("<%s>; rel=\"%s\"", value, rel);
  }

  public static String getCurrentMethodName() {
    final var stackWalker = StackWalker.getInstance();
    return stackWalker.walk(s -> s.skip(3).findFirst()).get().getMethodName();
  }

  public static void handle() throws ClassNotFoundException {
    /* NOP */
  }

  public static Class<?> getCurrentClass() {
    try {
      handle();
      final var className =
          StackWalker.getInstance().walk(s -> s.skip(3).findFirst()).get().getClassName();
      return Class.forName(className);
    } catch (ClassNotFoundException e) {
      throw new RuntimeException(e);
    }
  }

  public static Link getSelfLink(Class<?>[] paramTypes, Object... params) {
    try {
      final var controller = getCurrentClass();
      final var proxy = WebMvcLinkBuilder.methodOn(controller);
      final var proxyClass = proxy.getClass();
      final var method = getCurrentMethodName();
      final Method proxyMethod = proxyClass.getMethod(method, paramTypes);
      final var proxyMethodResult = proxyMethod.invoke(proxy, params);
      final var builder = WebMvcLinkBuilder.linkTo(proxyMethodResult);
      return builder.withSelfRel();
    } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
      e.printStackTrace();
      throw new RuntimeException(e);
    }
  }

  public static String getLocationLink(Link link, String id) {
    return String.format("%s/%s", link.getHref(), id);
  }

  public static HttpHeaders createHeadersWithLocation(
      String createdResourceId, Class<?>[] paramTypes, Object... params) {
    final var headers = new HttpHeaders();

    final var link = LinkHeaderUtil.getSelfLink(paramTypes, params);
    headers.add(HttpHeaders.LINK, link.toString());

    headers.add(HttpHeaders.LOCATION, LinkHeaderUtil.getLocationLink(link, createdResourceId));

    headers.add(
        HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS,
        String.join(",", HttpHeaders.LINK, HttpHeaders.LOCATION));

    return headers;
  }

  public static HttpHeaders createHeaders(Class<?>[] paramTypes, Object... params) {
    final var headers = new HttpHeaders();

    headers.add(HttpHeaders.LINK, LinkHeaderUtil.getSelfLink(paramTypes, params).toString());

    headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.LINK);

    return headers;
  }

  public static HttpHeaders createHeaders(
      List<Link> links, Class<?>[] paramTypes, Object... params) {
    final var headers = new HttpHeaders();

    headers.add(
        HttpHeaders.LINK,
        Links.of(LinkHeaderUtil.getSelfLink(paramTypes, params)).and(links).toString());

    headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.LINK);

    return headers;
  }

  public static <T> HttpHeaders createHeadersWithPagination(
      Page<T> page, Class<?>[] paramTypes, Object... params) {
    final var headers = new HttpHeaders();

    headers.add("X-Pagination-Total", Long.toString(page.getTotalElements()));
    headers.add("X-Pagination-Offset", Integer.toString(page.getNumber() * page.getSize()));
    headers.add("X-Pagination-Limit", Integer.toString(page.getSize()));
    headers.add("X-Pagination-Count", Long.toString(page.getContent().size()));

    final var selfLink = LinkHeaderUtil.getSelfLink(paramTypes, params);

    headers.add(
        HttpHeaders.LINK,
        Links.of(selfLink)
            .andIf(
                page.getTotalPages() > 0,
                Link.of(
                    UriComponentsBuilder.fromUriString(selfLink.getHref())
                        .replaceQueryParam("offset", 0)
                        .toUriString(),
                    "first"))
            .andIf(
                page.getTotalPages() > 0 && !page.isFirst(),
                Link.of(
                    UriComponentsBuilder.fromUriString(selfLink.getHref())
                        .replaceQueryParam(
                            "offset",
                            page.previousOrFirstPageable().getPageNumber() * page.getSize())
                        .toUriString(),
                    "prev"))
            .andIf(
                page.getTotalPages() > 0 && !page.isLast(),
                Link.of(
                    UriComponentsBuilder.fromUriString(selfLink.getHref())
                        .replaceQueryParam(
                            "offset", page.nextOrLastPageable().getPageNumber() * page.getSize())
                        .toUriString(),
                    "next"))
            .andIf(
                page.getTotalPages() > 0,
                Link.of(
                    UriComponentsBuilder.fromUriString(selfLink.getHref())
                        .replaceQueryParam(
                            "offset",
                            getLastOffset(
                                page.getTotalPages(), page.getTotalElements(), page.getSize()))
                        .toUriString(),
                    "last"))
            .toString());

    headers.add(
        HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS,
        String.join(
            ",",
            List.of(
                "X-Pagination-Total",
                "X-Pagination-Offset",
                "X-Pagination-Limit",
                "X-Pagination-Count",
                HttpHeaders.LINK)));

    return headers;
  }

  static long getLastOffset(int totalPages, long totalElements, int pageSize) {
    return totalPages > 0 ? (totalElements % totalPages * pageSize) : 0;
  }
}