Skip to content

Commit

Permalink
Added persisted session storage support [#538]
Browse files Browse the repository at this point in the history
 * Added a data model to be persisted.
 * Added a REST API for retrieving updates.

This is a foundation only and does not actually decorate the
session object. That will be done with new feature code that
will leverage this feature set.
  • Loading branch information
mcpierce committed Mar 1, 2021
1 parent 0782636 commit 229be6e
Show file tree
Hide file tree
Showing 11 changed files with 420 additions and 0 deletions.
4 changes: 4 additions & 0 deletions comixed-app/src/main/resources/application.properties
Expand Up @@ -22,6 +22,10 @@ spring.jpa.generate-ddl=false
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

# Session storage
spring.session.store-type=jdbc
spring.session.jdbc.initialize-schema=always

# Liquibase changelog
spring.liquibase.change-log=classpath:db/liquibase-changelog.xml

Expand Down
@@ -0,0 +1,8 @@
package org.comixedproject.model.session;

/**
* <code>SessionUpdate</code> holds the content of a session update returned to the browser.
*
* @author Darryl L. Pierce
*/
public class SessionUpdate {}
@@ -0,0 +1,26 @@
/*
* ComiXed - A digital comic book library management application.
* Copyright (C) 2020, The ComiXed Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses>
*/

package org.comixedproject.model.session;

/**
* <code>UserSession</code> represents the server-side details for a user's session.
*
* @author Darryl L. Pierce
*/
public class UserSession {}
Expand Up @@ -71,4 +71,7 @@ public interface AuditLogEntryList extends ApiResponse {}

/** Used when viewing a list of comic files. */
public interface ComicFileList extends ApiResponse {}

/** Used when retrieving a session update. */
public interface SessionUpdateView extends ApiResponse {}
}
4 changes: 4 additions & 0 deletions comixed-rest-api/pom.xml
Expand Up @@ -47,5 +47,9 @@
<artifactId>MarvinPlugins</artifactId>
<version>1.5.5</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-jdbc</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,81 @@
/*
* ComiXed - A digital comic book library management application.
* Copyright (C) 2020, The ComiXed Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses>
*/

package org.comixedproject.controller.user;

import com.fasterxml.jackson.annotation.JsonView;
import javax.servlet.http.HttpSession;
import lombok.extern.log4j.Log4j2;
import org.comixedproject.model.net.ApiResponse;
import org.comixedproject.model.net.session.SessionUpdateRequest;
import org.comixedproject.model.net.session.SessionUpdateResponse;
import org.comixedproject.model.session.SessionUpdate;
import org.comixedproject.model.session.UserSession;
import org.comixedproject.service.user.SessionService;
import org.comixedproject.views.View;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Log4j2
public class SessionController {
public static final String SESSION_ENTRY_KEY = "session-update";

@Autowired private SessionService sessionService;

/**
* Retrieves a session update. Waits a given period of time before returning an empty update.
*
* @param request the request body
* @return the session update
*/
@PostMapping(
value = "/api/session/updates",
produces = MediaType.APPLICATION_JSON_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE)
@JsonView(View.SessionUpdateView.class)
public ApiResponse<SessionUpdateResponse> getSessionUpdate(
final HttpSession httpSession, @RequestBody() final SessionUpdateRequest request) {
UserSession userSession = null;

if (request.getReset()) {
log.info("Resetting the user server-side session");
userSession = new UserSession();
} else {
log.info("Retrieving the user server-side session");
userSession = (UserSession) httpSession.getAttribute(SESSION_ENTRY_KEY);
if (userSession == null) {
log.info("Creating a new server-side session");
userSession = new UserSession();
}
}

final Long timeout = request.getTimeout();
log.info("Getting session update: timeout={}", timeout);
final SessionUpdate sessionUpdate = this.sessionService.getSessionUpdate(userSession, timeout);

this.log.info("Saving server-side session");
httpSession.setAttribute(SESSION_ENTRY_KEY, userSession);

log.info("Returning session update");
return new ApiResponse<>(new SessionUpdateResponse(sessionUpdate));
}
}
@@ -0,0 +1,42 @@
/*
* ComiXed - A digital comic book library management application.
* Copyright (C) 2020, The ComiXed Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses>
*/

package org.comixedproject.model.net.session;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
* <code>SessionUpdateRequest</code> contains the body for a session update request.
*
* @author Darryl L. Pierce
*/
@NoArgsConstructor
public class SessionUpdateRequest {
@JsonProperty("reset")
@Getter
@Setter
private Boolean reset;

@JsonProperty("timeout")
@Getter
@Setter
private Long timeout;
}
@@ -0,0 +1,42 @@
/*
* ComiXed - A digital comic book library management application.
* Copyright (C) 2020, The ComiXed Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses>
*/

package org.comixedproject.model.net.session;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonView;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.comixedproject.model.session.SessionUpdate;
import org.comixedproject.views.View;

/**
* <code>SessionUpdateResponse</code> contains the set of updates to be applied to the browser-side
* session.
*
* @author Darryl L. Pierce
*/
@AllArgsConstructor
public class SessionUpdateResponse {
@JsonProperty("update")
@Getter
@Setter
@JsonView(View.SessionUpdateView.class)
private SessionUpdate update;
}
@@ -0,0 +1,114 @@
/*
* ComiXed - A digital comic book library management application.
* Copyright (C) 2020, The ComiXed Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses>
*/

package org.comixedproject.controller.user;

import static junit.framework.TestCase.*;
import static org.comixedproject.controller.user.SessionController.SESSION_ENTRY_KEY;

import javax.servlet.http.HttpSession;
import org.comixedproject.model.net.ApiResponse;
import org.comixedproject.model.net.session.SessionUpdateRequest;
import org.comixedproject.model.net.session.SessionUpdateResponse;
import org.comixedproject.model.session.SessionUpdate;
import org.comixedproject.model.session.UserSession;
import org.comixedproject.service.user.SessionService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class SessionControllerTest {
private static final Long TEST_TIMEOUT = 717L;

@InjectMocks private SessionController sessionController;
@Mock private SessionService sessionService;
@Mock private HttpSession httpSession;
@Mock private UserSession userSession;
@Captor private ArgumentCaptor<UserSession> userSessionArgumentCaptor;
@Mock private SessionUpdateRequest sessionUpdateRequest;
@Mock private SessionUpdate sessionUpdate;

@Test
public void testGetSessionUpdateNoExistingSession() {
Mockito.when(sessionUpdateRequest.getReset()).thenReturn(Boolean.FALSE);
Mockito.when(httpSession.getAttribute(Mockito.anyString())).thenReturn(null);
Mockito.when(sessionUpdateRequest.getTimeout()).thenReturn(TEST_TIMEOUT);
Mockito.when(
sessionService.getSessionUpdate(userSessionArgumentCaptor.capture(), Mockito.anyLong()))
.thenReturn(sessionUpdate);

final ApiResponse<SessionUpdateResponse> response =
sessionController.getSessionUpdate(httpSession, sessionUpdateRequest);

assertNotNull(response);
assertTrue(response.isSuccess());
assertNotNull(response.getResult());
assertSame(sessionUpdate, response.getResult().getUpdate());
assertNotNull(userSessionArgumentCaptor.getValue());

Mockito.verify(sessionService, Mockito.times(1))
.getSessionUpdate(userSessionArgumentCaptor.getValue(), TEST_TIMEOUT);
Mockito.verify(httpSession, Mockito.times(1))
.setAttribute(SESSION_ENTRY_KEY, userSessionArgumentCaptor.getValue());
}

@Test
public void testGetSessionUpdateBrowserInitiateReset() {
Mockito.when(sessionUpdateRequest.getReset()).thenReturn(Boolean.TRUE);
Mockito.when(sessionUpdateRequest.getTimeout()).thenReturn(TEST_TIMEOUT);
Mockito.when(
sessionService.getSessionUpdate(userSessionArgumentCaptor.capture(), Mockito.anyLong()))
.thenReturn(sessionUpdate);

final ApiResponse<SessionUpdateResponse> response =
sessionController.getSessionUpdate(httpSession, sessionUpdateRequest);

assertNotNull(response);
assertTrue(response.isSuccess());
assertNotNull(response.getResult());
assertSame(sessionUpdate, response.getResult().getUpdate());
assertNotNull(userSessionArgumentCaptor.getValue());

Mockito.verify(sessionService, Mockito.times(1))
.getSessionUpdate(userSessionArgumentCaptor.getValue(), TEST_TIMEOUT);
Mockito.verify(httpSession, Mockito.times(1))
.setAttribute(SESSION_ENTRY_KEY, userSessionArgumentCaptor.getValue());
}

@Test
public void testGetSessionUpdate() {
Mockito.when(sessionUpdateRequest.getReset()).thenReturn(Boolean.FALSE);
Mockito.when(httpSession.getAttribute(Mockito.anyString())).thenReturn(userSession);
Mockito.when(sessionUpdateRequest.getTimeout()).thenReturn(TEST_TIMEOUT);
Mockito.when(sessionService.getSessionUpdate(Mockito.any(UserSession.class), Mockito.anyLong()))
.thenReturn(sessionUpdate);

final ApiResponse<SessionUpdateResponse> response =
sessionController.getSessionUpdate(httpSession, sessionUpdateRequest);

assertNotNull(response);
assertTrue(response.isSuccess());
assertNotNull(response.getResult());
assertSame(sessionUpdate, response.getResult().getUpdate());

Mockito.verify(sessionService, Mockito.times(1)).getSessionUpdate(userSession, TEST_TIMEOUT);
Mockito.verify(httpSession, Mockito.times(1)).setAttribute(SESSION_ENTRY_KEY, userSession);
}
}

0 comments on commit 229be6e

Please sign in to comment.