Add a custom parameter to Solr while using Spring Data Solr - spring

Is it possible to add an additional parameter to a Solr query using Spring Data Solr that generates the following request?
"params": {
"indent": "true",
"q": "*.*",
"_": "1430295713114",
"wt": "java",
"AuthenticatedUserName": "user#domain.com"
}
I want to add a parameter needed by Apache Manifoldcf, AuthenticatedUserName and its value, alongside the other ones that are automatically populated by Spring Data Solr (q, wt).
Thank you,
V.

I managed to make it work by looking at the source code of the SolrTemplate class but I was wondering if there is a less intrusive solution.
public Page<Document> searchDocuments(DocumentSearchCriteria criteria, Pageable page) {
String[] words = criteria.getTitle().split(" ");
Criteria conditions = createSearchConditions(words);
SimpleQuery query = new SimpleQuery(conditions);
query.setPageRequest(page);
SolrQuery solrQuery = queryParsers.getForClass(query.getClass()).constructSolrQuery(query);
solrQuery.add(AUTHENTICATED_USER_NAME, criteria.getLoggedUsername());
try {
String queryString = this.queryParsers.getForClass(query.getClass()).getQueryString(query);
solrQuery.set(CommonParams.Q, queryString);
QueryResponse response = solrTemplate.getSolrServer().query(solrQuery);
List<Document> beans = convertQueryResponseToBeans(response, Document.class);
SolrDocumentList results = response.getResults();
return new SolrResultPage<>(beans, query.getPageRequest(), results.getNumFound(), results.getMaxScore());
} catch (SolrServerException e) {
log.error(e.getMessage(), e);
return new SolrResultPage<>(Collections.<Document>emptyList());
}
}
private <T> List<T> convertQueryResponseToBeans(QueryResponse response, Class<T> targetClass) {
return response != null ? convertSolrDocumentListToBeans(response.getResults(), targetClass) : Collections
.<T> emptyList();
}
public <T> List<T> convertSolrDocumentListToBeans(SolrDocumentList documents, Class<T> targetClass) {
if (documents == null) {
return Collections.emptyList();
}
return solrTemplate.getConverter().read(documents, targetClass);
}
private Criteria createSearchConditions(String[] words) {
return new Criteria("title").contains(words)
.or(new Criteria("description").contains(words))
.or(new Criteria("content").contains(words))
.or(new Criteria("resourcename").contains(words));
}

Related

Micrometer - WebMvcTagsContributor not adding custom tags

I'm trying to add custom tags - the path variables and their values from each request - to each metric micrometer generates. I'm using spring-boot with java 16.
From my research i've found that creating a bean of type WebMvcTagsContributor alows me to do just that.
This is the code
public class CustomWebMvcTagsContributor implements WebMvcTagsContributor {
private static int PRINT_ERROR_COUNTER = 0;
#Override
public Iterable<Tag> getTags(HttpServletRequest request, HttpServletResponse response,
Object handler,
Throwable exception) {
return Tags.of(getAllTags(request));
}
private static List<Tag> getAllTags(HttpServletRequest request) {
Object attributesMapObject = request.getAttribute(View.PATH_VARIABLES);
if (isNull(attributesMapObject)) {
attributesMapObject = request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
if (isNull(attributesMapObject)) {
attributesMapObject = extractPathVariablesFromURI(request);
}
}
if (nonNull(attributesMapObject)) {
return getPathVariablesTags(attributesMapObject);
}
return List.of();
}
private static Object extractPathVariablesFromURI(HttpServletRequest request) {
Long currentUserId = SecurityUtils.getCurrentUserId().orElse(null);
try {
URI uri = new URI(request.getRequestURI());
String path = uri.getPath(); //get the path
UriTemplate uriTemplate = new UriTemplate((String) request.getAttribute(
HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE)); //create template
return uriTemplate.match(path); //extract values form template
} catch (Exception e) {
log.warn("[Error on 3rd attempt]", e);
}
return null;
}
private static List<Tag> getPathVariablesTags(Object attributesMapObject) {
try {
Long currentUserId = SecurityUtils.getCurrentUserId().orElse(null);
if (nonNull(attributesMapObject)) {
var attributesMap = (Map<String, Object>) attributesMapObject;
List<Tag> tags = attributesMap.entrySet().stream()
.map(stringObjectEntry -> Tag.of(stringObjectEntry.getKey(),
String.valueOf(stringObjectEntry.getValue())))
.toList();
log.warn("[CustomTags] [{}]", CommonUtils.toJson(tags));
return tags;
}
} catch (Exception e) {
if (PRINT_ERROR_COUNTER < 5) {
log.error("[Error while getting attributes map object]", e);
PRINT_ERROR_COUNTER++;
}
}
return List.of();
}
#Override
public Iterable<Tag> getLongRequestTags(HttpServletRequest request, Object handler) {
return null;
}
}
#Bean
public WebMvcTagsContributor webMvcTagsContributor() {
return new CustomWebMvcTagsContributor();
}
In order to test this, i've created a small spring boot app, added an endpoint to it. It works just fine.
The problem is when I add this code to the production app.
The metrics generates are the default ones and i can't figure out why.
What can I check to see why the tags are not added?
local test project
http_server_requests_seconds_count {exception="None", method="GET",id="123",outcome="Success",status="200",test="test",uri="/test/{id}/compute/{test}",)1.0
in prod - different (& bigger) app
http_server_requests_seconds_count {exception="None", method="GET",outcome="Success",status="200",uri="/api/{something}/test",)1.0
What i've tried and didn't work
Created a bean that implemented WebMvcTagsProvider - this one had an odd behaviour - it wasn't creating metrics for endpoints that had path variables in the path - though in my local test project it worked as expected
I added that log there in order to see what the extra tags are but doesn't seem to reach there as i don't see anything in the logs - i know, you might say that the current user id stops it, but it's not that.

Spring not picking updated #Query value Using AOP

JPA: Method
#Repository
public interface FloorRepository extends JpaRepository<TnFloor, Integer> {
#Query("select distinct tnFloor from TnFloor tnFloor where tnFloor.tnBuilding.buildingId in ?1")
public List<TnFloor> findByBuildingIds(List<Integer> buildingIds);
}
#Before("dataRolesPointCuts()")
public void beforeMethods(JoinPoint joinPoint) {
log.debug(" Before Advice Called " + joinPoint.toShortString());
String classArray[]=joinPoint.getTarget().getClass().getGenericInterfaces()[0].getTypeName().split("\\.");
String className = classArray[classArray.length-1];
String methodName = joinPoint.getSignature().getName();
String securedMethodName = className + "_" + methodName;
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(authentication!=null)
{
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
String loggedINUserName = userDetails.getUsername();
Map<String, Map<String, String>> userRoleMap = usernameRoleMap.get(loggedINUserName);
TnMethodSecurityModel methodSecurity = methodSecurityMap.get(securedMethodName); // Replace with Map
if(methodSecurity!=null && methodSecurity.getTnMethodSecurityFilters()!=null && methodSecurity.getTnMethodSecurityFilters().size()>0)
{
Class<?> clazz =((Class<?>) joinPoint.getTarget().getClass().getGenericInterfaces()[0]);
try {
Method[] methods = clazz.getMethods();
Method method=null;
for(Method meth: methods )
{
if(meth.getName().equals(methodName))
{
method=meth;
break;
}
}
if(method == null)
return;
Query secParam = method.getAnnotation(Query.class);
String query=secParam.value();
String securityPredicate=Util.getSecuirtyPredicate(methodSecurity, userRoleMap);
try {
System.out.println("old MethodAnnotation = " + secParam.value());
Util. changeAnnotationValue(secParam, "value", query+" "+securityPredicate);
System.out.println("Asspect Query :: "+query);
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("modified MethodAnnotation = " + secParam.value());
} catch ( SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
log.info("Executing with argument: {}", className + " " + methodName);
}
#SuppressWarnings("unchecked")
public static Object changeAnnotationValue(Annotation annotation, String key, Object newValue){
Object handler = Proxy.getInvocationHandler(annotation);
Field f;
try {
f = handler.getClass().getDeclaredField("memberValues");
} catch (NoSuchFieldException | SecurityException e) {
throw new IllegalStateException(e);
}
f.setAccessible(true);
Map<String, Object> memberValues;
try {
memberValues = (Map<String, Object>) f.get(handler);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new IllegalStateException(e);
}
Object oldValue = memberValues.get(key);
if (oldValue == null || oldValue.getClass() != newValue.getClass()) {
throw new IllegalArgumentException();
}
memberValues.put(key,newValue);
return oldValue;
}
Output After Before Advice execution :
old MethodAnnotation = select distinct tnFloor from TnFloor tnFloor where tnFloor.tnBuilding.buildingId in ?1
modified MethodAnnotation = select distinct tnFloor from TnFloor tnFloor where tnFloor.tnBuilding.buildingId in ?1 (tnFloor.tnBuilding.buildingId IN (0,1,6))
But Final JPA is executing query before modification OUTPUT :.
select tnbuilding0_.building_id as building1_17_0_, tnbuilding0_.description as descript2_17_0_, tnbuilding0_.name as name3_17_0_, tnbuilding0_.site_id as site_id4_17_0_, tnsite1_.site_id as site_id1_65_1_, tnsite1_.description as descript2_65_1_, tnsite1_.email as email3_65_1_, tnsite1_.name as name4_65_1_, tnsite1_.url as url5_65_1_ from tn_building tnbuilding0_ inner join tn_site tnsite1_ on tnbuilding0_.site_id=tnsite1_.site_id where tnbuilding0_.building_id=?
Sorry for lecturing you, but... Annotation values are constants. You cannot change them. Your hacky way of trying to change their in-memory representations is maybe a nifty exercise, but bad design. If your application or aspect really rely on something like this, as a developer you should feel an instant urge to refactor instead.
As for why it is not working as you dreamed it up: You might expect that Spring reads annotations every time before executing an annotated method. But usually such frameworks scan annotations while wiring an application.
Simply do not use hard-coded constants (like in annotations) for things you might want to make more dynamic, be it by using AOP or by other means. If you do want to keep the query annotation but use AOP, I think you should try to hook into another component where the query is being parsed or sent to the database and modify it there.

Performing faceted search with elastic search repositories using spring data?

I am in the need of performing faceted search using elastic search repositories developed using spring data.
One of the repositories which I have created are
public interface EmployeeSearchRepository extends ElasticsearchRepository<Employee, Long> {
}
it does provide a method called search with a signature:
FacetedPage<Employee> search(QueryBuilder query, Pageable pageable);
but the getFacets method of the FacetedPage returns null. How can I query to generate the facets?
I have the same problem and it seems that it is not implemented (yet).
If you look at DefaultResultMapper.mapResults() it calls response.getFacets() which is always null.
Note that facets are deprecated in elasticsearch and you should use aggregations instead. So maybe the contributors of the project are refactoring it?
I worked this around by writing my own results mapper class which extends the DefaultResultMapper but also converts the aggregations to FacetResults.
SomethingResultsMapper:
#Override
public <T> FacetedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
FacetedPage<T> facetedPage = super.mapResults(response, clazz, pageable);
//Process Aggregations.
if (response.getAggregations() != null) {
for (Aggregation aggregations : response.getAggregations().asList()) {
final Filter filterAggregations = (Filter) aggregations;
for (Aggregation filterAgg : filterAggregations.getAggregations().asList()) {
if (filterAgg instanceof Terms) {
final Terms aggTerm = (Terms) filterAgg;
if (!aggTerm.getBuckets().isEmpty()) {
facetedPage.getFacets().add(processTermAggregation(aggTerm));
}
} else if (filterAgg instanceof Nested) {
final Nested nestedAgg = (Nested) filterAgg;
for (Aggregation aggregation : nestedAgg.getAggregations().asList()) {
final Terms aggTerm = (Terms) aggregation;
if (!aggTerm.getBuckets().isEmpty()) {
facetedPage.getFacets().add(processTermAggregation(aggTerm));
}
}
} else {
throw new IllegalArgumentException("Aggregation type not (yet) supported: " + filterAgg.getClass().getName());
}
}
}
}
return facetedPage;
}
private FacetResult processTermAggregation(final Terms aggTerm) {
long total = 0;
List<Term> terms = new ArrayList<>();
List<Terms.Bucket> buckets = aggTerm.getBuckets();
for (Terms.Bucket bucket : buckets) {
terms.add(new Term(bucket.getKey(), (int) bucket.getDocCount()));
total += bucket.getDocCount();
}
return new FacetTermResult(aggTerm.getName(), FacetConfig.fromAggregationTerm(aggTerm.getName()).getLabel(),
terms, total, aggTerm.getSumOfOtherDocCounts(), aggTerm.getDocCountError());
}
Then i created a custom Spring data repository (see the docs) and defined a custom method where i provide my SomethingResultsMapper:
#Override
public FacetedPage<Something> searchSomething(final SearchQuery searchQuery) {
return elasticsearchTemplate.queryForPage(searchQuery, Something.class, new SomethingResultsMapper());
}
EDIT: I think this one is being fixed by https://jira.spring.io/browse/DATAES-211

How to externalize the queries to xml files using spring

I am using spring and their JDBC template to do read/write operations to the database. I am facing a problem in my reporting module that i have to frequently change the query sqls to cater to frequent changes.
Though using spring jdbc ORM, is there a way to externalize my query parameters such that i just change it in the XML & restart and there is no need to rebuild my source again for deployment. Any approach ORM (preferred) or simple Sql will do.
As of now i have to change the query again and again ,rebuild the source and deploy.
I am not sure if Spring provides some out of the box solutions to implement what you want. But here is one way to get it done, which i had implemented ones. So i will try to reduce some hardwork for you.
You might need to implement a utility to load from resources xml file. Something like this.
public final class LoadFromResourceFileUtils {
public static String loadQuery(final String libraryPath,
final String queryName) {
final InputStream is = StreamUtils
.streamFromClasspathResource(libraryPath);
if (is == null) {
throw new RuntimeException(String.format(
"The SQL Libary %s could not be found.", libraryPath));
}
final Document doc = XMLParseUtils.parse(is);
final Element qryElem = (Element) doc.selectSingleNode(String.format(
"SQLQueries/SQLQuery[#name='%s']", queryName));
final String ret = qryElem == null ? null : qryElem.getText();
return ret;
}
}
You would need to store your queries in an XML say queries.xml and keep it in your classpath, for e.g
<?xml version="1.0" encoding="UTF-8"?>
<SQLQueries>
<SQLQuery name="myQuery">
<![CDATA[
your query
]]>
</SQLQuery>
</SQLQueries>
And in your DAO you can do this to get the query
String query = LoadFromResourceFileUtils.loadQuery(
"queries.xml", "myQuery");
XMLParseUtils and StreamUtils for your reference
public final class XMLParseUtils {
public static Document parse(final InputStream inStream) {
Document ret = null;
try {
if (inStream == null) {
throw new RuntimeException(
"XML Input Stream for parsing is null");
}
final SAXReader saxReader = new SAXReader();
ret = saxReader.read(inStream);
} catch (final DocumentException exc) {
throw new RuntimeException("XML Parsing error", exc);
}
return ret;
}
}
public final class StreamUtils {
public static InputStream streamFromClasspathResource(
final String resourceClassPath) {
final Class<StreamUtils> clazz = StreamUtils.class;
final ClassLoader clLoader = clazz.getClassLoader();
final InputStream inStream = clLoader
.getResourceAsStream(resourceClassPath);
if (inStream == null) {
if(LOGGER.isDebugEnabled()){
LOGGER.debug(String.format("Resource %s NOT FOUND.",
resourceClassPath));
}
}
return inStream;
}
}

Template variables with ControllerLinkBuilder

I want my response to include this:
"keyMaps":{
"href":"http://localhost/api/keyMaps{/keyMapId}",
"templated":true
}
That's easy enough to achieve:
add(new Link("http://localhost/api/keyMaps{/keyMapId}", "keyMaps"));
But, of course, I'd rather use the ControllerLinkBuilder, like this:
add(linkTo(methodOn(KeyMapController.class).getKeyMap("{keyMapId}")).withRel("keyMaps"));
The problem is that by the time the variable "{keyMapId}" reaches the UriTemplate constructor, it's been included in an encoded URL:
http://localhost/api/keyMaps/%7BkeyMapId%7D
So UriTemplate's constructor doesn't recognise it as containing a variable.
How can I persuade ControllerLinkBuilder that I want to use template variables?
It looks to me like the current state of Spring-HATEOAS doesn't allow this via the ControllerLinkBuilder (I'd very much like to be proven wrong), so I have implemented this myself using the following classes for templating query parameters:
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, "}");
}
}
and
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);
}
}
Which embeds the normal ControllerLinkBuilder and then uses similar logic to parse for #RequestParam annotated parameters that are null and add these on to the query parameters. Also, our client resuses these templated URIs to perform further requests to the server. To achieve this and not need to worry about stripping out the unused templated params, I have to perform the reverse operation (swapping {params} with null), which I'm doing using a custom Spring RequestParamMethodArgumentResolver as follows
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;
}
}
Also this needs to replace the existing RequestParamMethodArgumentResolver which I do with:
#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);
}
}
Unfortunately, although { and } are valid characters in a templated URI, they are not valid in a URI, which may be a problem for your client code depending on how strict it is. I'd much prefer a neater solution built into Spring-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}",
Starting with this commit:
https://github.com/spring-projects/spring-hateoas/commit/2daf8aabfb78b6767bf27ac3e473832c872302c7
You can now pass null where path variable is expected. It works for me, without workarounds.
resource.add(linkTo(methodOn(UsersController.class).someMethod(null)).withRel("someMethod"));
And the result:
"someMethod": {
"href": "http://localhost:8080/api/v1/users/{userId}",
"templated": true
},
Also check related issues: https://github.com/spring-projects/spring-hateoas/issues/545
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);
}

Resources