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);
}
Related
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.
This is my POST method and it is successful and run well. My question is how to do the PUT request method so that it can update the data well?
Post method
public void addRecipe(RecipeDTO recipedto)
{
Category categoryTitle = categoryRepository.findByCategoryTitle(recipedto.getCategoryTitle());
Recipe recipe = new Recipe();
/*I map my dto data original model*/
recipe.setRID(recipedto.getrID());
recipe.setRecipeTitle(recipedto.getRecipeTitle());
recipe.setDescription(recipedto.getDescription());
recipe.setCookTime(recipedto.getCookTime());
List categoryList = new ArrayList<>();
categoryList.add(categoryTitle);
recipe.setCategories(categoryList);
Recipe savedRecipe = recipeRepository.save(recipe);
/*I map the data in ingredientDTO and setpDTO to actual model */
List ingredientList = new ArrayList<>();
for(IngredientDTO ingredientdto : recipedto.getIngredients())
{
Ingredient ingredient = new Ingredient();
ingredient.setIID(ingredientdto.getiID());
ingredient.setIngredientName(ingredientdto.getIngredientName());
ingredient.setRecipe(savedRecipe);
ingredientList.add(ingredient);
}
List stepList = new ArrayList<>();
for(StepDTO stepdto : recipedto.getSteps())
{
Step step = new Step();
step.setSID(stepdto.getsID());
step.setStepDescription(stepdto.getStepDescription());
step.setStepNumber(stepdto.getStepNumber());
step.setRecipe(savedRecipe);
stepList.add(step);
}
ingredientRepository.save(ingredientList);
stepRepository.save(stepList);
}
This is my put method and it wont work, how should I do it, because I have no idea. Please teach me to do this method, if it is better.
public void updateRecipe(RecipeDTO recipedto, String id)
{
Recipe recipe = recipeRepository.findByrID(recipedto.getrID());
if(id==recipedto.getrID().toString())
{
recipeRepository.save(recipe);
}
}
When building REST services in Java you usually use an Framework to help you with this.
Like "jax-rs": https://mvnrepository.com/artifact/javax.ws.rs/javax.ws.rs-api/2.0
If using jax-rs then you mark your method as an Http PUT method with the annotation #PUT, ex:
#PUT
#Path("ex/foo")
public Response somePutMethod() {
return Response.ok().entity("Put some Foos!").build();
}
If using Spring as an Framework you mark your PUT method with the #RequestMapping annotation, ex:
#RequestMapping(value = "/ex/foo", method = PUT)
public String putFoos() {
return "Put some Foos";
}
Firstly and very importantly, you DO NOT use String == String for checking equality. Your code:
public void updateRecipe(RecipeDTO recipedto, String id)
{
Recipe recipe = recipeRepository.findByrID(recipedto.getrID());
if(id==recipedto.getrID().toString())
{
recipeRepository.save(recipe);
}
}
It should be:
public void updateRecipe(RecipeDTO recipedto, String id)
{
Recipe recipe = recipeRepository.findByrID(recipedto.getrID());
if(recipedto.getrID().toString().equals(id))
{
recipeRepository.save(recipe);
}
}
Why?
Because equality with == checks if objects have the same memory address. In other words:
new Integer(1) == new Integer(1) //false
1 == 1 //true
new String("hello") == new String("hello") //false
"hello" == "hello" //true because literal strings are stored in a String pool
new String("hello") == "hello" //false
Secondly, you SHOULD ALWAYS use generics with Collection APIs.
Your code:
List categoryList = new ArrayList<>();
Should be:
List<Category> categoryList = new ArrayList<>();
And lastly, like askepan said, you have not defined what framework you are using. In case of Jersey (JAX-RS implementation) you have HTTP Request Methods:
#GET, #POST, #PUT, #DELETE, #HEAD, #OPTIONS.
#PUT
#Produces("text/plain")
#Consumes("text/plain")
public Response putContainer() {
System.out.println("PUT CONTAINER " + container);
URI uri = uriInfo.getAbsolutePath();
Container c = new Container(container, uri.toString());
Response r;
if (!MemoryStore.MS.hasContainer(c)) {
r = Response.created(uri).build();
} else {
r = Response.noContent().build();
}
MemoryStore.MS.createContainer(c);
return r;
}
If you use Spring, there are #RequestMapping(method = ), or short versions:
#GetMapping, #PutMapping, #PostMapping, #DeleteMapping.
#GetMapping("/{id}")
public Person getPerson(#PathVariable Long id) {
// ...
}
#PutMapping
public void add(#RequestBody Person person) {
// ...
}
According to the annotation, the method will be called accordingly.
More information in:
Spring,JAX-RS
In my Sling app I have data presenting documents, with pages, and content nodes. We mostly server those documents as HTML, but now I would like to have a servlet to serve these documents as PDF and PPT.
Basically, I thought about implementing the factory pattern : in my servlet, dependending on the extension of the request (pdf or ppt), I would get from a DocumentBuilderFactory, the proper DocumentBuilder implementation, either PdfDocumentBuilder or PptDocumentBuilder.
So first I had this:
public class PlanExportBuilderFactory {
public PlanExportBuilder getBuilder(String type) {
PlanExportBuilder builder = null;
switch (type) {
case "pdf":
builder = new PdfPlanExportBuilder();
break;
default:
logger.error("Unsupported plan export builder, type: " + type);
}
return builder;
}
}
In the servlet:
#Component(metatype = false)
#Service(Servlet.class)
#Properties({
#Property(name = "sling.servlet.resourceTypes", value = "myApp/document"),
#Property(name = "sling.servlet.extensions", value = { "ppt", "pdf" }),
#Property(name = "sling.servlet.methods", value = "GET")
})
public class PlanExportServlet extends SlingSafeMethodsServlet {
#Reference
PlanExportBuilderFactory builderFactory;
#Override
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException {
Resource resource = request.getResource();
PlanExportBuilder builder = builderFactory.getBuilder(request.getRequestPathInfo().getExtension());
}
}
But the problem is that in the builder I would like to reference other services to access Sling resources, and with this solution, they're not bound.
I looked at Services Factory with OSGi but from what I've understood, you use them to configure differently the same implementation of a service.
Then I found that you can get a specific implementation by naming it, or use a property and a filter.
So I've ended up with this:
public class PlanExportBuilderFactory {
#Reference(target = "(builderType=pdf)")
PlanExportBuilder pdfPlanExportBuilder;
public PlanExportBuilder getBuilder(String type) {
PlanExportBuilder builder = null;
switch (type) {
case "pdf":
return pdfPlanExportBuilder;
default:
logger.error("Unsupported plan export builder, type: " + type);
}
return builder;
}
}
The builder defining a "builderType" property :
// AbstractPlanExportBuilder implements PlanExportBuilder interface
#Component
#Service(value=PlanExportBuilder.class)
public class PdfPlanExportBuilder extends AbstractPlanExportBuilder {
#Property(name="builderType", value="pdf")
public PdfPlanExportBuilder() {
planDocument = new PdfPlanDocument();
}
}
I would like to know if it's a good way to retrieve my PDF builder implementation regarding OSGi good practices.
EDIT 1
From Peter's answer I've tried to add multiple references but with Felix it doesn't seem to work:
#Reference(name = "planExportBuilder", cardinality = ReferenceCardinality.MANDATORY_MULTIPLE, policy = ReferencePolicy.DYNAMIC)
private Map<String, PlanExportBuilder> builders = new ConcurrentHashMap<String, PlanExportBuilder>();
protected final void bindPlanExportBuilder(PlanExportBuilder b, Map<String, Object> props) {
final String type = PropertiesUtil.toString(props.get("type"), null);
if (type != null) {
this.builders.put((String) props.get("type"), b);
}
}
protected final void unbindPlanExportBuilder(final PlanExportBuilder b, Map<String, Object> props) {
final String type = PropertiesUtil.toString(props.get("type"), null);
if (type != null) {
this.builders.remove(type);
}
}
I get these errors :
#Reference(builders) : Missing method bind for reference planExportBuilder
#Reference(builders) : Something went wrong: false - true - MANDATORY_MULTIPLE
#Reference(builders) : Missing method unbind for reference planExportBuilder
The Felix documentation here http://felix.apache.org/documentation/subprojects/apache-felix-maven-scr-plugin/scr-annotations.html#reference says for the bind method:
The default value is the name created by appending the reference name to the string bind. The method must be declared public or protected and take single argument which is declared with the service interface type
So according to this, I understand it cannot work with Felix, as I'm trying to pass two arguments. However, I found an example here that seems to match what you've suggested but I cannot make it work: https://github.com/Adobe-Consulting-Services/acs-aem-samples/blob/master/bundle/src/main/java/com/adobe/acs/samples/services/impl/SampleMultiReferenceServiceImpl.java
EDIT 2
Just had to move the reference above the class to make it work:
#References({
#Reference(
name = "planExportBuilder",
referenceInterface = PlanExportBuilder.class,
policy = ReferencePolicy.DYNAMIC,
cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE)
})
public class PlanExportServlet extends SlingSafeMethodsServlet {
Factories are evil :-) Main reason is of course the yucky class loading hacks that are usually used but also because they tend to have global knowledge. In general, you want to be able to add a bundle with a new DocumentBuilder and then that type should become available.
A more OSGi oriented solution is therefore to use service properties. This could look like:
#Component( property=HTTP_WHITEBOARD_FILTER_REGEX+"=/as")
public class DocumentServlet {
final Map<String,DocBuilder> builders = new ConcurrentHashMap<>();
public void doGet( HttpServletRequest rq, HttpServletResponse rsp )
throws IOException, ServletException {
InputStream in = getInputStream( rq.getPathInfo() );
if ( in == null )
....
String type = toType( rq.getPathInfo(), rq.getParameter("type") );
DocBuilder docbuilder = builders.get( type );
if ( docbuilder == null)
....
docbuilder.convert( type, in, rsp.getOutputStream() );
}
#Reference( cardinality=MULTIPLE, policy=DYNAMIC )
void addDocBuilder( DocBuilder db, Map<String,Object> props ) {
docbuilders.put(props.get("type"), db );
}
void removeDocBuilder(Map<String,Object> props ) {
docbuilders.remove(props.get("type"));
}
}
A DocBuilder could look like:
#Component( property = "type=ppt-pdf" )
public class PowerPointToPdf implements DocBuilder {
...
}
I am running a JUnit test via maven where a struts action java method is being tested that makes the following call:
// Gets this from the "org.apache.struts2.util.TokenHelper" class in the struts2-core jar
String token = TokenHelper.getTokenName();
Here is the method in "TokenHelper.java":
/**
* Gets the token name from the Parameters in the ServletActionContext
*
* #return the token name found in the params, or null if it could not be found
*/
public static String getTokenName() {
Map params = ActionContext.getContext().getParameters();
if (!params.containsKey(TOKEN_NAME_FIELD)) {
LOG.warn("Could not find token name in params.");
return null;
}
String[] tokenNames = (String[]) params.get(TOKEN_NAME_FIELD);
String tokenName;
if ((tokenNames == null) || (tokenNames.length < 1)) {
LOG.warn("Got a null or empty token name.");
return null;
}
tokenName = tokenNames[0];
return tokenName;
}
The 1st line in this method is returning null:
Map params = ActionContext.getContext().getParameters();
The next LOC down, "params.containKey(...)" throws a NullPointerException because "params" is null.
When this action is called normally, this runs fine. However, during the JUnit test, this Null Pointer occurs.
My test class looks like this:
#Anonymous
public class MNManageLocationActionTest extends StrutsJUnit4TestCase {
private static MNManageLocationAction action;
#BeforeClass
public static void init() {
action = new MNManageLocationAction();
}
#Test
public void testGetActionMapping() {
ActionMapping mapping = getActionMapping("/companylocation/FetchCountyListByZip.action");
assertNotNull(mapping);
}
#Test
public void testLoadStateList() throws JSONException {
request.setParameter("Ryan", "Ryan");
String result = action.loadStateList();
assertEquals("Verify that the loadStateList() function completes without Exceptions.",
result, "success");
}
}
The ActionContext.getContext() is at least no longer null after I switched to using StrutsJUnit4TestCase.
Any idea why .getParameters() is returning null?
You need to initialize parameters map by yourself inside your test method. Additionally if you want to get token name you need to put it in parameters map.
Map<String, Object> params = new HashMap<String, Object>();
params.put(TokenHelper.TOKEN_NAME_FIELD,
new String[] { TokenHelper.DEFAULT_TOKEN_NAME });
ActionContext.getContext().setParameters(params);
Suppose I have a Website that is used in normal mode (browser) and in some other mode, like a MobileView mode (inside a mobile app). For each Controller I create, there might be correspondent controller for MobileView, processing the same url.
The easiest solution is to create ifs in all the Controllers that have MobileView logic. Another solution would be to use a correspondent url for MobileView (similar to the normal url) and two separate Controllers (possible where one extends from another; or use some other way to recycle common code)
But, a more elegant solution would be to have some extra annotations, like #SupportsMobileView (to mark a controller, and tell the app that this will have a correspondent MobileView Controller) and #MobileViewController (to mark a second controller, and tell the app that this controller needs to run immediately after the initial controller marked with #SupportsMobileView). The link between a normal controller and a MobileView controller would be through the url they process (defined with #RequestMapping).
Is it possible to extend Spring MVC (A)? Where to inject new annotation scanners (B) and annotation handlers / component handlers (C)? How should the MobileView controller be executed (D) (right now I am thinking that it could be executed through AOP, where the new handler of my new controller type programatically creates a Join-Point on the corresponding normal controller)
Note that I did not mention how this MobileView mode is triggered and detected. Let's just say that there a Session boolean variable (flag) for that.
Critics on any points (A), (B), (C) or (D) are welcomed, as well as technical hints and alternative solution to any point or the whole solution.
HandlerInterceptor can be used to intercept the RequestMapping handling. This is a simple example how to configure and implement one.
You can check for your session variable and will have a bunch of methods that will allow you to do custom processing or just exchange the view from the normal controller handling with your mobile view.
Ok, warnings:
this is only a proof of concept of what I understood must be done so:
+#MobileViewEnable and #MobileView annotated (and related) methods need to stay in the same controller
+there's no check for the httpAction used
+the two methods must have the same signature
+mobileView annotation value and requestMapping annotation value must be equals and uniques
+the logic inside callYourLogic(..) defines which method is going to be called, at the moment there's a very simple logic that check if exist the parameter ("mobile") in the request, just to test
+this code is not intended to be used as is (at all)
+don't know if it works at all outside my pc (joke :D, ehm..)
SO:
Annotations:
#Retention(RetentionPolicy.RUNTIME)
public #interface MobileView {
String value() default "";
}
#Retention(RetentionPolicy.RUNTIME)
public #interface MobileViewEnable {
}
ExampleController:
#Controller
public class MainController extends BaseController {
private final static Logger logger = LoggerFactory.getLogger(MainController.class);
private final static String PROVA_ROUTE = "prova";
#MobileViewEnable
#RequestMapping(PROVA_ROUTE)
public String prova() {
logger.debug("inside prova!!!");
return "provaview";
}
#MobileView(PROVA_ROUTE)
public String prova2() {
logger.debug("inside prova2!!!");
return "prova2view";
}
}
Aspect definition:
<bean id="viewAspect" class="xxx.yyy.ViewAspect" />
<aop:config>
<aop:pointcut expression="#annotation(xxx.yyy.MobileViewEnable)" id="viewAspectPointcut" />
<aop:aspect ref="viewAspect" order="1">
<aop:around method="around" pointcut-ref="viewAspectPointcut" arg-names="viewAspectPointcut"/>
</aop:aspect>
</aop:config>
Aspect implementation:
public class ViewAspect implements BeforeAdvice, ApplicationContextAware {
private final static Logger logger = LoggerFactory.getLogger(ViewAspect.class);
private ApplicationContext applicationContext;
public Object around(ProceedingJoinPoint joinPoint) {
Method mobileViewAnnotatedMethod = null;
HttpServletRequest request = getCurrentHttpRequest();
String controllerName = getSimpleClassNameWithFirstLetterLowercase(joinPoint);
Object[] interceptedMethodArgs = getInterceptedMethodArgs(joinPoint);
String methodName = getCurrentMethodName(joinPoint);
Method[] methods = getAllControllerMethods(joinPoint);
Method interceptedMethod = getInterceptedMethod(methods, methodName);
String interceptedMethodRoute = getRouteFromInterceptedMethod(interceptedMethod);
if (callYourLogic(request)) {
mobileViewAnnotatedMethod = getMobileViewAnnotatedMethodWithRouteName(methods, interceptedMethodRoute);
if (mobileViewAnnotatedMethod != null)
return invokeMethod(mobileViewAnnotatedMethod, interceptedMethodArgs, controllerName);
}
return continueInterceptedMethodExecution(joinPoint, interceptedMethodArgs);
}
private Object continueInterceptedMethodExecution(ProceedingJoinPoint joinPoint, Object[] interceptedMethodArgs) {
try {
return joinPoint.proceed(interceptedMethodArgs);
} catch (Throwable e) {
logger.error("unable to proceed with intercepted method call: " + e);
}
return null;
}
private Object[] getInterceptedMethodArgs(JoinPoint joinPoint) {
return joinPoint.getArgs();
}
private boolean callYourLogic(HttpServletRequest request) {
// INSERT HERE YOUR CUSTOM LOGIC (e.g.: is the server accessed from a mobile device?)
// THIS IS A STUPID LOGIC USED ONLY FOR EXAMPLE
return request.getParameter("mobile")!= null;
}
private HttpServletRequest getCurrentHttpRequest() {
return ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
}
private String invokeMethod(Method method, Object[] methodArgs, String className) {
if (method != null) {
try {
Object classInstance = getInstanceOfClass(method, className);
return (String) method.invoke(classInstance, methodArgs);
} catch (Exception e) {
logger.error("unable to invoke method" + method + " - " + e);
}
}
return null;
}
private Object getInstanceOfClass(Method method, String className) {
return applicationContext.getBean(className);
}
private Method getMobileViewAnnotatedMethodWithRouteName(Method[] methods, String routeName) {
for (Method m : methods) {
MobileView mobileViewAnnotation = m.getAnnotation(MobileView.class);
if (mobileViewAnnotation != null && mobileViewAnnotation.value().equals(routeName))
return m;
}
return null;
}
private String getRouteFromInterceptedMethod(Method method) {
RequestMapping requestMappingAnnotation = method.getAnnotation(RequestMapping.class);
if (requestMappingAnnotation != null)
return requestMappingAnnotation.value()[0];
return null;
}
private String getCurrentMethodName(JoinPoint joinPoint) {
return joinPoint.getSignature().getName();
}
private Method[] getAllControllerMethods(JoinPoint joinPoint) {
return joinPoint.getThis().getClass().getSuperclass().getMethods();
}
private String getSimpleClassNameWithFirstLetterLowercase(JoinPoint joinPoint) {
String simpleClassName = joinPoint.getThis().getClass().getSuperclass().getSimpleName();
return setFirstLetterLowercase(simpleClassName);
}
private String setFirstLetterLowercase(String simpleClassName) {
String firstLetterOfTheString = simpleClassName.substring(0, 1).toLowerCase();
String restOfTheString = simpleClassName.substring(1);
return firstLetterOfTheString + restOfTheString;
}
private Method getInterceptedMethod(Method[] methods, String lookingForMethodName) {
for (Method m : methods)
if (m.getName().equals(lookingForMethodName))
return m;
return null;
}
#Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}