Skip to content

Commit

Permalink
Merge pull request #81 from VEuPathDB/temporary-data
Browse files Browse the repository at this point in the history
Convert session to temporary user data store
  • Loading branch information
ryanrdoherty authored Feb 24, 2024
2 parents 54d7c3c + 6c7777a commit 72c89a8
Show file tree
Hide file tree
Showing 11 changed files with 128 additions and 45 deletions.
5 changes: 5 additions & 0 deletions Model/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@
<artifactId>oauth2-client</artifactId>
</dependency>

<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>

<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package org.gusdb.wdk.cache;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalListener;
import com.github.benmanes.caffeine.cache.stats.CacheStats;

/**
* Manages a map of user-scoped short-term information. Traditionally,
* this data was stored in the user's session object; instead, we store
* it now in a userId-keyed map, whose values time out some duration
* after last access (currently 60 minutes). If instance() is called
* within the application, shutDown() should also be called to clean
* up the expiration thread threadpool.
*/
public class TemporaryUserDataStore {

private static final Logger LOG = Logger.getLogger(TemporaryUserDataStore.class);

public static class TemporaryUserData extends ConcurrentHashMap<String,Object> {

private final TemporaryUserDataStore _parent;
private final Long _owner;

private TemporaryUserData(TemporaryUserDataStore parent, Long owner) {
_parent = parent;
_owner = owner;
}

public void invalidate() {
clear();
_parent.remove(_owner);
}

}

// singleton pattern
private static TemporaryUserDataStore _instance;

public static synchronized TemporaryUserDataStore instance() {
return _instance == null ? (_instance = new TemporaryUserDataStore()) : _instance;
}

public static void shutDown() {
if (_instance != null)
_instance._threadPool.shutdown();
_instance = null;
}

private static final RemovalListener<Long,Map<String,Object>> LISTENER =
(k,v,cause) -> LOG.info("User " + k + "'s temporary user data store has expired with " + v.size() + " entries; Reason: " + cause);

private final ExecutorService _threadPool;
private final Cache<Long,TemporaryUserData> _data;

private TemporaryUserDataStore() {
_threadPool = Executors.newCachedThreadPool();
_data = Caffeine.newBuilder()
.executor(_threadPool)
.recordStats()
.removalListener(LISTENER)
.expireAfterAccess(60, TimeUnit.MINUTES)
.build();
}

public TemporaryUserData get(Long userId) {
return _data.get(userId, id -> new TemporaryUserData(this, id));
}

public void remove(Long userId) {
_data.invalidate(userId);
}

public CacheStats getStats() {
return _data.stats();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.gusdb.fgputil.logging.ThreadLocalLoggingVars;
import org.gusdb.fgputil.runtime.GusHome;
import org.gusdb.fgputil.web.ApplicationContext;
import org.gusdb.wdk.cache.TemporaryUserDataStore;
import org.gusdb.wdk.model.ThreadMonitor;
import org.gusdb.wdk.model.Utilities;
import org.gusdb.wdk.model.WdkModel;
Expand Down Expand Up @@ -77,6 +78,9 @@ public static void terminateWdk(ApplicationContext applicationScope) {
// shut down thread monitor
ThreadMonitor.shutDown();

// shut down TemporaryUserData threadpool
TemporaryUserDataStore.shutDown();

WdkModel wdkModel = getWdkModel(applicationScope);
if (wdkModel != null) {
// insulate in case model never properly loaded
Expand Down
4 changes: 0 additions & 4 deletions Model/src/main/java/org/gusdb/wdk/errors/ErrorContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,16 @@ public static enum ErrorLocation {
private final WdkModel _wdkModel;
private final RequestSnapshot _requestData;
private final ReadOnlyMap<String, Object> _requestAttributeMap;
private final ReadOnlyMap<String, Object> _sessionAttributeMap;
private final ErrorLocation _errorLocation;
private final ThreadContextBundle _mdcBundle;
private final String _logMarker;
private final Date _date;

public ErrorContext(WdkModel wdkModel, RequestSnapshot requestData,
ReadOnlyMap<String, Object> sessionAttributeMap,
ErrorLocation errorLocation) {
_wdkModel = wdkModel;
_requestData = requestData;
_requestAttributeMap = requestData.getAttributes();
_sessionAttributeMap = sessionAttributeMap;
_errorLocation = errorLocation;
_mdcBundle = ThreadLocalLoggingVars.getThreadContextBundle();
_logMarker = UUID.randomUUID().toString();
Expand All @@ -46,7 +43,6 @@ public ErrorContext(WdkModel wdkModel, RequestSnapshot requestData,
public WdkModel getWdkModel() { return _wdkModel; }
public RequestSnapshot getRequestData() { return _requestData; }
public ReadOnlyMap<String, Object> getRequestAttributeMap() { return _requestAttributeMap; }
public ReadOnlyMap<String, Object> getSessionAttributeMap() { return _sessionAttributeMap; }

/**
* A site is considered monitored if the administrator email from adminEmail in the model-config.xml has content.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ public void filter(ContainerRequestContext requestContext) throws IOException {
ThreadLocalLoggingVars.setIpAddress(request.getRemoteIpAddress());
ThreadLocalLoggingVars.setRequestedDomain(request.getServerName());
ThreadLocalLoggingVars.setRequestId(String.valueOf(requestId.getAndIncrement()));
ThreadLocalLoggingVars.setSessionId(request.getSession().getId());

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@
import org.gusdb.fgputil.json.JsonType.ValueType;
import org.gusdb.fgputil.validation.ValidObjectFactory.RunnableObj;
import org.gusdb.fgputil.validation.ValidationLevel;
import org.gusdb.fgputil.web.SessionProxy;
import org.gusdb.wdk.core.api.JsonKeys;
import org.gusdb.wdk.model.WdkModel;
import org.gusdb.wdk.model.WdkModelException;
import org.gusdb.wdk.model.WdkUserException;
import org.gusdb.wdk.model.answer.AnswerValue;
import org.gusdb.wdk.model.answer.factory.AnswerValueFactory;
import org.gusdb.wdk.cache.TemporaryUserDataStore.TemporaryUserData;
import org.gusdb.wdk.model.dataset.Dataset;
import org.gusdb.wdk.model.dataset.DatasetContents;
import org.gusdb.wdk.model.dataset.DatasetFactory;
Expand Down Expand Up @@ -63,14 +63,14 @@ public static Dataset createFromRequest(
DatasetRequest request,
User user,
DatasetFactory factory,
SessionProxy session
TemporaryUserData tmpUserData
) throws WdkModelException, DataValidationException, RequestMisformatException {
JsonType value = request.getConfigValue();
switch(request.getSourceType()) {
case ID_LIST: return createFromIdList(value.getJSONArray(), user, factory);
case BASKET: return createFromBasket(value.getString(), user, factory);
case STRATEGY: return createFromStrategy(getStrategyId(value), user, factory);
case FILE: return createFromTemporaryFile(value.getString(), user, factory, request.getAdditionalConfig(), session);
case FILE: return createFromTemporaryFile(value.getString(), user, factory, request.getAdditionalConfig(), tmpUserData);
case URL: return createFromUrl(value.getString(), user, factory, request.getAdditionalConfig());
default:
throw new DataValidationException("Unrecognized " + JsonKeys.SOURCE_TYPE + ": " + request.getSourceType());
Expand Down Expand Up @@ -235,13 +235,13 @@ private static Dataset createFromTemporaryFile(
final User user,
final DatasetFactory factory,
final Map<String, JsonType> additionalConfig,
final SessionProxy session
final TemporaryUserData tmpUserData
) throws DataValidationException, WdkModelException, RequestMisformatException {

var model = factory.getWdkModel();
var parser = getDatasetParser(model, additionalConfig);

var tempFilePath = TemporaryFileService.getTempFileFactory(factory.getWdkModel(), session)
var tempFilePath = TemporaryFileService.getTempFileFactory(factory.getWdkModel(), tmpUserData)
.apply(tempFileId)
.orElseThrow(() -> new DataValidationException(
"Temporary file with the ID '" + tempFileId + "' could not be found in this session."));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@
import org.gusdb.fgputil.IoUtil;
import org.gusdb.fgputil.events.Events;
import org.gusdb.fgputil.web.RequestData;
import org.gusdb.fgputil.web.SessionProxy;
import org.gusdb.oauth2.client.ValidatedToken;
import org.gusdb.wdk.cache.TemporaryUserDataStore;
import org.gusdb.wdk.cache.TemporaryUserDataStore.TemporaryUserData;
import org.gusdb.wdk.controller.ContextLookup;
import org.gusdb.wdk.errors.ErrorContext;
import org.gusdb.wdk.errors.ErrorContext.ErrorLocation;
Expand Down Expand Up @@ -143,8 +144,8 @@ protected Map<String, List<String>> getHeaders() {
*
* @return genericized session object (compatible with both Servlet and Grizzly sessions)
*/
protected SessionProxy getSession() {
return getRequest().getSession();
protected TemporaryUserData getTemporaryUserData() {
return TemporaryUserDataStore.instance().get(getRequestingUser().getUserId());
}

protected User getRequestingUser() {
Expand Down Expand Up @@ -216,7 +217,6 @@ public static ErrorContext getErrorContext(RequestData request, WdkModel wdkMode
return new ErrorContext(
wdkModel,
request.getSnapshot(),
request.getSession().getAttributeMap(),
errorLocation);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,11 @@
import org.gusdb.fgputil.events.Events;
import org.gusdb.fgputil.web.CookieBuilder;
import org.gusdb.fgputil.web.LoginCookieFactory;
import org.gusdb.fgputil.web.SessionProxy;
import org.gusdb.oauth2.client.ValidatedToken;
import org.gusdb.oauth2.exception.InvalidTokenException;
import org.gusdb.wdk.cache.TemporaryUserDataStore.TemporaryUserData;
import org.gusdb.wdk.core.api.JsonKeys;
import org.gusdb.wdk.events.NewUserEvent;
import org.gusdb.wdk.model.Utilities;
import org.gusdb.wdk.model.WdkModel;
import org.gusdb.wdk.model.WdkModelException;
import org.gusdb.wdk.model.WdkRuntimeException;
Expand Down Expand Up @@ -85,7 +84,7 @@ public class SessionService extends AbstractWdkService {
@Produces(MediaType.APPLICATION_JSON)
public Response getOauthStateToken() throws WdkModelException {
String newToken = generateStateToken(getWdkModel());
getSession().setAttribute(STATE_TOKEN_KEY, newToken);
getTemporaryUserData().put(STATE_TOKEN_KEY, newToken);
JSONObject json = new JSONObject();
json.put(JsonKeys.OAUTH_STATE_TOKEN, newToken);
return Response.ok(json.toString()).build();
Expand Down Expand Up @@ -145,8 +144,8 @@ public Response processOauthLogin(
}

// get state token off session and remove; only needed for this request
String storedStateToken = (String) getSession().getAttribute(STATE_TOKEN_KEY);
getSession().removeAttribute(STATE_TOKEN_KEY);
String storedStateToken = (String) getTemporaryUserData().get(STATE_TOKEN_KEY);
getTemporaryUserData().remove(STATE_TOKEN_KEY);

// Is the state token present and does it match the session state token?
if (stateToken == null || !stateToken.equals(storedStateToken)) {
Expand Down Expand Up @@ -239,12 +238,10 @@ protected void transferOwnership(User oldUser, User newUser, WdkModel wdkModel)
*/
private Response getSuccessResponse(ValidatedToken bearerToken, User newUser, User oldUser,
String redirectUrl, boolean isRedirectResponse) throws WdkModelException {
SessionProxy session = getSession();

// synchronize on the underlying session object (SessionProxy is request-local)
synchronized(session.getUnderlyingSession()) {
TemporaryUserData tmpData = getTemporaryUserData();

session.setAttribute(Utilities.WDK_USER_KEY, newUser);
synchronized(tmpData) {

Events.triggerAndWait(new NewUserEvent(newUser, oldUser),
new WdkRuntimeException("Unable to complete WDK user assignement."));
Expand Down Expand Up @@ -284,11 +281,10 @@ public Response processLogout() throws WdkModelException {

// get the current session's user, then invalidate the session
User oldUser = getRequestingUser();
getSession().invalidate(); // legacy
getTemporaryUserData().invalidate(); // legacy

// get a new session and add new guest user to it
TwoTuple<ValidatedToken, User> newUser = getWdkModel().getUserFactory().createUnregisteredUser();
getSession().setAttribute(Utilities.WDK_USER_KEY, newUser.getSecond());

// throw new user event
Events.triggerAndWait(new NewUserEvent(newUser.getSecond(), oldUser),
Expand All @@ -302,6 +298,7 @@ public Response processLogout() throws WdkModelException {
extraCookie.setMaxAge(-1);
logoutCookies.add(extraCookie);
}

ResponseBuilder builder = createRedirectResponse(getContextUri());
for (CookieBuilder logoutCookie : logoutCookies) {
builder.cookie(logoutCookie.toJaxRsCookie());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.gusdb.fgputil.IoUtil;
import org.gusdb.fgputil.web.SessionProxy;
import org.gusdb.wdk.cache.TemporaryUserDataStore.TemporaryUserData;
import org.gusdb.wdk.model.WdkModel;
import org.gusdb.wdk.model.WdkModelException;
import org.gusdb.wdk.model.WdkRuntimeException;
Expand Down Expand Up @@ -109,14 +109,14 @@ public static java.nio.file.Path writeTemporaryFile(WdkModel wdkModel, InputStre
}

private void addTempFileToSession(TemporaryFileMetadata tempFileMetadata) {
SessionProxy session = getSession();
TemporaryUserData tmpData = getTemporaryUserData();
@SuppressWarnings("unchecked")
Map<String, TemporaryFileMetadata> tempFilesInSession = (Map<String, TemporaryFileMetadata>)(session.getAttribute(TEMP_FILE_METADATA));
Map<String, TemporaryFileMetadata> tempFilesInSession = (Map<String, TemporaryFileMetadata>)(tmpData.get(TEMP_FILE_METADATA));
if (tempFilesInSession == null) {
tempFilesInSession = Collections.synchronizedMap(new HashMap<>());
}
tempFilesInSession.put(tempFileMetadata.getTempName(), tempFileMetadata);
session.setAttribute(TEMP_FILE_METADATA, tempFilesInSession);
tmpData.put(TEMP_FILE_METADATA, tempFilesInSession);
}

/**
Expand All @@ -133,12 +133,12 @@ public Response deleteTempFile(@PathParam("id") String tempFileName) throws WdkM
java.nio.file.Path path = optPath.orElseThrow(() -> new NotFoundException(
"Temporary file with ID " + tempFileName + " is not found in this user's session"));

SessionProxy session = getSession();
TemporaryUserData tmpData = getTemporaryUserData();
@SuppressWarnings("unchecked")
Map<String, TemporaryFileMetadata> tempFilesInSession = (Map<String, TemporaryFileMetadata>)(session.getAttribute(TEMP_FILE_METADATA));
Map<String, TemporaryFileMetadata> tempFilesInSession = (Map<String, TemporaryFileMetadata>)(tmpData.get(TEMP_FILE_METADATA));
if (tempFilesInSession != null) {
tempFilesInSession.remove(tempFileName);
session.setAttribute(TEMP_FILE_METADATA, tempFilesInSession);
tmpData.put(TEMP_FILE_METADATA, tempFilesInSession);
}

try {
Expand All @@ -157,12 +157,12 @@ public Response deleteTempFile(@PathParam("id") String tempFileName) throws WdkM
*
* @param wdkModel
* the WDK model
* @param session
* the current user session
* @param userTmpData
* user's temporary data
*
* @return a factory function for looking up temporary file paths by name
*/
public static Function<String, Optional<java.nio.file.Path>> getTempFileFactory(WdkModel wdkModel, SessionProxy session) {
public static Function<String, Optional<java.nio.file.Path>> getTempFileFactory(WdkModel wdkModel, TemporaryUserData userTmpData) {
java.nio.file.Path tempDirPath = wdkModel.getModelConfig().getWdkTempDir();

return tempFileName -> {
Expand All @@ -172,7 +172,7 @@ public static Function<String, Optional<java.nio.file.Path>> getTempFileFactory(
throw new WdkRuntimeException("WDK Temp file " + tempFilePath + " exists but is not readable");
}
@SuppressWarnings("unchecked")
Map<String, TemporaryFileMetadata> tempFilesInSession = (Map<String, TemporaryFileMetadata>)(session.getAttribute(TEMP_FILE_METADATA));
Map<String, TemporaryFileMetadata> tempFilesInSession = (Map<String, TemporaryFileMetadata>)(userTmpData.get(TEMP_FILE_METADATA));
if (tempFilesInSession != null && tempFilesInSession.containsKey(tempFileName)) {
return Optional.of(tempFilePath);
}
Expand All @@ -187,17 +187,17 @@ public static Function<String, Optional<java.nio.file.Path>> getTempFileFactory(
* session, or does not exist, return empty optional.
*/
private Optional<java.nio.file.Path> getTempFileFromSession(String tempFileName) {
return getTempFileFactory(getWdkModel(), getSession()).apply(tempFileName);
return getTempFileFactory(getWdkModel(), getTemporaryUserData()).apply(tempFileName);
}

/**
* Returns the metadata for a temp file that is known by the user's session
* (having been put there by the temp file service). If the metadata is not
* found in the session, return empty optional.
*/
public static Optional<TemporaryFileMetadata> getTempFileMetadata(String tempFileName, SessionProxy session) {
public static Optional<TemporaryFileMetadata> getTempFileMetadata(String tempFileName, TemporaryUserData userTmpData) {
@SuppressWarnings("unchecked")
Map<String, TemporaryFileMetadata> tempFilesInSession = (Map<String, TemporaryFileMetadata>)(session.getAttribute(TEMP_FILE_METADATA));
Map<String, TemporaryFileMetadata> tempFilesInSession = (Map<String, TemporaryFileMetadata>)(userTmpData.get(TEMP_FILE_METADATA));

return Optional.ofNullable(
tempFilesInSession.get(tempFileName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public Response addDatasetFromJson(JSONObject input)
var user = getUserBundle(Access.PRIVATE).getSessionUser();
var factory = getWdkModel().getDatasetFactory();
var request = new DatasetRequest(input);
var dataset = DatasetRequestProcessor.createFromRequest(request, user, factory, getSession());
var dataset = DatasetRequestProcessor.createFromRequest(request, user, factory, getTemporaryUserData());

if (request.getDisplayName().isPresent()) {
dataset.setName(request.getDisplayName().get());
Expand Down
Loading

0 comments on commit 72c89a8

Please sign in to comment.