0

I'm developing a web application using Spring-MVC (4.2.5) and Spring-Security (4.0.3). My issue is that I need a method which runs at set time intervals, such as every 30 seconds, but before it's executed I have to make sure that the user is logged in.

When the user submit login form, an HTTP request is send to an external server and, if the credentials are correct, an HTTP response containing a session token is returned.

The session token is fundamental for all other request, including the ones I want to forward within the timed method.

In my controller I can obtain the session token from the Spring Security Context, in every moment:

String token = SecurityContextHolder.getContext().getAuthentication().getDetails().toString();

I tried to use a scheduled task but unfortunately that doesn't work, because this is started at application launch, when there is no session and runs in a thread separated from the servlet container. The following exception is thrown:

GRAVE: Unexpected error occurred in scheduled task.
java.lang.NullPointerException
    at service.RetrieveBettingOdds.retrieveMatchByNationality(RetrieveBettingOdds.java:54)
    at tasks.OddsTask.run(OddsTask.java:36)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:65)
    at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
    at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308)
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180)
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

because SecurityContextHolder.getContext() is null.

OddsTask

@Component
public class OddsTask {
    @Autowired
    private BettingService bettingService;
    @Autowired
    private MatchDao matchDao;
    private List<Match> betfairMatches;
    private List<Match> databaseMatches;

    public void run() {
        betfairMatches = new ArrayList<>();
        databaseMatches = new ArrayList<>();
        databaseMatches = matchDao.retrieveAllMatches();

        for(Country country : Country.values()) {
            betfairMatches = bettingService.retrieveMatchByNationality(country.name(), getCompetitionId(country.name()));
        }
    //cut code
}

RetrieveBettingOdds

@Service
public class RetrieveBettingOdds implements BettingService {
    private static final String BETTING_END_POINT = "https://api.betfair.com/exchange/betting/json-rpc/v1";
    private static final String APP_KEY = "my_app_key";
    private String token;

    @Override
    public List<Match> retrieveMatchByNationality(String country, String competitionId) {
        token = SecurityContextHolder.getContext().getAuthentication().getDetails().toString();
    /**
    * Cut the following code, in which I create an HTTP request including the token
    **/
}

spring-mvc.xml

<bean id="oddsTask" class="tasks.OddsTask">
</bean>

<task:scheduler id="myScheduler" pool-size="10" />

<task:scheduled-tasks scheduler="myScheduler">
    <task:scheduled ref="oddsTask" method="run"
        fixed-delay="10000" initial-delay="60000" />
</task:scheduled-tasks>

What can I use instead? How can I fix it?

andy
  • 269
  • 1
  • 12
  • 22
  • What about add a bean with scope session and that bean will run the scheduled task? – schrieveslaach Nov 01 '16 at 14:40
  • How about you define an [HttpSessionListener](https://tomcat.apache.org/tomcat-5.5-doc/servletapi/javax/servlet/http/HttpSessionListener.html) and every time a session is created you put it in a shared, global data structure (e.g. static list or spring singleton bean), every time a session dies, you remove it. When the scheduled task runs you run it for all those in that structure. – Edwin Dalorzo Nov 01 '16 at 14:45
  • you say "make sure that the user is logged in". are you only expecting one user to be logged in at once? If not, as Edwin says, you're better off writing a background job that runs for ALL logged in users - then there's no problem. You can either manage who is logged in yourself, or scan a db table, or use spring's SessionRegistry (http://stackoverflow.com/questions/11271449/how-can-i-have-list-of-all-users-logged-in-via-spring-security-my-web-applicat) – David Lavender Nov 01 '16 at 14:49
  • @Schrieveslaach currently I'm running the scheduled task from the configuration file. Edited my question with some code and the xml – andy Nov 01 '16 at 16:44
  • @MrSpoon for now I'm expecting only a user to be logged in – andy Nov 01 '16 at 16:46

1 Answers1

0

I have an idea that unfortunately I cannot test at this point, but I hope this somehow helps, or at least give you other ideas on how this could be done.

How about you define an Http event listener that updates a data structure with active session?.

public class ActiveUsers implements HttpSessionListener {

    private ActiveUsersDao active = new ActiveUsersDao<>();

    @Override
    public void sessionCreated(HttpSessionEvent event) {
        active.add(event.getSession().getId());

    }

    @Override
    public void sessionDestroyed(HttpSessionEvent event) {
        active.remove(event.getSession().getId());
    }        
}

Then in your web.xml you register the listener

<listener>
   <listener-class>com.stackoverflow.ActiveUsers</listener-class>
</listener>

Finally, in your scheduled bean you can get data from your DAO.

@Service
public class TaskScheduler

   @Auotwired ActiveUsersDao activeUsers;

   @Scheduled
   public void run() {
      Set<String> snapshot = activeUsers.findAll();
      for(String user: snapshot) {
         //do what you've got to do
      }
   }
Edwin Dalorzo
  • 76,803
  • 25
  • 144
  • 205
  • First of all thanks for the answer. I wondered that was a good way to sort it out, but I still have some problems: 1) ActiveUsersDao is an object representing a List? Or a Dao class + interface? 2) What is event.getSessionId()? 3) sessionCreated and sessionDestroyed will intercept the session created/destroyed by Spring Security, is that right? But how can I force the scheduled method to run only after the login event is occurred? – andy Nov 01 '16 at 17:53
  • I forgot to specify that only one user can use the web app, but there will not be this limit in the future – andy Nov 01 '16 at 17:56