Pregunta

Quiero que mi respuesta a incluir esto:

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

Eso es bastante fácil de lograr:

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

Pero, por supuesto, prefiero usar el ControllerLinkBuilder, como este:

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

El problema es que por el momento la variable "{keyMapId}" llega a la UriTemplate constructor, que ha sido incluido en una dirección URL codificada:

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

Así UriTemplate del constructor no la reconocen como portadores de una variable.

¿Cómo puedo convencer a ControllerLinkBuilder que quiero usar las variables de la plantilla?

¿Fue útil?

Solución

A partir de esta confirmación:

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

Ahora puede pasar null donde ruta de acceso de la variable que se espera.A mí me funciona, sin soluciones.

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

Y el resultado:

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

Compruebe también los problemas relacionados con: https://github.com/spring-projects/spring-hateoas/issues/545

Otros consejos

A mí me parece que el estado actual de la Primavera-HATEOAS no permite esto a través de la ControllerLinkBuilder (Me gustaría mucho que para ser probada mal), así que he aplicado esto a mí mismo con las siguientes clases para la creación de plantillas de los parámetros de consulta:

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, "}");
    }

}

y

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);
    }
}

Que incorpora la normal ControllerLinkBuilder y, a continuación, utiliza una lógica similar a analizar para @RequestParam anotado parámetros que son nulos y añadir estos a los parámetros de la consulta.También, nuestro cliente resuses estos plantilla URIs para realizar más peticiones al servidor.Para lograr esto, y no necesita preocuparse por el desmantelamiento de los no usados plantilla de los parámetros, tengo que realizar la operación inversa (intercambio de {params} con null), que estoy haciendo con una costumbre de la Primavera RequestParamMethodArgumentResolver de la siguiente manera

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;
    }

}

También este necesita para reemplazar el existente RequestParamMethodArgumentResolver que hago con:

@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);
    }

}

Desafortunadamente, a pesar de { y } son caracteres válidos en un con plantilla URI, no son válidas en un URI, que puede ser un problema para su código de cliente dependiendo de lo estricta que es.Me gustaría mucho que prefiere una solución más limpio construido en la Primavera-HATEOAS!

With latest versions of spring-hateoas you can do the following:

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

will give you: http://localhost:8080/bla{/keyMapId}",

We've run into the same problem. General workaround is we have our own LinkBuilder class with a bunch of static helpers. Templated ones look like this:

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)
    );
}

This is for exposing the parameters of a controller response of a PagedResource.

then in the controllers we call this an append a withRel as needed.

According to this issue comment, this will be addressed in an upcoming release of spring-hateoas.

For now, there's a drop-in replacement for ControllerLinkBuilder available from de.escalon.hypermedia:spring-hateoas-ext in Maven Central.

I can now do this:

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

...

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

I pass in null as the parameter value to indicate I want to use a template variable. The name of the variable is automatically pulled from the controller.

I needed to include a link with template variables in the root of a spring data rest application, to get access via traverson to an oauth2 token. This is working fine, maybe useful:

@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
    }
}

Based on the previous comments I have implemented a generic helper method (against spring-hateoas-0.20.0) as a "temporary" workaround. The implementation does consider only RequestParameters and is far from being optimized or well tested. It might come handy to some other poor soul traveling down the same rabbit hole though:

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);
}
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top