Adding a tenant ID header to MessagingGateway for every message sent - spring

I have an abstract class in a library project where I do:
this.eventGateway.publishEvent("SomeEvent", null);
and EventGateway is this:
#MessagingGateway
public interface EventGateway {
#Gateway(requestChannel = "SomeChannel")
void publishEvent(#Header(value = "EventName") String event, #Payload Object payload);
}
and my SomeChannel definition:
#Bean(name = "SomeChannel)
public MessageChannel someChannel() {
return new PublishSubscribeChannel(Executors.newCachedThreadPool());
}
Now it's been working fine until we wanted to use the library in a multi-tenant environment, where we want to add a header (say, "TenantId") to every message we send using EventGateway. The obvious solution would be adding EventGateway this method:
#Gateway(requestChannel = "SomeChannel")
void publishTenantEvent(#Header(value = "EventName") String event, #Header(value = "TenantId") String tenantId, #Payload Object payload);
and using it like:
final TenantContext context = TenantContextHolder.getContext();
final Tenant tenant = context.getTenant();
this.eventGateway.publishEvent("SomeEvent", tenant.getId(), null);
But changing every place where we do this.eventGateway.publishEvent("SomeEvent", null); to above is almost equal to writing the library from scratch.
FWIW, my SomeChannel definition is as follows:
Is there a way that I can add "TenantId" header to every message I send if it's present in TenantContextHolder (a class holding a thread local variable TenantContext)?

The #Gateway has a:
/**
* Specify additional headers that will be added to the request message.
* #return the headers.
*/
GatewayHeader[] headers() default { };
That #GatewayHeader has this:
/**
* #return The {#code Expression} to be evaluated to produce a value for the header.
*/
String expression() default "";
which you can declare for your use-case like:
#Gateway(requestChannel = "SomeChannel",
headers = #GatewayHeader(name = "TenantId",
expression = "T(TenantContextHolder).context.tenant.id"))
Where you have to use a fully-qualified class name for that TenantContextHolder.
See more info about T operator in SpEL docs: https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions-types. The getters are resolved from the property names.

Related

SftpOutboundGateway - pass localDirectory dynamically

I have the below code snippet to download a file from a remote server. I would like to update it to be more generic, so a directory where the file should be downloaded (localDirectory) could be passed as a parameter. Is it possible to somehow update the gateway's method or extend the handler to configure it?
#Bean
#ServiceActivator(inputChannel = "channel")
public MessageHandler handler() {
SftpOutboundGateway handler = new SftpOutboundGateway(sftpSessionFactory(), "get", "payload");
handler.setLocalDirectory("/folder"); //can this be pointed dynamically when downloading a file?
return handler;
}
#MessagingGateway
public interface TestGateway{
#Gateway(requestChannel = "channel")
File getFile(String filePath);
}
For that purpose we suggest a SpEL-based options to evaluate a value from request message a runtime. See this one for your use-case:
/**
* Specify a SpEL expression to evaluate the directory path to which remote files will
* be transferred.
* #param localDirectoryExpression the SpEL to determine the local directory.
* #since 5.0
*/
public void setLocalDirectoryExpressionString(String localDirectoryExpression) {
So, this one can be configured in your gateway like this:
handler.setLocalDirectoryExpressionString("headers.my_local_directory");
There is also a #remoteDirectory SpEL variable in present in the EvaluationContext for your convenience.
See more info in docs:
https://docs.spring.io/spring-integration/reference/html/spel.html#spel

Why does Mockito return null when I specify an Optional of my type

I have a method on a controller to get a list of all the punishment types for a chat room (Kick, Ban, Warn and Mute). On the first test when I mock the data it works as expected and the test passes.
However, on my second test I provided. I defined what should be returned as an Optional<Punishment> with the attribute of punishmentName set as "mute". I am very confused why this is giving me null. When I run the Spring application outside of testing, the route works fine. For some reason the mock never wants to return the value I specified but only null. Specifically, this is being caught in the test on the line .andExpect(jsonPath("$.punishmentName", Matchers.equalTo("mute"))); as the fields value is null giving the following error:
java.lang.AssertionError: No value at JSON path "$.punishmentName"
For clarity, I have also provided the controller methods and service methods.
Punishment Controller Test:
#WebMvcTest(PunishmentController.class)
#RunWith(SpringRunner.class)
public class PunishmentControllerTest {
#Autowired
private MockMvc mvc;
#MockBean
private PunishmentService punishmentService;
#MockBean
private PunishmentValidator punishmentValidator;
#Test
public void getAllPunishmentTypesReturnsAListOfPunishmentTypes() throws Exception {
List<Punishment> punishments = new ArrayList<>();
punishments.add(new Punishment("mute"));
punishments.add(new Punishment("kick"));
punishments.add(new Punishment("ban"));
punishments.add(new Punishment("warn"));
Mockito.when(punishmentService.getAllPunishmentTypes()).thenReturn(punishments);
mvc.perform(get("/api/punishments"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.hasSize(4)))
.andExpect(jsonPath("$[0].punishmentName", Matchers.equalTo("mute")))
.andExpect(jsonPath("$[1].punishmentName", Matchers.equalTo("kick")))
.andExpect(jsonPath("$[2].punishmentName", Matchers.equalTo("ban")))
.andExpect(jsonPath("$[3].punishmentName", Matchers.equalTo("warn")));
}
#Test
public void getPunishmentTypeReturnsMuteWhenMuteIsSpecified() throws Exception {
Optional<Punishment> mute = Optional.of(new Punishment("mute"));
Mockito.when(punishmentService.getPunishmentType("mute")).thenReturn(mute);
mvc.perform(get("/api/punishments/mute"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.punishmentName", Matchers.equalTo("mute")));
}
Controller methods:
/**
* GET request for all punishment types.
* #return List<Punishment> - When Punishments are found in the database they are returned in a List object.
* Otherwise, an empty list is returned if no records are found or an error occurs.
*/
#GetMapping
public List<Punishment> getAllPunishments() {
return punishmentService.getAllPunishmentTypes();
}
/**
* GET request for one punishment type.
* #param punishmentType String - The type of punishment.
* #return Optional<Punishment> - The rule that gets returned or an empty optional if no rule is found.
*/
#GetMapping(path = "{punishmentType}")
public Optional<Punishment> getPunishment(#PathVariable("punishmentType") String punishmentType) {
boolean isPunishmentTypeValid = punishmentValidator.validatePunishmentName(punishmentType);
if (isPunishmentTypeValid) {
return punishmentService.getPunishmentType(punishmentType);
} else {
return Optional.empty();
}
}
}
Service methods:
/**
* Gets all the punishment types
* #return List<Punishment> - The rules in the community
*/
public List<Punishment> getAllPunishmentTypes() {
return punishmentRepository.findAll();
}
/**
* Gets a specific punishment type.
* #param punishmentType String - The type of punishment.
* #return The punishment retrieved.
*/
public Optional<Punishment> getPunishmentType(String punishmentType) {
return punishmentRepository.findById(punishmentType);
}
I believe it is because you forget to mock the method PunishmentValidator#validatePunishmentName("mute") to return true such that the method that you stub on PunishmentService is never invoked because by default if you do not stub a method , it will return false (see this).
Also it is a known behaviour that #MockBean is configured as lenient stubbing which will not reported error (i.e. throw UnnecessaryStubbingException) if you stub a method but it actually does not get executed.
So change the following should fix your problem :
#Test
public void getPunishmentTypeReturnsMuteWhenMuteIsSpecified() throws Exception {
Optional<Punishment> mute = Optional.of(new Punishment("mute"));
Mockito.when(punishmentService.getPunishmentType("mute")).thenReturn(mute);
Mockito.when(punishmentValidator.validatePunishmentName("mute")).thenReturn(true);
mvc.perform(get("/api/punishments/mute"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.punishmentName", Matchers.equalTo("mute")));
}

spring: customizing the authorizationEndpoint (OAuth2)

I am trying to customize the code of the spring oauth authorization server.
for now I have just copied the framework authorizationEndpoint code and placed it in another class. I just changed the address mapping to /custom/oauth/authorize. I have also added #Controller before the class declaration otherwise this code will not be used at all:
#Controller
//#Order(Ordered.HIGHEST_PRECEDENCE)
#SessionAttributes("authorizationRequest")
public class AuthorizationEndpointCustom extends AuthorizationEndpoint {
#Autowired
private AuthenticationManager authenticationManager;
private AuthorizationCodeServices authorizationCodeServices = new InMemoryAuthorizationCodeServices();
private RedirectResolver redirectResolver = new DefaultRedirectResolver();
private UserApprovalHandler userApprovalHandler = new DefaultUserApprovalHandler();
private SessionAttributeStore sessionAttributeStore = new DefaultSessionAttributeStore();
private OAuth2RequestValidator oauth2RequestValidator = new DefaultOAuth2RequestValidator();
private String userApprovalPage = "forward:/oauth/confirm_access";
private String errorPage = "forward:/oauth/error";
private Object implicitLock = new Object();
public void setSessionAttributeStore(SessionAttributeStore sessionAttributeStore) {
this.sessionAttributeStore = sessionAttributeStore;
}
public void setErrorPage(String errorPage) {
this.errorPage = errorPage;
}
#RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, #RequestParam Map<String, String> parameters,
SessionStatus sessionStatus, Principal principal) {
System.out.println("\n\ninside custom authorization endpoint");
// Pull out the authorization request first, using the OAuth2RequestFactory. All further logic should
// query off of the authorization request instead of referring back to the parameters map. The contents of the
// parameters map will be stored without change in the AuthorizationRequest object once it is created.
AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);
Set<String> responseTypes = authorizationRequest.getResponseTypes();
if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
}
if (authorizationRequest.getClientId() == null) {
throw new InvalidClientException("A client id must be provided");
}
try {
if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
throw new InsufficientAuthenticationException(
"User must be authenticated with Spring Security before authorization can be completed.");
}
ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
// The resolved redirect URI is either the redirect_uri from the parameters or the one from
// clientDetails. Either way we need to store it on the AuthorizationRequest.
String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
if (!StringUtils.hasText(resolvedRedirect)) {
throw new RedirectMismatchException(
"A redirectUri must be either supplied or preconfigured in the ClientDetails");
}
authorizationRequest.setRedirectUri(resolvedRedirect);
// We intentionally only validate the parameters requested by the client (ignoring any data that may have
// been added to the request by the manager).
oauth2RequestValidator.validateScope(authorizationRequest, client);
// Some systems may allow for approval decisions to be remembered or approved by default. Check for
// such logic here, and set the approved flag on the authorization request accordingly.
authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
(Authentication) principal);
// TODO: is this call necessary?
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);
// Validation is all done, so we can check for auto approval...
if (authorizationRequest.isApproved()) {
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest);
}
if (responseTypes.contains("code")) {
return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
(Authentication) principal));
}
}
// Place auth request into the model so that it is stored in the session
// for approveOrDeny to use. That way we make sure that auth request comes from the session,
// so any auth request parameters passed to approveOrDeny will be ignored and retrieved from the session.
model.put("authorizationRequest", authorizationRequest);
return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
}
catch (RuntimeException e) {
sessionStatus.setComplete();
throw e;
}
}
private OAuth2AccessToken getAccessTokenForImplicitGrant(TokenRequest tokenRequest,
OAuth2Request storedOAuth2Request) {
OAuth2AccessToken accessToken = null;
// These 1 method calls have to be atomic, otherwise the ImplicitGrantService can have a race condition where
// one thread removes the token request before another has a chance to redeem it.
synchronized (this.implicitLock) {
accessToken = getTokenGranter().grant("implicit",
new ImplicitTokenRequest(tokenRequest, storedOAuth2Request));
}
return accessToken;
}
.
.
.
I have also instructed the framework to change the mappring from /oauth/authorize to /custom/oauth/authorize:
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager).tokenStore(tokenStore())
.accessTokenConverter(tokenEnhancer()).pathMapping("/oauth/authorize", "/custom/authorize/");
}
but when I run the code I encounter the following error:
Description:
Field tokenGranter in com.example.demo.controller.AuthorizationEndpointCustom required a bean of type 'org.springframework.security.oauth2.provider.TokenGranter' that could not be found.
The injection point has the following annotations:
- #org.springframework.beans.factory.annotation.Autowired(required=true)
Action:
Consider defining a bean of type 'org.springframework.security.oauth2.provider.TokenGranter' in your configuration.
the parent class of AuthorizationEndpoint (AbstractEndpoint) declares tokenGranter but it is not instantiated. there is no #autowired for this and other attributes of this class. who does genereate and inject these variable into this class?
how can I get hold of tokenGranter obj and inject it?
/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.provider.endpoint;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.util.Assert;
/**
* #author Dave Syer
*
*/
public class AbstractEndpoint implements InitializingBean {
protected final Log logger = LogFactory.getLog(getClass());
private WebResponseExceptionTranslator providerExceptionHandler = new DefaultWebResponseExceptionTranslator();
private TokenGranter tokenGranter;
private ClientDetailsService clientDetailsService;
private OAuth2RequestFactory oAuth2RequestFactory;
private OAuth2RequestFactory defaultOAuth2RequestFactory;
public void afterPropertiesSet() throws Exception {
Assert.state(tokenGranter != null, "TokenGranter must be provided");
Assert.state(clientDetailsService != null, "ClientDetailsService must be provided");
defaultOAuth2RequestFactory = new DefaultOAuth2RequestFactory(getClientDetailsService());
if (oAuth2RequestFactory == null) {
oAuth2RequestFactory = defaultOAuth2RequestFactory;
}
}
public void setProviderExceptionHandler(WebResponseExceptionTranslator providerExceptionHandler) {
this.providerExceptionHandler = providerExceptionHandler;
}
public void setTokenGranter(TokenGranter tokenGranter) {
this.tokenGranter = tokenGranter;
}
protected TokenGranter getTokenGranter() {
return tokenGranter;
}
protected WebResponseExceptionTranslator getExceptionTranslator() {
return providerExceptionHandler;
}
protected OAuth2RequestFactory getOAuth2RequestFactory() {
return oAuth2RequestFactory;
}
protected OAuth2RequestFactory getDefaultOAuth2RequestFactory() {
return defaultOAuth2RequestFactory;
}
public void setOAuth2RequestFactory(OAuth2RequestFactory oAuth2RequestFactory) {
this.oAuth2RequestFactory = oAuth2RequestFactory;
}
protected ClientDetailsService getClientDetailsService() {
return clientDetailsService;
}
public void setClientDetailsService(ClientDetailsService clientDetailsService) {
this.clientDetailsService = clientDetailsService;
}
}
I am answering my own question.
I took a good look at the framework code and I found out that AuthorizationServerEndpointsConfiguration class creates an object of type AuthorizationEndpoint and populates it's attributes and then return this object as a bean.
I managed to solve above mentioned problem with TokenGranter by creating a bean of my new AuthorizationEndpointCustom the same way AuthorizationServerEndpointsConfiguration does. this is the code to do so:
#Autowired
private ClientDetailsService clientDetailsService;
#Autowired
AuthorizationServerEndpointsConfiguration asec;
#Bean
#Order(value = Ordered.HIGHEST_PRECEDENCE)
#Primary
public AuthorizationEndpoint authorizationEndpoint () throws Exception{
AuthorizationEndpointCustom authorizationEndpoint = new AuthorizationEndpointCustom();
FrameworkEndpointHandlerMapping mapping = asec.getEndpointsConfigurer().getFrameworkEndpointHandlerMapping();
authorizationEndpoint.setUserApprovalPage(extractPath(mapping, "/oauth/confirm_access"));
authorizationEndpoint.setProviderExceptionHandler(asec.getEndpointsConfigurer().getExceptionTranslator());
authorizationEndpoint.setErrorPage(extractPath(mapping, "/oauth/error"));
authorizationEndpoint.setTokenGranter(asec.getEndpointsConfigurer().getTokenGranter());
authorizationEndpoint.setClientDetailsService(clientDetailsService);
authorizationEndpoint.setAuthorizationCodeServices(asec.getEndpointsConfigurer().getAuthorizationCodeServices());
authorizationEndpoint.setOAuth2RequestFactory(asec.getEndpointsConfigurer().getOAuth2RequestFactory());
authorizationEndpoint.setOAuth2RequestValidator(asec.getEndpointsConfigurer().getOAuth2RequestValidator());
authorizationEndpoint.setUserApprovalHandler(asec.getEndpointsConfigurer().getUserApprovalHandler());
return authorizationEndpoint;
}
private String extractPath(FrameworkEndpointHandlerMapping mapping, String page) {
String path = mapping.getPath(page);
if (path.contains(":")) {
return path;
}
return "forward:" + path;
}
but this did not result in what I hoped to. the new bean does not replace the bean from framework code. this situation with overriding beans led to another question:
how replace framework beans
buttom line, this is not the way to override the framework endpoints. you can simply create a controller with mappings for these endpoints (e.g /oauth/authorize or /oauth/token). automatically these mappings will get precedence over framework endpoints. for more info refer to spring doc

ActionContext.getContext().getParameters() returns null during StrutsJUnit4TestCase

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

Add camel route at runtime in Java

How can I add a camel route at run-time in Java? I have found a Grails example but I have implement it in Java.
My applicationContext.xml already has some predefined static routes and I want to add some dynamic routes to it at run time.
Is it possible?
Because the only way to include dynamic route is to write the route.xml and then load the route definition to context. How will it work on existing static routes?
Route at runtime
you can simply call a few different APIs on the CamelContext to add routes...something like this
context.addRoutes(new MyDynamcRouteBuilder(context, "direct:foo", "mock:foo"));
....
private static final class MyDynamcRouteBuilder extends RouteBuilder {
private final String from;
private final String to;
private MyDynamcRouteBuilder(CamelContext context, String from, String to) {
super(context);
this.from = from;
this.to = to;
}
#Override
public void configure() throws Exception {
from(from).to(to);
}
}
see this unit test for the complete example...
https://svn.apache.org/repos/asf/camel/trunk/camel-core/src/test/java/org/apache/camel/builder/AddRoutesAtRuntimeTest.java
#Himanshu,
Please take a look at dynamicroute options (in other words routing slip) that may help you dynamically route to different 'destinations' based on certain condition.
Check the dynamic router help link in camel site;
http://camel.apache.org/dynamic-router.html
from("direct:start")
// use a bean as the dynamic router
.dynamicRouter(method(DynamicRouterTest.class, "slip"));
And within the slip method;
/**
* Use this method to compute dynamic where we should route next.
*
* #param body the message body
* #return endpoints to go, or <tt>null</tt> to indicate the end
*/
public String slip(String body) {
bodies.add(body);
invoked++;
if (invoked == 1) {
return "mock:a";
} else if (invoked == 2) {
return "mock:b,mock:c";
} else if (invoked == 3) {
return "direct:foo";
} else if (invoked == 4) {
return "mock:result";
}
// no more so return null
return null;
}
Hope it helps...
Thanks.
One such solution could be:
Define route:
private RouteDefinition buildRouteDefinition() {
RouteDefinition routeDefinition = new RouteDefinition();
routeDefinition.from(XX).to(ZZ); // define any route you want
return routeDefinition;
}
Get Model Context and create route:
CamelContext context = getContext();
ModelCamelContext modelContext = context.adapt(ModelCamelContext.class);
modelContext.addRouteDefinition(routeDefinition);
There are more way of getting camel context. To name few:
In processor, you can use exchange.getContext()
Through RouteBuilder reference, you can use routeBuilder.getContext()

Resources