I am doing spring batch application to get the data from database and finally updating some records in the database.
The problem is ItemProcessor and ItemWriter are not being called.
here is my config file.
<job id="BpmJob" xmlns="http://www.springframework.org/schema/batch">
<step id="step1">
<tasklet transaction-manager="transactionManager">
<chunk reader="pagingItemReader" processor="testApp" writer="itemWriter" commit-interval="1" />
</tasklet>
</step>
</job>
<bean id="pagingItemReader" class="com.tcs.controller.BpmReader" scope="step" />
<bean id="testApp" class="com.tcs.controller.BpmProcess" scope="step" />
<bean id="itemWriter" class="com.tcs.controller.BpmWriter" scope="step" />
and i customized ItemProcessor and Item writer.
#Component
public class BpmWriter implements ItemWriter<List<User>> {
#Autowired
private JdbcTemplate jdbcTemplate;
#Override
public void write(List<? extends List<User>> userList) throws Exception {
for(int i=0;i<userList.size();i++){
User user=(User)userList.get(i);
try{
String query="update com_tt_bpm_batch set status =:status where seqNo =:seqNo";
SqlParameterSource namedParameters = new MapSqlParameterSource();
((MapSqlParameterSource) namedParameters).addValue("status","INACTIVE");
((MapSqlParameterSource) namedParameters).addValue("seqNo",user.getSeqNo());
jdbcTemplate.update(query, namedParameters);
logger.info("updation is successful for seqNo "+user.getSeqNo());
}catch(Exception e){
logger.error("exception at updating the status to inactive ..");
logger.error(e.getStackTrace());
}
}
}
}
Customzied ItemProcessor
#Component
public class BpmProcess implements ItemProcessor<User,User>{
List<User>userList=new ArrayList<User>();
private User userDetails;
private static final Logger logger=Logger.getLogger(BpmProcess. class);
#Override
public User process(User user) {
try{
if(logger.isDebugEnabled()){
logger.debug("process method begins");
}
userDetails=new User();
userDetails.setSeqNo(user.getSeqNo());
userList.add(userDetails);
if(logger.isDebugEnabled()){
logger.debug("process method ends");
}
}
catch(Exception e){
logger.error("Exception at data processing");
logger.error(e.getMessage());
}
return userDetails;
}
}
Related
We are in the process of moving to Azure SQL Server from Oracle DB for our Spring Batch application.
I am getting the following error while trying to execute two different jobs at the same time that updates different tables however uses the same common BATCH_ tables
Caused by: org.springframework.dao.DataAccessResourceFailureException:
Could not increment identity; nested exception is
com.microsoft.sqlserver.jdbc.SQLServerException: Transaction (Process
ID 167) was deadlocked on lock resources with another process and has
been chosen as the deadlock victim. Rerun the transaction. at
org.springframework.jdbc.support.incrementer.SqlServerMaxValueIncrementer.getNextKey(SqlServerMaxValueIncrementer.java:124)
~[bat-applybatch-jobs-2.2.12-SNAPSHOT.jar:?] at
org.springframework.jdbc.support.incrementer.AbstractDataFieldMaxValueIncrementer.nextLongValue(AbstractDataFieldMaxValueIncrementer.java:125)
My Job Repository configuration
<job-repository id="jobRepository" isolation-level-for-create="READ_COMMITED" />
Database deadlock
<deadlock>
<victim-list>
<victimProcess id="process2a41675a4e8" />
</victim-list>
<process-list>
<process id="process2a41675a4e8" taskpriority="0" logused="280" waitresource="RID: 6:9:24682488:29" waittime="4984" ownerId="696000712" transactionname="implicit_transaction" lasttranstarted="2021-12-29T12:18:30.153" XDES="0x29a22bc4428" lockMode="U" schedulerid="4" kpid="52760" status="suspended" spid="173" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2021-12-29T12:18:30.157" lastbatchcompleted="2021-12-29T12:18:30.153" lastattention="1900-01-01T00:00:00.153" clientapp="Microsoft JDBC Driver for SQL Server" hostname="ServerName" hostpid="0" loginname="LoginName" isolationlevel="read committed (2)" xactid="696000712" currentdb="6" currentdbname="Database" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128058">
<executionStack>
<frame procname="unknown" queryhash="0xadc42a7474869694" queryplanhash="0x238c4f9df8a5d6cc" line="1" stmtstart="26" stmtend="146" sqlhandle="0x020000007654041849f4ffe980c136b592ccbe8260983e220000000000000000000000000000000000000000">
unknown </frame>
<frame procname="unknown" queryhash="0xadc42a7474869694" queryplanhash="0x238c4f9df8a5d6cc" line="1" stmtend="126" sqlhandle="0x0200000045a2af306ade799ae9ffa65edc0f722c526e26330000000000000000000000000000000000000000">
unknown </frame>
</executionStack>
<inputbuf>
delete from LoginName.BATCH_STEP_EXECUTION_SEQ where ID < 10899 </inputbuf>
</process>
<process id="process2a42d680ca8" taskpriority="0" logused="420" waitresource="RID: 6:9:24682490:8" waittime="4984" ownerId="696000707" transactionname="implicit_transaction" lasttranstarted="2021-12-29T12:18:30.153" XDES="0x2a41ae18428" lockMode="U" schedulerid="7" kpid="53280" status="suspended" spid="129" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2021-12-29T12:18:30.153" lastbatchcompleted="2021-12-29T12:18:30.153" lastattention="1900-01-01T00:00:00.153" clientapp="Microsoft JDBC Driver for SQL Server" hostname="ServerName" hostpid="0" loginname="LoginName" isolationlevel="read committed (2)" xactid="696000707" currentdb="6" currentdbname="Database" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128058">
<executionStack>
<frame procname="unknown" queryhash="0xadc42a7474869694" queryplanhash="0x238c4f9df8a5d6cc" line="1" stmtstart="26" stmtend="146" sqlhandle="0x020000007654041849f4ffe980c136b592ccbe8260983e220000000000000000000000000000000000000000">
unknown </frame>
<frame procname="unknown" queryhash="0xadc42a7474869694" queryplanhash="0x238c4f9df8a5d6cc" line="1" stmtend="126" sqlhandle="0x02000000a0f1f51de77e1eefa19367c42fc9d1938c2075020000000000000000000000000000000000000000">
unknown </frame>
</executionStack>
<inputbuf>
delete from LoginName.BATCH_STEP_EXECUTION_SEQ where ID < 10898 </inputbuf>
</process>
</process-list>
<resource-list>
<ridlock fileid="9" pageid="24682488" dbid="6" objectname="162589bb-bc36-4834-8bdc-e58a2deca742.LoginName.BATCH_STEP_EXECUTION_SEQ" id="lock2a043bbcc00" mode="X" associatedObjectId="72057594071547904">
<owner-list>
<owner id="process2a42d680ca8" mode="X" />
</owner-list>
<waiter-list>
<waiter id="process2a41675a4e8" mode="U" requestType="wait" />
</waiter-list>
</ridlock>
<ridlock fileid="9" pageid="24682490" dbid="6" objectname="162589bb-bc36-4834-8bdc-e58a2deca742.LoginName.BATCH_STEP_EXECUTION_SEQ" id="lock29f5f1b7f00" mode="X" associatedObjectId="72057594071547904">
<owner-list>
<owner id="process2a41675a4e8" mode="X" />
</owner-list>
<waiter-list>
<waiter id="process2a42d680ca8" mode="U" requestType="wait" />
</waiter-list>
</ridlock>
</resource-list>
</deadlock>
tried:
<job-repository id="jobRepository" isolation-level-for-create="READ_UNCOMMITED" />
<job-repository id="jobRepository"
isolation-level-for-create="ISOLATION_REPEATABLE_READ" />
<job-repository id="jobRepository"
isolation-level-for-create="SERIALIZABLE" />
I have created the tables as highlighted below
CREATE TABLE BATCH_STEP_EXECUTION_SEQ (
ID BIGINT IDENTITY(<last Oracle sequence value>, 1)
);
CREATE TABLE BATCH_JOB_EXECUTION_SEQ (
ID BIGINT IDENTITY(<last Oracle sequence value>, 1)
);
CREATE TABLE BATCH_JOB_SEQ (
ID BIGINT IDENTITY(<last Oracle sequence value>, 1)
);
what is the issue? How do I fix this?
Update: Job Definition
<bean id="simpleStep" class="org.springframework.batch.core.step.factory.SimpleStepFactoryBean"
abstract="true">
<property name="transactionManager" ref="transactionManager" />
<property name="jobRepository" ref="jobRepository" />
<property name="startLimit" value="100" />
<property name="commitInterval" value="1" />
</bean>
Update#2:
Can I try something like this?
<bean id="informixIncrementer" class="com.bah.batch.informixsupport.InformixMaxValueIncrementerFactory"><property name="dataSource" ref="dataSource" />
<bean id="jobRepository" class="org.springframework.batch.core.repository.support.JobRepositoryFactoryBean" isolation-level-for-create="READ_COMMITTED" table-prefix="BATCH_">
<property name="incrementerFactory" ref="informixIncrementer"/>
</bean>
I'm afraid this is a known bug in Spring Batch 4.x that is currently only planned to be resolved with Spring Batch 5, which is not to be expected for a couple of months: https://github.com/spring-projects/spring-batch/issues/3927
You can emulate the fix by making local adjustments as in the commit of the fix: https://github.com/spring-projects/spring-batch/commit/fe911c8456bb49a69b1c84c78c0a0e0fdf224803, i.e.
Adjust the schema to contain sequences
Change the DataFieldMaxValueIncrementerFactory that is used to build the JobRepository.
I don't think the latter is feasible with pure XML configuration. The discussion on this issue contains some hints how it can be done in Java: https://github.com/spring-projects/spring-batch/issues/1448
The concrete customization depends on the customizations that you already have but it should work at least roughly as follows. You can apply the incrementer factory with a BatchConfigurer:
#Bean
public BatchConfigurer batchConfigurer(DataSource dataSource) {
return new DefaultBatchConfigurer(dataSource) {
#Override
protected JobRepository createJobRepository() throws Exception {
JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
factory.setDataSource(getDataSource());
factory.setTransactionManager(getTransactionManager());
factory.setIncrementerFactory(new MyIncrementerFactory(getDataSource()));
factory.afterPropertiesSet();
return factory.getObject();
}
};
}
where
public class MyIncrementerFactory implements DataFieldMaxValueIncrementerFactory {
private final DataSource dataSource;
public MyIncrementerFactory(DataSource dataSource) {
this.dataSource = dataSource;
}
#Override
public DataFieldMaxValueIncrementer getIncrementer(String databaseType, String incrementerName) {
return new SqlServerSequenceMaxValueIncrementer(dataSource, incrementerName);
}
#Override
public boolean isSupportedIncrementerType(String databaseType) {
return true;
}
#Override
public String[] getSupportedIncrementerTypes() {
return null; // method should not get called anyway
}
}
and SqlServerSequenceMaxValueIncrementer should be the incrementer from the commit.
Basically, you need to overwrite incrementerFactory in JobRepository like so:
#Bean
public JobRepository jobRepository(DataSource dataSource, PlatformTransactionManager transactionManager) throws Exception {
JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
factory.setDataSource(dataSource);
factory.setTransactionManager(transactionManager);
factory.setIncrementerFactory(new DefaultDataFieldMaxValueIncrementerFactory(dataSource) {
#Override
public DataFieldMaxValueIncrementer getIncrementer(String incrementerType, String incrementerName) {
return new SqlServerSequenceMaxValueIncrementer(dataSource, incrementerName);
}
});
factory.afterPropertiesSet();
return factory.getObject();
}
#Bean
public SimpleJobLauncher jobLauncher(JobRepository jobRepository) {
SimpleJobLauncher simpleJobLauncher = new SimpleJobLauncher();
simpleJobLauncher.setJobRepository(jobRepository);
return simpleJobLauncher;
}
And here is the source code of SqlServerSequenceMaxValueIncrementer which you can include in your codebase.
I want to pass some data from one step to another, say from step_1 to step_2. I've passed the data into an ExecutionContext in step_1. In step_2, I'm attempting to recover the data with a #beforeStep that retrieves the JobExecution.
However, step_2 references a CompositeItemProcessor, which does not seem to be registered as a listener by default. How can I register an ItemProcessListener to just one process of the CompositeItemProcessor to recover the JobExecution (to then recover the data from step_1)?
to register any listener , you can list the listeners in your job - inside the step and then when you implement the listener - you can implement specific listeners
<bean id="customItemProcessListener"
class="com.listeners.CustomItemProcessListener" />
<job id="myJob" xmlns="http://www.springframework.org/schema/batch">
<step id="step2">
<tasklet>
<chunk reader="reader" writer="writer"
commit-interval="1" />
<listeners>
<listener ref="customItemProcessListener" />
</listeners>
</tasklet>
</step>
</job>
and then you can implement the processor
public class CustomItemProcessListener implements ItemProcessListener<T> {
#Override
public void beforeProcess(T items) {
System.out.println("ItemProcessListener - beforeProcess");
}
#Override
public void afterProcess(T items , S result) {
System.out.println("ItemProcessListener - afterProcess");
}
#Override
public void onProcessError(T items , Exception exception) {
System.out.println("ItemProcessListener - onProcessError ");
}
}
I have a Spring Batch job that I am launching from a Spring boot application, like so:
Main:
#SpringBootApplication
#ImportResource("jobApplicationContext.xml")
public class BatchJobRunner {
public static void main(String[] args) {
SpringApplication.run(BatchJobRunner.class, args);
}
}
In my job's application context, I have the following items:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:batch="http://www.springframework.org/schema/batch"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="classpath:*.properties"/>
<bean id="jobRegistry" class="org.springframework.batch.core.configuration.support.MapJobRegistry"/>
<bean id="jobRepository" class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean"/>
<bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
<property name="jobRepository" ref="jobRepository" />
</bean>
<batch:job id="myJob" job-repository="jobRepository">
<batch:split id="main" task-executor="simpleAsyncTaskExecutor" next="step3">
<batch:flow>
<batch:step id="flow1">
<!-- definition -->
</batch:step>
</batch:flow>
<batch:flow>
<batch:step id="flow2">
<!-- definition -->
</batch:step>
</batch:flow>
</batch:split>
<batch:step id="step3">
<batch:tasklet ref="someTasklet"/>
</batch:step>
</batch:job>
</beans>
And finally, I just run it like this:
java -jar my-module.jar
The job starts but:
It does not print out anything. Here is my log4j.properties:
log4j.rootLogger=INFO, stdout
log4j.logger.org.springframework.batch=INFO
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
The job hangs at the end. I put a Sys.out.print in the step3 and it does indeed print, but the spring boot application keeps running and never exits. I also tried to add an #AfterJob with a System.exit(..) and it didn't help either.
I am using Spring f/w 4.1.8, spring boot 1.2.8 and spring batch 3.0.6 (i cannot upgrade my spring-core as some dependencies use that version).
Any idea what I am doing wrong?
Edit:
Looks like beforeJob and afterJob listeners are not firing at all.
ClassCastExeption can be result of Spring different binding (early in xml and late in java). Try configure your batch fully in java. Result can look like this (this is with stored repository in DB, inmemory repository should looks similar):
#Configuration
#EnableBatchProcessing
#ComponentScan("my.batch.*")
#ImportResource("classpath:batch-config.xml")
#PropertySource(value = "classpath:batch.properties")
public class BatchConfiguration implements BatchConfigurer {
#Autowired
private DataSource dataSource;
private PlatformTransactionManager transactionManager;
private JobRepository jobRepository;
private JobLauncher jobLauncher;
private JobExplorer jobExplorer;
#Override
public JobRepository getJobRepository() throws Exception {
return jobRepository;
}
#Override
public PlatformTransactionManager getTransactionManager() throws Exception {
return transactionManager;
}
#Override
public JobLauncher getJobLauncher() throws Exception {
return jobLauncher;
}
#Override
public JobExplorer getJobExplorer() throws Exception {
return jobExplorer;
}
#PostConstruct
public void initialize() {
try {
transactionManager = new DataSourceTransactionManager(dataSource);
jobRepository = createJobRepository();
jobExplorer = createJobExplorer();
jobLauncher = createJobLauncher();
} catch (Exception ex) {
throw new BatchConfigurationException(ex);
}
}
private JobRepository createJobRepository() throws Exception {
JobRepositoryFactoryBean repoFactory = new JobRepositoryFactoryBean();
repoFactory.setDataSource(dataSource);
repoFactory.setTransactionManager(transactionManager);
repoFactory.setTablePrefix(PREFIX);
repoFactory.afterPropertiesSet();
return repoFactory.getObject();
}
private JobExplorer createJobExplorer() throws Exception {
JobExplorerFactoryBean explorerFactory = new JobExplorerFactoryBean();
explorerFactory.setDataSource(dataSource);
explorerFactory.setTablePrefix(PREFIX);
explorerFactory.afterPropertiesSet();
return explorerFactory.getObject();
}
private JobLauncher createJobLauncher() throws Exception {
SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
jobLauncher.setJobRepository(jobRepository);
jobLauncher.setTaskExecutor(new SimpleAsyncTaskExecutor());
jobLauncher.afterPropertiesSet();
return jobLauncher;
}
}
You miss #EnableBatchProcessing docs.
I'm working on a new REST Server that I develop with spring. The Server must have only server side logic, nothing js or views.
At the moment I started with Spring-boot version 1.2.7, but this only to give a server to the front-end developers until the setup for the server is done.
After I will change to the spring-core version 4.1 or something similar.
At the moment I have difficulty in configure the security part.
At the beginning I started with a Java Config configuration but after I changed to xml, because I already had a similar configuration.
My end result must be these:
hostname **/api/auth**** :is the entry point where a frontend developer make a **POST request with username and password of a customer. This call returns a token. This token permits me to identify the user the next time.
hostname /api/secure/resource/1 : is a resource that is protected, and can be accessible only with a valid token
hostname /api/other/1 : is an other type of resource that is not protected and can be accessible for everyone
hostname /api/secure/bambi/: is a resource that can be accessed from everyone but if it has a token then, more object-parameters are shown.
This seams for me a relative simple configuration, but I'm not able to configure it.
I know that this is not very his work, but for handle the token, and the resource access I would use OAUTH2 infrastructure (I'know, that this can be done better, but this was a requisite)
As follow I write you my configuration:
StartUpApplication.java
#SpringBootApplication(exclude = DispatcherServletAutoConfiguration.class)
#Import({ InMemoryDBConfigurationImpl.class})
#ImportResource({ "classpath:config/security-context.xml" })
public class SalustroApplication {
#Autowired
#Qualifier("InMemoryConfig")
private SystemConfiguration systemConfiguration;
public static void main(String[] args) {
SpringApplication app = new SpringApplication(SalustroApplication.class);
app.run(args);
}
#Bean
public ServletRegistrationBean foo() {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(FooConfig.class);
dispatcherServlet.setApplicationContext(applicationContext);
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(dispatcherServlet, "/");
servletRegistrationBean.setName("foo");
return servletRegistrationBean;
}
Do I need the foo method for the security part?
security-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:oauth="http://www.springframework.org/schema/security/oauth2"
xmlns:c="http://www.springframework.org/schema/c"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:sec="http://www.springframework.org/schema/security"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<sec:http pattern="/api/auth" create-session="stateless"
authentication-manager-ref="clientAuthenticationManager" use-expressions="true">
<sec:intercept-url pattern="/api/auth" access="IS_AUTHENTICATED_FULLY" />
<sec:anonymous enabled="false" />
<sec:custom-filter ref="clientCredentialsTokenEndpointFilter"
after="BASIC_AUTH_FILTER" />
<sec:access-denied-handler ref="oauthAccessDeniedHandler" />
<sec:http-basic entry-point-ref="clientAuthenticationEntryPoint" />
<sec:csrf disabled="true" />
</sec:http>
<sec:http pattern="/api/**" create-session="never"
entry-point-ref="oauthAuthenticationEntryPoint" use-expressions="true"
access-decision-manager-ref="accessDecisionManager">
<sec:intercept-url pattern="/api/**"
access="isFullyAuthenticated() AND hasRole('ROLE_USER')" />
<sec:anonymous enabled="false" />
<sec:custom-filter ref="resourceServerFilter"
before="PRE_AUTH_FILTER" />
<sec:access-denied-handler ref="oauthAccessDeniedHandler" />
<sec:csrf disabled="true" />
<sec:headers />
</sec:http>
<sec:authentication-manager id="clientAuthenticationManager">
<sec:authentication-provider
user-service-ref="clientDetailsUserService" />
</sec:authentication-manager>
<bean id="accessDecisionManager"
class="org.springframework.security.access.vote.UnanimousBased"
c:decisionVoters-ref="votersList" />
<bean id="clientAuthenticationEntryPoint" class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint"
p:realmName="p4me-test/client" p:typeName="Basic" />
<bean id="clientCredentialsTokenEndpointFilter"
class="org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter"
p:authenticationManager-ref="clientAuthenticationManager"
p:filterProcessesUrl="/api/auth" />
<bean id="clientDetailsService"
class="app.security.ClientDetailsServiceImpl" />
<bean id="clientDetailsUserService"
class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService"
c:clientDetailsService-ref="clientDetailsService" />
<bean id="clientDetailServiceImpl" class="app.security.ClientDetailsServiceImpl" />
<bean id="oauthAccessDeniedHandler"
class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler" />
<bean id="oauthAuthenticationEntryPoint"
class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint"
p:realmName="p4me-test">
</bean>
<oauth:resource-server id="resourceServerFilter"
resource-id="test" token-services-ref="tokenServices" />
<bean id="tokenEnhancer" class="app.security.CustomTokenEnhancer" />
<bean id="tokenServices" class="app.security.CustomTokenServices"
p:tokenStore-ref="tokenStore" p:clientDetailsService-ref="clientDetailsService"
p:supportRefreshToken="true" p:tokenEnhancer-ref="tokenEnhancer"
p:accessTokenValiditySeconds="1800" />
<bean id="tokenStore" class="org.springframework.security.oauth2.provider.token.store.InMemory TokenStore" />
<sec:authentication-manager alias="authenticationManager">
<sec:authentication-provider ref="userAuthenticationProvider" />
</sec:authentication-manager>
<bean id="userAuthenticationProvider"
class="app.config.impl.security.SecureAuthenticationProvider"/>
<oauth:authorization-server
client-details-service-ref="clientDetailsService"
token-services-ref="tokenServices" user-approval-handler-ref="userApprovalHandler"
token-endpoint-url="/api/auth">
<oauth:authorization-code />
<oauth:implicit />
<oauth:refresh-token />
<oauth:client-credentials />
<oauth:password />
</oauth:authorization-server>
<bean id="userApprovalHandler"
class="org.springframework.security.oauth2.provider.approval.TokenStoreUserApprovalHandler"
p:tokenStore-ref="tokenStore" p:requestFactory-ref="requestFactory" />
<bean id="requestFactory"
class="org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory"
c:clientDetailsService-ref="clientDetailServiceImpl" />
<util:list id="votersList">
<bean class="app.security.AccessVoter" />
<bean class="org.springframework.security.oauth2.provider.vote.ScopeVoter" />
<bean class="org.springframework.security.access.vote.RoleVoter" />
<bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
<bean class="org.springframework.security.web.access.expression.WebExpressionVoter">
<property name="expressionHandler">
<bean class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler" />
</property>
</bean>
</util:list>
The test class
#WebAppConfiguration
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = SalustroApplication.class)
public class AuthTest {
#Autowired
private WebApplicationContext context;
#Autowired
private Filter springSecurityFilterChain;
#Test
public void find1() throws Exception {
ResultActions doLogin = doLogin();
String contentAsString = doLogin.andReturn().getResponse().getContentAsString();
JSONObject json = new JSONObject(contentAsString);
DefaultMockMvcBuilder webAppContextSetup = MockMvcBuilders.webAppContextSetup(context)
.addFilter(springSecurityFilterChain);
MockMvc build = webAppContextSetup.build();
final ResultActions userResult = build.perform(post("/api/secure/user/1")
.param("access_token", json.getString("access_token")).accept(MediaType.APPLICATION_JSON))
.andDo(print());
assertEquals(someUser, userResult);
}
protected ResultActions doLogin() throws Exception {
DefaultMockMvcBuilder webAppContextSetup = MockMvcBuilders.webAppContextSetup(context)
.addFilter(springSecurityFilterChain);
MockMvc build = webAppContextSetup.build();
final ResultActions loginResult = build.perform(post("/api/auth").param("grant_type", "password")
.param("client_id", "testId").param("client_secret", "testSecret").param("username", "someUser")
.param("password", "somePassword").param("scope", "read").accept(MediaType.APPLICATION_JSON)).andDo(print());
return loginResult;
}
}
SecureAuthenticationPriver.class
#Component
public class SecureAuthenticationProvider implements AuthenticationProvider {
protected final static Logger logger = LoggerFactory.getLogger(SecureAuthenticationProvider.class);
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String name = authentication.getName();
String password = authentication.getCredentials().toString();
List<GrantedAuthority> grantedAuths = new ArrayList<>();
GrantedAuthority authorithy = new SimpleGrantedAuthority("USER");
grantedAuths.add(authorithy);
UserEntity authenticatedUser = userPersistence.findByUserName(name, password);
if (authenticatedUser != null) {
return new UsernamePasswordAuthenticationToken(name, password, grantedAuths);
} else
return null; }
#Override
public boolean supports(Class<?> authentication) {
return false;
}
}
AccessVoter.class
#Service
public class AccessVoter implements AccessDecisionVoter<Object> {
#Override
public boolean supports(final ConfigAttribute attribute) {
return true;
}
#Override
public boolean supports(final Class<?> clazz) {
return true;
}
#Override
#Transactional
public int vote(final Authentication authentication, final Object object,
final Collection<ConfigAttribute> attributes) {
final Object principal = authentication.getPrincipal();
return 1;
}
private int refreshUserDetails(final Principal principal) {
return 1;
}
}
ClientDetailServiceImpl.class
public class ClientDetailsServiceImpl implements ClientDetailsService {
#Override
public ClientDetails loadClientByClientId(final String clientId) {
if ("invalid".equals(clientId)) {
throw new ClientRegistrationException(clientId + " not found");
}
return createClientDetails(clientId);
}
private ClientDetails createClientDetails(final String clientId) {
final Set<GrantedAuthority> grantAuthorities = new HashSet<GrantedAuthority>();
grantAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));
final Set<String> authorizedGrantTypes = new HashSet<String>();
authorizedGrantTypes.add("password");
final BaseClientDetails details = new BaseClientDetails();
details.setClientId("testId");
details.setClientSecret("testSecret");
details.setAuthorizedGrantTypes(authorizedGrantTypes);
details.setAuthorities(grantAuthorities);
return details;
}
}
CustomTokenEnhancer.class
#Component
public class CustomTokenEnhancer implements TokenEnhancer {
private List<TokenEnhancer> delegates = Collections.emptyList();
#Autowired
private UserService userService;
public void setTokenEnhancers(final List<TokenEnhancer> delegates) {
this.delegates = delegates;
}
#Override
public OAuth2AccessToken enhance(final OAuth2AccessToken accessToken, final OAuth2Authentication authentication) {
final DefaultOAuth2AccessToken tempResult = (DefaultOAuth2AccessToken) accessToken;
// tempResult.setAdditionalInformation(getAuthenticationMethod(authentication));
OAuth2AccessToken result = tempResult;
for (final TokenEnhancer enhancer : delegates) {
result = enhancer.enhance(result, authentication);
}
return result;
}
private boolean isAdmin(final Collection<GrantedAuthority> authorities) {
for (final GrantedAuthority grantedAuthority : authorities) {
if (grantedAuthority.getAuthority().compareTo("ROLE_ADMIN") == 0) {
return true;
}
}
return false;
}
}
CustomTokenServices.class
public class CustomTokenServices extends DefaultTokenServices {
private TokenStore tokenStore;
private ClientDetailsService clientDetailsService;
private TokenEnhancer accessTokenEnhancer;
#Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(tokenStore, "tokenStore must be set");
}
#Override
public OAuth2AccessToken createAccessToken(final OAuth2Authentication authentication) {
final OAuth2AccessToken existingAccessToken = tokenStore
.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
if (existingAccessToken != null && existingAccessToken.isExpired()) {
if (existingAccessToken.getRefreshToken() != null) {
refreshToken = existingAccessToken.getRefreshToken();
tokenStore.removeRefreshToken(refreshToken);
}
tokenStore.removeAccessToken(existingAccessToken);
}
refreshToken = createRefreshToken(authentication);
final ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = createRefreshToken(authentication);
}
final OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
tokenStore.storeAccessToken(accessToken, authentication);
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
#Override
public OAuth2Authentication loadAuthentication(final String accessTokenValue) {
final DefaultOAuth2AccessToken accessToken = (DefaultOAuth2AccessToken) tokenStore
.readAccessToken(accessTokenValue);
if (accessToken == null) {
throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
} else if (accessToken.isExpired()) {
tokenStore.removeAccessToken(accessToken);
throw new InvalidTokenException("Access token expired: " + accessTokenValue);
}
final OAuth2Authentication result = tokenStore
.readAuthentication(accessToken);
if (clientDetailsService != null) {
final String clientId = result.getOAuth2Request().getClientId();
try {
clientDetailsService.loadClientByClientId(clientId);
} catch (final ClientRegistrationException e) {
throw new InvalidTokenException("Client not valid: " + clientId, e);
}
}
final int validitySeconds = getAccessTokenValiditySeconds(result
.getOAuth2Request());
accessToken
.setExpiration(new Date(System.currentTimeMillis() + validitySeconds * 1000L));
return result;
}
private ExpiringOAuth2RefreshToken createRefreshToken(final OAuth2Authentication authentication) {
if (!isSupportRefreshToken(authentication.getOAuth2Request())) {
return null;
}
final int validitySeconds = getRefreshTokenValiditySeconds(authentication
.getOAuth2Request());
final ExpiringOAuth2RefreshToken refreshToken = new DefaultExpiringOAuth2RefreshToken(UUID
.randomUUID().toString(), new Date(System.currentTimeMillis() + validitySeconds * 1000L));
return refreshToken;
}
private OAuth2AccessToken createAccessToken(final OAuth2Authentication authentication, final OAuth2RefreshToken refreshToken) {
final DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID
.randomUUID().toString());
final int validitySeconds = getAccessTokenValiditySeconds(authentication
.getOAuth2Request());
if (validitySeconds > 0) {
token.setExpiration(new Date(System.currentTimeMillis() + validitySeconds * 1000L));
}
token.setRefreshToken(refreshToken);
token.setScope(authentication.getOAuth2Request().getScope());
return accessTokenEnhancer != null ? accessTokenEnhancer
.enhance(token, authentication) : token;
}
#Override
public void setTokenEnhancer(final TokenEnhancer accessTokenEnhancer) {
this.accessTokenEnhancer = accessTokenEnhancer;
}
#Override
public void setTokenStore(final TokenStore tokenStore) {
this.tokenStore = tokenStore;
}
#Override
public void setClientDetailsService(final ClientDetailsService clientDetailsService) {
this.clientDetailsService = clientDetailsService;
}
}
How I said at the beginning: I started from an other working copy of configuration, and transformed to the needs of this application.
It is also possible that I confused a little bit some config.
I repeat the at the end I would utilize the OAUTH2 system to generate a token and utilize this token to authenticate the users. This authentication is made at with /api/auth (or /api/secure/auth?), the resources are available under /api/secure only for user with a valid token and other resources are available under /api/yyy and if they have a token more info is returned
When I run the test to try to make take a resource I receive this error:
Body = {"error":"access_denied","error_description":"Access is denied"}
Now I don't know exactely where I have to operate. In the security-context.xml or add some class to check the token.
The Exception
Body = {"error":"unauthorized","error_description":"There is no client authentication. Try adding an appropriate authentication filter."}
Is actually related to AuthTest test class where the WebApplicationContext doesn't contain the filter chain for the Spring Security. You need to make the following changes for your AuthTest test class.
#WebAppConfiguration
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = SalustroApplication.class)
public class AuthTest {
#Autowired
private WebApplicationContext context;
// Inject this
#Autowired
private Filter springSecurityFilterChain;
#Test
public void testLogin() throws Exception {
ResultActions doLogin = doLogin();
assertEquals(doLogin.andReturn().getResponse().getContentAsString(), "A valid Token");
}
protected ResultActions doLogin() throws Exception {
DefaultMockMvcBuilder webAppContextSetup = MockMvcBuilders.webAppContextSetup(context).addFilter(springSecurityFilterChain); // Add filter
MockMvc build = webAppContextSetup.build();
final ResultActions loginResult = build.perform(post("/api/auth").param("grant_type", "password")
.param("client_id", "testId").param("client_secret", "testSecret").param("username", "aUserName")
.param("password", "123456").accept(MediaType.APPLICATION_JSON)).andDo(print());
return loginResult;
}
}
I am using ReloadableResourceBundleMessageSource to store value list of my system. So i can use the i18N functionality
i am using multiple resources in the basenames of ReloadableResourceBundleMessageSource.
I want to pass all the localized labels of the web UI to the client (front-end) in order to be cached locally at the client.
Is there a way to load the entire keys of my resource bundles?
here is my ReloadableResourceBundleMessageSource bean definition:
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<list>
<value>classpath:resource1</value>
<value>classpath:resource2</value>
</list>
</property>
<property name="cacheSeconds" value="60"/>
<property name="defaultEncoding" value="UTF-8"/>
<property name="useCodeAsDefaultMessage" value="true"/>
</bean>
and this is my controller that pass all the keys:
#Component
#RequestMapping("/bundle")
public class ResourceBundleController {
#Autowired
private MessageSource messageSource;
#RequestMapping(value = "/locales.js")
public ModelAndView strings(HttpServletRequest request) {
// Call the string.jsp view
return new ModelAndView("/WEB-INF/includes/locales.jsp", "keys", ??? );// HERE IS MY PROBLEM. HOW SHOULD I GET ALL THE KEYS FROM MESSAGESOURCE
}
}
and here is my the resource bundle keys for the client:
<%#page contentType="text/javascript" pageEncoding="UTF-8"%>
<%#taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%#taglib prefix="spring" uri="http://www.springframework.org/tags"%>
var messages = {};
<c:forEach var="key" items="${keys}">
messages["<spring:message text='${key}' javaScriptEscape='true'/>"] = "<spring:message code='${key}' javaScriptEscape='true' />";
</c:forEach>
Any help will be appreciated.
Updated information about spring configuration regards cache of resource bundle
You could make something like this:
Your own implementation of ReloadableResourceBundleMessageSource:
public class ExposedResourceMessageBundleSource extends ReloadableResourceBundleMessageSource {
private static final Logger LOGGER = Logger.getLogger(ExposedResourceMessageBundleSource.class);
#Override
protected Properties loadProperties(Resource resource, String fileName) throws IOException {
LOGGER.info("Load " + fileName);
return super.loadProperties(resource, fileName);
}
/**
* Gets all messages for presented Locale.
* #param locale user request's locale
* #return all messages
*/
public Properties getMessages(Locale locale){
return getMergedProperties(locale).getProperties();
}
}
Service definition to handle reasource operations:
public interface MessageResolveService extends MessageSourceAware{
String getMessage(String key, Object[] argumentsForKey, Locale locale);
Map<String,String> getMessages(Locale locale);
}
And implementation:
#Service
public class MessageResolverServiceImpl implements MessageResolveService{
private static final Logger LOGGER = Logger.getLogger(MessageResolverServiceImpl.class);
private MessageSource messageSource;
#Override
public void setMessageSource(MessageSource messageSource) {
LOGGER.info("Messages i18n injected");
this.messageSource = messageSource;
}
public String getMessage(String key, Object[] arguments, Locale locale){
String message = "";
try{
message = messageSource.getMessage(key,arguments,locale);
} catch(NoSuchMessageException e){
message = key;
LOGGER.warn("No message found: "+key);
}
return message;
}
public Map<String,String> getMessages(Locale locale){
Properties properties = ((XmlWebApplicationContext)messageSource).getBean("messageSource",
ExposedResourceMessageBundleSource.class).getMessages(locale);
Map<String,String> messagesMap = new HashMap<String,String>();
for(Map.Entry<Object,Object> entry: properties.entrySet()){
if(entry.getKey() != null && entry.getValue() != null) {
messagesMap.put(entry.getKey().toString(), entry.getValue().toString());
}
}
return messagesMap;
}
}
Spring configuration:
<bean id="messageSource" class="your.package.ExposedResourceMessageBundleSource">
<property name="basenames">
<list>
<value>classpath:yourFileName</value>
<value>classpath:yourNextFileName</value>
</list>
</property>
<property name="defaultEncoding" value="UTF-8"/>
<property name="cacheSeconds" value="10"/> //If you want reload message every couple seconds. In this case every 10 seconds.
</bean>
And your #Controller(similar to this):
#Component
#RequestMapping("/bundle")
public class ResourceBundleController {
#Autowired
private MessageResolveService messageResolveService;
#RequestMapping(value = "/locales.js")
public ModelAndView strings(HttpServletRequest request) {
// Call the string.jsp view
return new ModelAndView("/WEB-INF/includes/locales.jsp", "keys", messageResolverService.getMessages(LocaleContextHolder.getLocale()));
}