Переменные шаблона с помощью 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);
}