Переменные шаблона с помощью ControllerLinkBuilder

Я хочу, чтобы мой ответ включал следующее:

"keyMaps":{
  "href":"http://localhost/api/keyMaps{/keyMapId}",
  "templated":true
 }

Этого достаточно легко достичь:

add(new Link("http://localhost/api/keyMaps{/keyMapId}", "keyMaps"));

Но, конечно, я бы предпочел использовать ControllerLinkBuilder, например:

add(linkTo(methodOn(KeyMapController.class).getKeyMap("{keyMapId}")).withRel("keyMaps"));

Проблема заключается в том, что к тому моменту, когда переменная "{keyMapId}" достигнет конструктора UriTemplate, она была включена в кодированный URL:

http://localhost/api/keyMaps/%7BkeyMapId%7D

Таким образом, конструктор UriTemplate не распознает его как содержащую переменную.

Как я могу убедить ControllerLinkBuilder, что я хочу использовать переменные шаблона?

Ответ 1

Начиная с этой фиксации:

https://github.com/spring-projects/spring-hateoas/commit/2daf8aabfb78b6767bf27ac3e473832c872302c7

Теперь вы можете передать null, где ожидается переменная пути. Это работает для меня, без обходных решений.

resource.add(linkTo(methodOn(UsersController.class).someMethod(null)).withRel("someMethod"));

И результат:

    "someMethod": {
        "href": "http://localhost:8080/api/v1/users/{userId}",
        "templated": true
    },

Также проверьте связанные проблемы: https://github.com/spring-projects/spring-hateoas/issues/545

Ответ 2

Мне кажется, что текущее состояние Spring -HATEOAS не позволяет это через ControllerLinkBuilder (я бы очень хотел, чтобы вас доказали неправильно), поэтому я сам реализовал это, используя следующие классы для шаблонов параметров запроса:

public class TemplatedLinkBuilder {

    private static final TemplatedLinkBuilderFactory FACTORY = new TemplatedLinkBuilderFactory();
    public static final String ENCODED_LEFT_BRACE = "%7B";
    public static final String ENCODED_RIGHT_BRACE = "%7D";

    private UriComponentsBuilder uriComponentsBuilder;

    TemplatedLinkBuilder(UriComponentsBuilder builder) {
        uriComponentsBuilder = builder;
    }

    public static TemplatedLinkBuilder linkTo(Object invocationValue) {
        return FACTORY.linkTo(invocationValue);
    }

    public static <T> T methodOn(Class<T> controller, Object... parameters) {
        return DummyInvocationUtils.methodOn(controller, parameters);
    }

    public Link withRel(String rel) {
        return new Link(replaceTemplateMarkers(uriComponentsBuilder.build().toString()), rel);
    }

    public Link withSelfRel() {
        return withRel(Link.REL_SELF);
    }

    private String replaceTemplateMarkers(String encodedUri) {
        return encodedUri.replaceAll(ENCODED_LEFT_BRACE, "{").replaceAll(ENCODED_RIGHT_BRACE, "}");
    }

}

и

public class TemplatedLinkBuilderFactory {

    private final ControllerLinkBuilderFactory controllerLinkBuilderFactory;

    public TemplatedLinkBuilderFactory() {
        this.controllerLinkBuilderFactory = new ControllerLinkBuilderFactory();
    }

    public TemplatedLinkBuilder linkTo(Object invocationValue) {
        ControllerLinkBuilder controllerLinkBuilder = controllerLinkBuilderFactory.linkTo(invocationValue);
        UriComponentsBuilder uriComponentsBuilder = controllerLinkBuilder.toUriComponentsBuilder();

        Assert.isInstanceOf(DummyInvocationUtils.LastInvocationAware.class, invocationValue);
        DummyInvocationUtils.LastInvocationAware invocations = (DummyInvocationUtils.LastInvocationAware) invocationValue;
        DummyInvocationUtils.MethodInvocation invocation = invocations.getLastInvocation();
        Object[] arguments = invocation.getArguments();
        MethodParameters parameters = new MethodParameters(invocation.getMethod());

        for (MethodParameter requestParameter : parameters.getParametersWith(RequestParam.class)) {
            Object value = arguments[requestParameter.getParameterIndex()];
            if (value == null) {
                uriComponentsBuilder.queryParam(requestParameter.getParameterName(), "{" + requestParameter.getParameterName() + "}");
            }
        }
        return new TemplatedLinkBuilder(uriComponentsBuilder);
    }
}

Что внедряет обычный ControllerLinkBuilder, а затем использует аналогичную логику для анализа для @RequestParam аннотированных параметров, которые являются нулевыми, и добавьте их к параметрам запроса. Кроме того, наш клиент повторно использует эти шаблонные URI для выполнения дальнейших запросов на сервер. Чтобы достичь этого и не нужно беспокоиться об изъятии неиспользуемых шаблонных параметров, я должен выполнить обратную операцию (свопинг {params} с помощью null), что я делаю, используя пользовательский Spring RequestParamMethodArgumentResolver, как следует

public class TemplatedRequestParamResolver extends RequestParamMethodArgumentResolver {

    public TemplatedRequestParamResolver() {
        super(false);
    }

    @Override
    protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest webRequest) throws Exception {
        Object value = super.resolveName(name, parameter, webRequest);
        if (value instanceof Object[]) {
            Object[] valueAsCollection = (Object[])value;
            List<Object> resultList = new LinkedList<Object>();
            for (Object collectionEntry : valueAsCollection) {
                if (nullifyTemplatedValue(collectionEntry) != null) {
                    resultList.add(collectionEntry);
                }
            }
            if (resultList.isEmpty()) {
                value = null;
            } else {
                value = resultList.toArray();
            }
        } else{
            value = nullifyTemplatedValue(value);
        }
        return value;
    }

    private Object nullifyTemplatedValue(Object value) {
        if (value != null && value.toString().startsWith("{") && value.toString().endsWith("}")) {
            value = null;
        }
        return value;
    }

}

Также необходимо заменить существующий RequestParamMethodArgumentResolver, который я делаю:

@Configuration
public class ConfigureTemplatedRequestParamResolver {

    private @Autowired RequestMappingHandlerAdapter adapter;

    @PostConstruct
    public void replaceArgumentMethodHandlers() {
        List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<HandlerMethodArgumentResolver>(adapter.getArgumentResolvers());
        for (int cursor = 0; cursor < argumentResolvers.size(); ++cursor) {
            HandlerMethodArgumentResolver handlerMethodArgumentResolver = argumentResolvers.get(cursor);
            if (handlerMethodArgumentResolver instanceof RequestParamMethodArgumentResolver) {
                argumentResolvers.remove(cursor);
                argumentResolvers.add(cursor, new TemplatedRequestParamResolver());
                break;
            }
        }
        adapter.setArgumentResolvers(argumentResolvers);
    }

}

К сожалению, хотя { и } являются допустимыми символами в templated URI, они недействительны в URI, что может быть проблемой для вашего кода клиента в зависимости от того, насколько это строго является. Я бы предпочел более аккуратное решение, встроенное в Spring -HATEOAS!

Ответ 3

В последних версиях spring-hateoas вы можете сделать следующее:

UriComponents uriComponents = UriComponentsBuilder.fromUri(linkBuilder.toUri()).build();
UriTemplate template = new UriTemplate(uriComponents.toUriString())
   .with("keyMapId", TemplateVariable.SEGMENT);

предоставит вам: http://localhost:8080/bla{/keyMapId}",

Ответ 4

Мы столкнулись с той же проблемой. Обходной путь - у нас есть свой собственный класс LinkBuilder с кучей статических помощников. Шаблонные выглядят так:

public static Link linkToSubcategoriesTemplated(String categoryId){

    return new Link(
        new UriTemplate(
            linkTo(methodOn(CategoryController.class).subcategories(null, null, categoryId))
                .toUriComponentsBuilder().build().toUriString(),
            // register it as variable
            getBaseTemplateVariables()
        ),
        REL_SUBCATEGORIES
    );
}

private static TemplateVariables getBaseTemplateVariables() {
    return new TemplateVariables(
        new TemplateVariable("page", TemplateVariable.VariableType.REQUEST_PARAM),
        new TemplateVariable("sort", TemplateVariable.VariableType.REQUEST_PARAM),
        new TemplateVariable("size", TemplateVariable.VariableType.REQUEST_PARAM)
    );
}

Это для предоставления параметров ответа контроллера PagedResource.

затем в контроллерах мы называем это добавлением withRel по мере необходимости.

Ответ 5

В соответствии с этот комментарий, это будет рассмотрено в предстоящем выпуске spring -hateoas.

На данный момент есть замена для ControllerLinkBuilder, доступная из de.escalon.hypermedia:spring-hateoas-ext в Maven Central.

Теперь я могу сделать это:

import static de.escalon.hypermedia.spring.AffordanceBuilder.*

...

add(linkTo(methodOn(KeyMapController.class).getKeyMap(null)).withRel("keyMaps"));

Я передаю null в качестве значения параметра, чтобы указать, что я хочу использовать переменную шаблона. Имя переменной автоматически вытаскивается из контроллера.

Ответ 6

Мне нужно было включить ссылку с переменными шаблона в корневую папку приложения для сохранения данных spring, чтобы получить доступ через traverson к маркеру oauth2. Это прекрасно работает, может быть, полезно:

@Component
class RepositoryLinksResourceProcessor implements ResourceProcessor<RepositoryLinksResource> {

    @Override
    RepositoryLinksResource process(RepositoryLinksResource resource) {

        UriTemplate uriTemplate =  new UriTemplate(
                ControllerLinkBuilder.
                        linkTo(
                                TokenEndpoint,
                                TokenEndpoint.getDeclaredMethod("postAccessToken", java.security.Principal, Map )).
                        toUriComponentsBuilder().
                        build().
                        toString(),
                new TemplateVariables([
                        new TemplateVariable("username", TemplateVariable.VariableType.REQUEST_PARAM),
                        new TemplateVariable("password", TemplateVariable.VariableType.REQUEST_PARAM),
                        new TemplateVariable("clientId", TemplateVariable.VariableType.REQUEST_PARAM),
                        new TemplateVariable("clientSecret", TemplateVariable.VariableType.REQUEST_PARAM)
                ])
        )

        resource.add(
                new Link( uriTemplate,
                        "token"
                )
        )

        return resource
    }
}

Ответ 7

На основе предыдущих комментариев я применил общий вспомогательный метод (против spring -hateoas-0.20.0) как временный метод обхода. Реализация рассматривает только RequestParameters и далеко не оптимизирована или хорошо протестирована. Это может пригодиться какой-то другой бедной душе, идущей по той же самой кроличьей норе:

public static Link getTemplatedLink(final Method m, final String rel) {
    DefaultParameterNameDiscoverer disco = new DefaultParameterNameDiscoverer();

    ControllerLinkBuilder builder = ControllerLinkBuilder.linkTo(m.getDeclaringClass(), m);
    UriTemplate uriTemplate = new UriTemplate(UriComponentsBuilder.fromUri(builder.toUri()).build().toUriString());
    Annotation[][] parameterAnnotations = m.getParameterAnnotations();

    int param = 0;
    for (Annotation[] parameterAnnotation : parameterAnnotations) {
        for (Annotation annotation : parameterAnnotation) {
            if (annotation.annotationType().equals(RequestParam.class)) {
                RequestParam rpa = (RequestParam) annotation;
                String parameterName = rpa.name();
                if (StringUtils.isEmpty(parameterName)) parameterName = disco.getParameterNames(m)[param];
                uriTemplate = uriTemplate.with(parameterName, TemplateVariable.VariableType.REQUEST_PARAM);
            }
        }
        param++;
    }
    return new Link(uriTemplate, rel);
}