PageRenderTime 100ms CodeModel.GetById 4ms app.highlight 82ms RepoModel.GetById 2ms app.codeStats 0ms

/src/main/java/org/nrg/xnat/services/cache/DefaultGroupsAndPermissionsCache.java

https://bitbucket.org/radiologics/xnat-web-old-v2
Java | 2002 lines | 1631 code | 227 blank | 144 comment | 214 complexity | cd0aae515f9ce3d9be056923c204fa8b MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1/*
   2 * web: org.nrg.xnat.services.cache.DefaultUserProjectCache
   3 * XNAT http://www.xnat.org
   4 * Copyright (c) 2017, Washington University School of Medicine
   5 * All Rights Reserved
   6 *
   7 * Released under the Simplified BSD.
   8 */
   9
  10package org.nrg.xnat.services.cache;
  11
  12import com.google.common.base.Function;
  13import com.google.common.base.Joiner;
  14import com.google.common.base.Predicate;
  15import com.google.common.base.Predicates;
  16import com.google.common.collect.*;
  17import lombok.extern.slf4j.Slf4j;
  18import net.sf.ehcache.CacheException;
  19import net.sf.ehcache.Ehcache;
  20import net.sf.ehcache.Element;
  21import org.apache.commons.collections.CollectionUtils;
  22import org.apache.commons.lang3.StringUtils;
  23import org.apache.commons.lang3.time.DurationFormatUtils;
  24import org.nrg.framework.exceptions.NrgServiceRuntimeException;
  25import org.nrg.framework.orm.DatabaseHelper;
  26import org.nrg.framework.utilities.LapStopWatch;
  27import org.nrg.xdat.XDAT;
  28import org.nrg.xdat.display.ElementDisplay;
  29import org.nrg.xdat.om.*;
  30import org.nrg.xdat.schema.SchemaElement;
  31import org.nrg.xdat.security.SecurityManager;
  32import org.nrg.xdat.security.*;
  33import org.nrg.xdat.security.helpers.Permissions;
  34import org.nrg.xdat.security.helpers.Users;
  35import org.nrg.xdat.security.user.exceptions.UserInitException;
  36import org.nrg.xdat.security.user.exceptions.UserNotFoundException;
  37import org.nrg.xdat.services.Initializing;
  38import org.nrg.xdat.services.cache.GroupsAndPermissionsCache;
  39import org.nrg.xdat.servlet.XDATServlet;
  40import org.nrg.xft.db.PoolDBUtils;
  41import org.nrg.xft.event.XftItemEventI;
  42import org.nrg.xft.event.methods.XftItemEventCriteria;
  43import org.nrg.xft.exception.ElementNotFoundException;
  44import org.nrg.xft.exception.FieldNotFoundException;
  45import org.nrg.xft.exception.ItemNotFoundException;
  46import org.nrg.xft.exception.XFTInitException;
  47import org.nrg.xft.schema.XFTManager;
  48import org.nrg.xft.security.UserI;
  49import org.nrg.xnat.services.cache.jms.InitializeGroupRequest;
  50import org.slf4j.event.Level;
  51import org.springframework.beans.factory.annotation.Autowired;
  52import org.springframework.cache.CacheManager;
  53import org.springframework.dao.DataAccessException;
  54import org.springframework.jdbc.core.JdbcTemplate;
  55import org.springframework.jdbc.core.ResultSetExtractor;
  56import org.springframework.jdbc.core.RowCallbackHandler;
  57import org.springframework.jdbc.core.namedparam.EmptySqlParameterSource;
  58import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
  59import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
  60import org.springframework.jms.core.JmsTemplate;
  61import org.springframework.scheduling.annotation.Async;
  62import org.springframework.scheduling.annotation.AsyncResult;
  63import org.springframework.stereotype.Service;
  64
  65import javax.annotation.Nonnull;
  66import javax.annotation.Nullable;
  67import java.sql.ResultSet;
  68import java.sql.SQLException;
  69import java.text.DateFormat;
  70import java.text.NumberFormat;
  71import java.util.*;
  72import java.util.concurrent.ConcurrentHashMap;
  73import java.util.concurrent.Future;
  74import java.util.concurrent.atomic.AtomicBoolean;
  75import java.util.regex.Matcher;
  76import java.util.regex.Pattern;
  77
  78import static org.nrg.framework.exceptions.NrgServiceError.ConfigurationError;
  79import static org.nrg.xapi.rest.users.DataAccessApi.*;
  80import static org.nrg.xdat.security.PermissionCriteria.dumpCriteriaList;
  81import static org.nrg.xdat.security.helpers.Groups.*;
  82import static org.nrg.xft.event.XftItemEventI.*;
  83
  84@SuppressWarnings("Duplicates")
  85@Service
  86@Slf4j
  87public class DefaultGroupsAndPermissionsCache extends AbstractXftItemAndCacheEventHandlerMethod implements GroupsAndPermissionsCache, Initializing, GroupsAndPermissionsCache.Provider {
  88
  89    @Autowired
  90    public DefaultGroupsAndPermissionsCache(final CacheManager cacheManager, final NamedParameterJdbcTemplate template, final JmsTemplate jmsTemplate) throws SQLException {
  91        super(cacheManager,
  92              XftItemEventCriteria.builder().xsiType(XnatProjectdata.SCHEMA_ELEMENT_NAME).actions(CREATE, UPDATE, DELETE).build(),
  93              XftItemEventCriteria.builder().xsiType(XnatSubjectdata.SCHEMA_ELEMENT_NAME).xsiType(XnatExperimentdata.SCHEMA_ELEMENT_NAME).actions(CREATE, XftItemEventI.DELETE, SHARE).build(),
  94              XftItemEventCriteria.getXsiTypeCriteria(XdatUsergroup.SCHEMA_ELEMENT_NAME),
  95              XftItemEventCriteria.getXsiTypeCriteria(XdatElementSecurity.SCHEMA_ELEMENT_NAME));
  96
  97        _template = template;
  98        _jmsTemplate = jmsTemplate;
  99        _helper = new DatabaseHelper((JdbcTemplate) _template.getJdbcOperations());
 100        _totalCounts = new HashMap<>();
 101        _missingElements = new HashMap<>();
 102        _userChecks = new ConcurrentHashMap<>();
 103        _initialized = new AtomicBoolean(false);
 104        if (_helper.tableExists("xnat_projectdata")) {
 105            resetTotalCounts();
 106        }
 107    }
 108
 109    /**
 110     * {@inheritDoc}
 111     */
 112    @Override
 113    public void notifyElementRemoved(final Ehcache cache, final Element element) throws CacheException {
 114        handleCacheRemoveEvent(cache, element, "removed");
 115    }
 116
 117    /**
 118     * {@inheritDoc}
 119     */
 120    @Override
 121    public void notifyElementExpired(final Ehcache cache, final Element element) {
 122        handleCacheRemoveEvent(cache, element, "expired");
 123    }
 124
 125    /**
 126     * {@inheritDoc}
 127     */
 128    @Override
 129    public void notifyElementEvicted(final Ehcache cache, final Element element) {
 130        handleCacheRemoveEvent(cache, element, "evicted");
 131    }
 132
 133    /**
 134     * {@inheritDoc}
 135     */
 136    @Override
 137    public void notifyRemoveAll(final Ehcache cache) {
 138        handleCacheRemoveEvent(cache, null, "removed");
 139    }
 140
 141    /**
 142     * {@inheritDoc}
 143     */
 144    @Override
 145    public String getCacheName() {
 146        return CACHE_NAME;
 147    }
 148
 149    /**
 150     * Gets the specified project if the user has any access to it. Returns null otherwise.
 151     *
 152     * @param groupId The ID or alias of the project to retrieve.
 153     *
 154     * @return The project object if the user can access it, null otherwise.
 155     */
 156    @Override
 157    public UserGroupI get(final String groupId) {
 158        if (StringUtils.isBlank(groupId)) {
 159            throw new IllegalArgumentException("Can not request a group with a blank group ID");
 160        }
 161
 162        // Check that the group is cached and, if so, return it.
 163        log.trace("Retrieving group through cache ID {}", groupId);
 164        final UserGroupI cachedGroup = getCachedGroup(groupId);
 165        if (cachedGroup != null) {
 166            log.debug("Found cached group entry for cache ID '{}'", groupId);
 167            return cachedGroup;
 168        }
 169
 170        try {
 171            log.trace("Initializing group entry for cache ID '{}'", groupId);
 172            return initGroup(groupId);
 173        } catch (ItemNotFoundException e) {
 174            log.info("Can't find a group with the group ID '{}', returning null", groupId);
 175            return null;
 176        }
 177    }
 178
 179    /**
 180     * {@inheritDoc}
 181     */
 182    @Override
 183    public Map<String, Long> getReadableCounts(final UserI user) {
 184        if (user == null) {
 185            return Collections.emptyMap();
 186        }
 187
 188        final String username = user.getUsername();
 189        final String cacheId  = getCacheIdForUserElements(username, READABLE);
 190
 191        // Check whether the element types are cached and, if so, return that.
 192        log.trace("Retrieving readable counts for user {} through cache ID {}", username, cacheId);
 193        final Map<String, Long> cachedReadableCounts = getCachedMap(cacheId);
 194        if (cachedReadableCounts != null) {
 195            log.debug("Found cached readable counts entry for user '{}' with cache ID '{}' containing {} entries", username, cacheId, cachedReadableCounts.size());
 196            return cachedReadableCounts;
 197        }
 198
 199        return initReadableCountsForUser(cacheId, user);
 200    }
 201
 202    /**
 203     * {@inheritDoc}
 204     */
 205    @Override
 206    public Map<String, ElementDisplay> getBrowseableElementDisplays(final UserI user) {
 207        if (user == null) {
 208            return Collections.emptyMap();
 209        }
 210
 211        final Map<String, ElementDisplay> guestBrowseableElementDisplays = getGuestBrowseableElementDisplays();
 212        if (user.isGuest()) {
 213            log.debug("Got a request for browseable element displays for the guest user, returning {} entries", guestBrowseableElementDisplays.size());
 214            return guestBrowseableElementDisplays;
 215        }
 216
 217        final String username = user.getUsername();
 218        final String cacheId  = getCacheIdForUserElements(username, BROWSEABLE);
 219
 220        // Check whether the element types are cached and, if so, return that.
 221        log.trace("Retrieving browseable element displays for user {} through cache ID {}", username, cacheId);
 222        final Map<String, ElementDisplay> cachedUserEntry = getCachedMap(cacheId);
 223        if (cachedUserEntry != null) {
 224            @SuppressWarnings("unchecked") final Map<String, ElementDisplay> browseables = buildImmutableMap(cachedUserEntry, guestBrowseableElementDisplays);
 225            log.debug("Found a cached entry for user '{}' browseable element displays under cache ID '{}' with {} entries", username, cacheId, browseables.size());
 226            return browseables;
 227        }
 228
 229        log.trace("Initializing browseable element displays for user '{}' with cache ID '{}'", username, cacheId);
 230        @SuppressWarnings("unchecked") final Map<String, ElementDisplay> browseables = buildImmutableMap(initBrowseableElementDisplaysForUser(cacheId, user), guestBrowseableElementDisplays);
 231        log.debug("Initialized browseable element displays for user '{}' with cache ID '{}' with {} entries (including guest browseable element displays)", username, cacheId, browseables.size());
 232        return browseables;
 233    }
 234
 235    /**
 236     * {@inheritDoc}
 237     */
 238    @Override
 239    public List<ElementDisplay> getSearchableElementDisplays(final UserI user) {
 240        if (user == null) {
 241            return Collections.emptyList();
 242        }
 243
 244        final String username = user.getUsername();
 245        final String cacheId  = getCacheIdForUserElements(username, SEARCHABLE);
 246        log.debug("Retrieving searchable element displays for user {} through cache ID {}", username, cacheId);
 247
 248        final Map<String, Long> counts = getReadableCounts(user);
 249        try {
 250            return Lists.newArrayList(Iterables.filter(getActionElementDisplays(username, SecurityManager.READ), new Predicate<ElementDisplay>() {
 251                @Override
 252                public boolean apply(@Nullable final ElementDisplay elementDisplay) {
 253                    if (elementDisplay == null) {
 254                        return false;
 255                    }
 256                    final String elementName = elementDisplay.getElementName();
 257                    try {
 258                        return ElementSecurity.IsSearchable(elementName) && counts.containsKey(elementName) && counts.get(elementName) > 0;
 259                    } catch (Exception e) {
 260                        return false;
 261                    }
 262                }
 263            }));
 264        } catch (Exception e) {
 265            log.error("An unknown error occurred", e);
 266        }
 267
 268        return Collections.emptyList();
 269    }
 270
 271    /**
 272     * {@inheritDoc}
 273     */
 274    @Override
 275    public List<ElementDisplay> getActionElementDisplays(final UserI user, final String action) {
 276        return getActionElementDisplays(user.getUsername(), action);
 277    }
 278
 279    @Override
 280    public List<ElementDisplay> getActionElementDisplays(final String username, final String action) {
 281        if (!ACTIONS.contains(action)) {
 282            throw new NrgServiceRuntimeException(ConfigurationError, "The action '" + action + "' is invalid, must be one of: " + StringUtils.join(ACTIONS, ", "));
 283        }
 284        final List<ElementDisplay> elementDisplays = getActionElementDisplays(username).get(action);
 285        if (log.isTraceEnabled()) {
 286            log.trace("Found {} element displays for user {} action {}: {}", elementDisplays.size(), username, action, formatElementDisplays(elementDisplays));
 287        }
 288        return elementDisplays;
 289    }
 290
 291    /**
 292     * {@inheritDoc}
 293     */
 294    @Override
 295    public List<PermissionCriteriaI> getPermissionCriteria(final UserI user, final String dataType) {
 296        return getPermissionCriteria(user.getUsername(), dataType);
 297    }
 298
 299    /**
 300     * {@inheritDoc}
 301     */
 302    @Override
 303    public List<PermissionCriteriaI> getPermissionCriteria(final String username, final String dataType) {
 304        try {
 305            PoolDBUtils.CheckSpecialSQLChars(dataType);
 306        } catch (Exception e) {
 307            return null;
 308        }
 309
 310        try {
 311            final List<PermissionCriteriaI> criteria = new ArrayList<>();
 312
 313            final Map<String, ElementAccessManager> managers = getElementAccessManagers(username);
 314            if (managers.isEmpty()) {
 315                log.info("Couldn't find element access managers for user {} trying to retrieve permissions for data type {}", username, dataType);
 316            } else {
 317                final ElementAccessManager manager = managers.get(dataType);
 318                if (manager == null) {
 319                    log.info("Couldn't find element access manager for data type {} for user {} while trying to retrieve permissions ", dataType, username);
 320                } else {
 321                    criteria.addAll(manager.getCriteria());
 322                    if (criteria.isEmpty()) {
 323                        log.debug("Couldn't find any permission criteria for data type {} for user {} while trying to retrieve permissions ", dataType, username);
 324                    }
 325                }
 326            }
 327
 328            final Map<String, UserGroupI> userGroups = getMutableGroupsForUser(username);
 329            if (log.isDebugEnabled()) {
 330                log.debug("Found {} user groups for the user {}", userGroups.size(), username, userGroups.isEmpty() ? "" : ": " + Joiner.on(", ").join(userGroups.keySet()));
 331            }
 332            final Set<String> groups = userGroups.keySet();
 333            if (CollectionUtils.containsAny(groups, ALL_DATA_GROUPS)) {
 334                if (groups.contains(ALL_DATA_ADMIN_GROUP)) {
 335                    _template.query(QUERY_GET_ALL_MEMBER_GROUPS, new RowCallbackHandler() {
 336                        @Override
 337                        public void processRow(final ResultSet resultSet) throws SQLException {
 338                            final String projectId = resultSet.getString("project_id");
 339                            final String groupId   = resultSet.getString("group_id");
 340
 341                            // If the user is a collaborator on a project, we're going to upgrade them to member,
 342                            // so remove that collaborator nonsense, this is the big time.
 343                            userGroups.remove(projectId + "_collaborator");
 344
 345                            // If the user is already a member of owner of a project, then don't bother: they already have
 346                            // sufficient access to the project.
 347                            if (!userGroups.containsKey(projectId + "_owner") && !userGroups.containsKey(projectId + "_member")) {
 348                                userGroups.put(groupId, get(groupId));
 349                            }
 350                        }
 351                    });
 352                } else if (userGroups.containsKey(ALL_DATA_ACCESS_GROUP)) {
 353                    _template.query(QUERY_GET_ALL_COLLAB_GROUPS, new RowCallbackHandler() {
 354                        @Override
 355                        public void processRow(final ResultSet resultSet) throws SQLException {
 356                            final String projectId = resultSet.getString("project_id");
 357                            final String groupId   = resultSet.getString("group_id");
 358
 359                            // If the user has no group membership, then add as a collaborator.
 360                            if (!CollectionUtils.containsAny(groups, Arrays.asList(groupId, projectId + "_member", projectId + "_owner"))) {
 361                                userGroups.put(groupId, get(groupId));
 362                            }
 363                        }
 364                    });
 365                }
 366            }
 367
 368            for (final UserGroupI group : userGroups.values()) {
 369                final List<PermissionCriteriaI> permissions = group.getPermissionsByDataType(dataType);
 370                if (permissions != null) {
 371                    if (log.isTraceEnabled()) {
 372                        log.trace("Searched for permission criteria for user {} on type {} in group {}: {}", username, dataType, group.getId(), dumpCriteriaList(permissions));
 373                    } else {
 374                        log.debug("Searched for permission criteria for user {} on type {} in group {}: {} permissions found", username, dataType, group.getId(), permissions.size());
 375                    }
 376                    criteria.addAll(permissions);
 377                } else {
 378                    log.warn("Tried to retrieve permissions for data type {} for user {} in group {}, but this returned null.", dataType, username, group.getId());
 379                }
 380            }
 381
 382            if (!isGuest(username)) {
 383                try {
 384                    final List<PermissionCriteriaI> permissions = getPermissionCriteria(getGuest().getUsername(), dataType);
 385                    if (permissions != null) {
 386                        criteria.addAll(permissions);
 387                    } else {
 388                        log.warn("Tried to retrieve permissions for data type {} for the guest user, but this returned null.", dataType);
 389                    }
 390                } catch (Exception e) {
 391                    log.error("An error occurred trying to retrieve the guest user", e);
 392                }
 393            }
 394
 395            if (log.isTraceEnabled()) {
 396                log.trace("Retrieved permission criteria for user {} on the data type {}: {}", username, dataType, dumpCriteriaList(criteria));
 397            } else {
 398                log.debug("Retrieved permission criteria for user {} on the data type {}: {} criteria found", username, dataType, criteria.size());
 399            }
 400
 401            return ImmutableList.copyOf(criteria);
 402        } catch (UserNotFoundException e) {
 403            log.error("Couldn't find the indicated user");
 404            return Collections.emptyList();
 405        }
 406    }
 407
 408    @Override
 409    public Map<String, Long> getTotalCounts() {
 410        if (_totalCounts.isEmpty()) {
 411            resetTotalCounts();
 412        }
 413
 414        return ImmutableMap.copyOf(_totalCounts);
 415    }
 416
 417    @Nonnull
 418    @Override
 419    public List<String> getProjectsForUser(final String username, final String access) {
 420        log.info("Getting projects with {} access for user {}", access, username);
 421        final String cacheId = getCacheIdForUserProjectAccess(username, access);
 422
 423        final List<String> cachedUserProjects = getCachedList(cacheId);
 424        if (cachedUserProjects != null) {
 425            log.debug("Found a cache entry for user '{}' '{}' access with ID '{}' and {} elements", username, access, cacheId, cachedUserProjects.size());
 426            return cachedUserProjects;
 427        }
 428
 429        return updateUserProjectAccess(username, access, cacheId);
 430    }
 431
 432    /**
 433     * {@inheritDoc}
 434     */
 435    @Nonnull
 436    @Override
 437    public List<UserGroupI> getGroupsForTag(final String tag) {
 438        // Get the group IDs associated with the tag.
 439        log.info("Getting groups for tag {}", tag);
 440        final List<String> groupIds = getTagGroups(tag);
 441        return getUserGroupList(groupIds);
 442    }
 443
 444    @Nonnull
 445    @Override
 446    public Map<String, UserGroupI> getGroupsForUser(final String username) throws UserNotFoundException {
 447        return ImmutableMap.copyOf(getMutableGroupsForUser(username));
 448    }
 449
 450    /**
 451     * {@inheritDoc}
 452     */
 453    @Override
 454    public void refreshGroupsForUser(final String username) throws UserNotFoundException {
 455        initUserGroupIds(getCacheIdForUserGroups(username), username);
 456    }
 457
 458    /**
 459     * {@inheritDoc}
 460     */
 461    @Override
 462    @Nullable
 463    public UserGroupI getGroupForUserAndTag(final String username, final String tag) throws UserNotFoundException {
 464        final String groupId = _template.query(QUERY_GET_GROUP_FOR_USER_AND_TAG, checkUser(username).addValue("tag", tag), new ResultSetExtractor<String>() {
 465            @Override
 466            public String extractData(final ResultSet results) throws DataAccessException, SQLException {
 467                return results.next() ? results.getString("id") : null;
 468            }
 469        });
 470        return StringUtils.isNotBlank(groupId) ? get(groupId) : null;
 471    }
 472
 473    /**
 474     * {@inheritDoc}
 475     */
 476    @Override
 477    public List<String> getUserIdsForGroup(final String groupId) {
 478        final UserGroupI userGroup = get(groupId);
 479        if (userGroup == null) {
 480            return Collections.emptyList();
 481        }
 482        return ImmutableList.copyOf(userGroup.getUsernames());
 483    }
 484
 485    @Override
 486    public void refreshGroup(final String groupId) throws ItemNotFoundException {
 487        final UserGroupI group = initGroup(groupId);
 488        for (final String username : group.getUsernames()) {
 489            try {
 490                if (!getGroupIdsForUser(username).contains(groupId)) {
 491                    refreshGroupsForUser(username);
 492                }
 493            } catch (UserNotFoundException ignored) {
 494                //
 495            }
 496        }
 497    }
 498
 499    @Override
 500    public Date getUserLastUpdateTime(final UserI user) {
 501        return getUserLastUpdateTime(user.getUsername());
 502    }
 503
 504    @Override
 505    public Date getUserLastUpdateTime(final String username) {
 506        try {
 507            @SuppressWarnings("unchecked") final List<String> cacheIds = new ArrayList<>(buildImmutableSet(getGroupIdsForUser(username), getCacheIdsForUsername(username)));
 508            if (cacheIds.isEmpty()) {
 509                return new Date();
 510            }
 511            if (log.isDebugEnabled()) {
 512                log.debug("Found {} cache entries related to user {}: {}", cacheIds.size(), username, StringUtils.join(cacheIds, ", "));
 513            }
 514            final long lastUpdateTime = Collections.max(Lists.transform(cacheIds, new Function<String, Long>() {
 515                @Override
 516                public Long apply(@Nullable final String cacheId) {
 517                    final Date lastUpdateTime = getCacheEntryLastUpdateTime(cacheId);
 518                    log.trace("User {} cache entry '{}' last updated: {}", username, cacheId, lastUpdateTime == null ? "null" : lastUpdateTime.getTime());
 519                    return lastUpdateTime == null ? 0L : lastUpdateTime.getTime();
 520                }
 521            }));
 522            log.debug("Found latest cache entry last updated time for user {}: {}", username, lastUpdateTime);
 523            return new Date(lastUpdateTime);
 524        } catch (UserNotFoundException ignored) {
 525            log.warn("Someone requested the cache entry last updated time for user {} but that user wasn't found", username);
 526            return new Date();
 527        }
 528    }
 529
 530    /**
 531     * Finds all user element cache IDs for the specified user and evicts them from the cache.
 532     *
 533     * @param username The username to be cleared.
 534     */
 535    @Override
 536    public void clearUserCache(final String username) {
 537        final List<String> cacheIds = getCacheIdsForUserElements(username);
 538        if (log.isDebugEnabled()) {
 539            log.debug("Clearing caches for user '{}': {}", username, StringUtils.join(cacheIds, ", "));
 540        }
 541        evict(cacheIds);
 542    }
 543
 544    @Override
 545    public boolean canInitialize() {
 546        try {
 547            if (_listener == null) {
 548                return false;
 549            }
 550            final boolean doesUserGroupTableExists            = _helper.tableExists("xdat_usergroup");
 551            final boolean isXftManagerComplete                = XFTManager.isComplete();
 552            final boolean isDatabasePopulateOrUpdateCompleted = XDATServlet.isDatabasePopulateOrUpdateCompleted();
 553            log.info("User group table {}, XFTManager initialization completed {}, database populate or updated completed {}", doesUserGroupTableExists, isXftManagerComplete, isDatabasePopulateOrUpdateCompleted);
 554            return doesUserGroupTableExists && isXftManagerComplete && isDatabasePopulateOrUpdateCompleted;
 555        } catch (SQLException e) {
 556            log.info("Got an SQL exception checking for xdat_usergroup table", e);
 557            return false;
 558        }
 559    }
 560
 561    @Async
 562    @Override
 563    public Future<Boolean> initialize() {
 564        final LapStopWatch stopWatch = LapStopWatch.createStarted(log, Level.INFO);
 565
 566        // This clears out any group initialization requests that may be left in the database from earlier starts.
 567        _template.update("DELETE FROM activemq_msgs WHERE container LIKE '%initializeGroupRequest'", EmptySqlParameterSource.INSTANCE);
 568
 569        final int tags = initializeTags();
 570        stopWatch.lap("Processed {} tags", tags);
 571
 572        final List<String> groupIds = _template.queryForList(QUERY_ALL_GROUPS, EmptySqlParameterSource.INSTANCE, String.class);
 573        _listener.setGroupIds(groupIds);
 574        stopWatch.lap("Initialized listener of type {} with {} tags", _listener.getClass().getName(), tags);
 575
 576        try {
 577            final UserI adminUser = Users.getAdminUser();
 578            assert adminUser != null;
 579
 580            stopWatch.lap("Found {} group IDs to run through, initializing cache with these as user {}", groupIds.size(), adminUser.getUsername());
 581            for (final String groupId : groupIds) {
 582                stopWatch.lap(Level.DEBUG, "Creating queue entry for group {}", groupId);
 583                XDAT.sendJmsRequest(_jmsTemplate, new InitializeGroupRequest(groupId));
 584            }
 585        } finally {
 586            if (stopWatch.isStarted()) {
 587                stopWatch.stop();
 588            }
 589            log.info("Total time to queue {} groups was {} ms", groupIds.size(), NUMBER_FORMAT.format(stopWatch.getTime()));
 590            if (log.isInfoEnabled()) {
 591                log.info(stopWatch.toTable());
 592            }
 593        }
 594
 595        resetGuestBrowseableElementDisplays();
 596        _initialized.set(true);
 597        return new AsyncResult<>(true);
 598    }
 599
 600    @Override
 601    public boolean isInitialized() {
 602        return _initialized.get();
 603    }
 604
 605    @Override
 606    public Map<String, String> getInitializationStatus() {
 607        final Map<String, String> status = new HashMap<>();
 608        if (_listener == null) {
 609            status.put("message", "No listener registered, so no status to report.");
 610            return status;
 611        }
 612
 613        final Set<String> processed      = _listener.getProcessed();
 614        final int         processedCount = processed.size();
 615        final Set<String> unprocessed    = _listener.getUnprocessed();
 616        final Date        start          = _listener.getStart();
 617
 618        status.put("start", DATE_FORMAT.format(start));
 619        status.put("processedCount", Integer.toString(processedCount));
 620        status.put("processed", StringUtils.join(processed, ", "));
 621
 622        if (unprocessed.isEmpty()) {
 623            final Date   completed = _listener.getCompleted();
 624            final String duration  = DurationFormatUtils.formatPeriodISO(start.getTime(), completed.getTime());
 625
 626            status.put("completed", DATE_FORMAT.format(completed));
 627            status.put("duration", duration);
 628            status.put("message", "Cache initialization is complete. Processed " + processedCount + " groups in " + duration);
 629            return status;
 630        }
 631
 632        final Date   now              = new Date();
 633        final String duration         = DurationFormatUtils.formatPeriodISO(start.getTime(), now.getTime());
 634        final int    unprocessedCount = unprocessed.size();
 635
 636        status.put("unprocessedCount", Integer.toString(unprocessedCount));
 637        status.put("unprocessed", StringUtils.join(unprocessed, ", "));
 638        status.put("current", DATE_FORMAT.format(now));
 639        status.put("duration", duration);
 640        status.put("message", "Cache initialization is on-going, with " + processedCount + " groups processed and " + unprocessedCount + " groups remaining, time elapsed so far is " + duration);
 641        return status;
 642    }
 643
 644    @Override
 645    public void registerListener(final Listener listener) {
 646        _listener = listener;
 647    }
 648
 649    @Override
 650    public Listener getListener() {
 651        return _listener;
 652    }
 653
 654    @Override
 655    protected boolean handleEventImpl(final XftItemEventI event) {
 656        switch (event.getXsiType()) {
 657            case XnatProjectdata.SCHEMA_ELEMENT_NAME:
 658                return handleProjectEvents(event);
 659
 660            case XnatSubjectdata.SCHEMA_ELEMENT_NAME:
 661                return handleSubjectEvents(event);
 662
 663            case XdatUsergroup.SCHEMA_ELEMENT_NAME:
 664                return handleGroupRelatedEvents(event);
 665
 666            case XdatElementSecurity.SCHEMA_ELEMENT_NAME:
 667                return handleElementSecurityEvents(event);
 668
 669            default:
 670                // This is always some type of experiment.
 671                return handleExperimentEvents(event);
 672        }
 673    }
 674
 675    private boolean handleProjectEvents(final XftItemEventI event) {
 676        final String         xsiType    = event.getXsiType();
 677        final String         id         = event.getId();
 678        final String         action     = event.getAction();
 679        final Map<String, ?> properties = event.getProperties();
 680
 681        try {
 682            switch (action) {
 683                case CREATE:
 684                    log.debug("New project created with ID {}, caching new instance", xsiType, id);
 685                    for (final String owner : getProjectOwners(id)) {
 686                        updateUserProjectAccess(owner);
 687                        if (!Iterables.any(getActionElementDisplays(owner).get(SecurityManager.CREATE), CONTAINS_MR_SESSION)) {
 688                            initActionElementDisplays(owner, true);
 689                        }
 690                    }
 691
 692                    final boolean created = !initGroups(getGroups(xsiType, id)).isEmpty();
 693                    final String access = Permissions.getProjectAccess(_template, id);
 694                    if (StringUtils.isNotBlank(access)) {
 695                        switch (access) {
 696                            case "private":
 697                                clearAllDataUserProjectAccess();
 698                                break;
 699
 700                            case "public":
 701                                if (!Iterables.any(getActionElementDisplays(GUEST_USERNAME).get(SecurityManager.READ), CONTAINS_MR_SESSION)) {
 702                                    initActionElementDisplays(GUEST_USERNAME, true);
 703                                }
 704
 705                            case "protected":
 706                                updateProjectRelatedCaches(xsiType, id, false);
 707                                break;
 708                        }
 709                    }
 710                    return created;
 711
 712                case UPDATE:
 713                    log.debug("The {} object {} was updated, caching updated instance", xsiType, id);
 714                    if (properties.containsKey("accessibility")) {
 715                        final String accessibility = (String) properties.get("accessibility");
 716                        switch (accessibility) {
 717                            case "private":
 718                                return updateProjectRelatedCaches(xsiType, id, true);
 719
 720                            case "public":
 721                                if (!Iterables.any(getActionElementDisplays(GUEST_USERNAME).get(SecurityManager.READ), CONTAINS_MR_SESSION)) {
 722                                    initActionElementDisplays(GUEST_USERNAME, true);
 723                                }
 724
 725                            case "protected":
 726                                return updateProjectRelatedCaches(xsiType, id, true);
 727
 728                            default:
 729                                log.warn("The project {}'s accessibility setting was updated to an invalid value: {}. Must be one of private, protected, or public.", id, accessibility);
 730                        }
 731                    }
 732                    break;
 733
 734                case XftItemEventI.DELETE:
 735                    log.debug("The {} {} was deleted, removing related instances from cache", xsiType, id);
 736                    final String cacheId = getCacheIdForProject(id);
 737                    evict(cacheId);
 738                    for (final String accessCacheId : Iterables.filter(getCacheIdsForUserElements(), Predicates.contains(REGEX_USER_PROJECT_ACCESS_CACHE_ID))) {
 739                        final List<String> projectIds = getCachedList(accessCacheId);
 740                        if (projectIds != null && projectIds.contains(id)) {
 741                            final List<String> updated = new ArrayList<>(projectIds);
 742                            updated.remove(id);
 743                            forceCacheObject(accessCacheId, updated);
 744                        }
 745                    }
 746                    resetGuestBrowseableElementDisplays();
 747                    initReadableCountsForUsers(this.<String>getCachedSet(cacheId));
 748                    resetTotalCounts();
 749                    return true;
 750
 751                default:
 752                    log.warn("I was informed that the '{}' action happened to the project with ID '{}'. I don't know what to do with this action.", action, xsiType, id);
 753                    break;
 754            }
 755        } catch (ItemNotFoundException e) {
 756            log.warn("While handling action {}, I couldn't find a group for type {} ID {}.", action, xsiType, id);
 757        }
 758
 759        return false;
 760    }
 761
 762    private void clearAllDataUserProjectAccess() {
 763        for (final String groupId : ALL_DATA_GROUPS) {
 764            final UserGroupI group = get(groupId);
 765            if (group != null) {
 766                for (final String user : group.getUsernames()) {
 767                    for (final String projectAction : ACTIONS) {
 768                        evict(getCacheIdForUserProjectAccess(user, projectAction));
 769                    }
 770                }
 771            }
 772        }
 773    }
 774
 775    private boolean updateProjectRelatedCaches(final String xsiType, final String id, final boolean affectsOtherDataTypes) throws ItemNotFoundException {
 776        final boolean cachedRelatedGroups = !initGroups(getGroups(xsiType, id)).isEmpty();
 777
 778        evict(GUEST_CACHE_ID);
 779        evict(GUEST_ACTION_READ);
 780        resetGuestBrowseableElementDisplays();
 781        initActionElementDisplays(GUEST_USERNAME, true);
 782
 783        final Set<String> readableCountCacheIds = new HashSet<>(getCacheIdsForUserReadableCounts());
 784        if (affectsOtherDataTypes) {
 785            for (final String cacheId : readableCountCacheIds) {
 786                evict(cacheId);
 787            }
 788            resetTotalCounts();
 789        } else {
 790            // Update existing user element displays
 791            final List<String> cacheIds = getCacheIdsForActions();
 792            cacheIds.addAll(getCacheIdsForUserElements());
 793            clearAllUserProjectAccess();
 794            initReadableCountsForUsers(Sets.newHashSet(Iterables.filter(Lists.transform(cacheIds, FUNCTION_CACHE_IDS_TO_USERNAMES), Predicates.notNull())));
 795            resetProjectCount();
 796        }
 797
 798        return cachedRelatedGroups;
 799    }
 800
 801    private boolean handleSubjectEvents(final XftItemEventI event) {
 802        final String action = event.getAction();
 803        log.debug("Handling subject {} event for {} {}", XftItemEventI.ACTIONS.get(action), event.getXsiType(), event.getId());
 804        final Set<String> projectIds = new HashSet<>();
 805        switch (action) {
 806            case CREATE:
 807                projectIds.add(_template.queryForObject(QUERY_GET_SUBJECT_PROJECT, new MapSqlParameterSource("subjectId", event.getId()), String.class));
 808                resetTotalCounts();
 809                break;
 810
 811            case SHARE:
 812                projectIds.add((String) event.getProperties().get("target"));
 813                break;
 814
 815            case MOVE:
 816                projectIds.add((String) event.getProperties().get("origin"));
 817                projectIds.add((String) event.getProperties().get("target"));
 818                break;
 819
 820            case XftItemEventI.DELETE:
 821                projectIds.add((String) event.getProperties().get("target"));
 822                handleGroupRelatedEvents(event);
 823                resetTotalCounts();
 824                break;
 825
 826            default:
 827                log.warn("I was informed that the '{}' action happened to subject '{}'. I don't know what to do with this action.", action, event.getId());
 828        }
 829        if (projectIds.isEmpty()) {
 830            return false;
 831        }
 832        final Set<String> users = getProjectUsers(projectIds);
 833        for (final String username : users) {
 834            initReadableCountsForUser(username);
 835        }
 836        return true;
 837    }
 838
 839    private boolean handleGroupRelatedEvents(final XftItemEventI event) {
 840        final String         xsiType    = event.getXsiType();
 841        final String         id         = event.getId();
 842        final String         action     = event.getAction();
 843        final Map<String, ?> properties = event.getProperties();
 844        final Set<String>    usernames  = new HashSet<>();
 845
 846        try {
 847            final List<UserGroupI> groups = getGroups(xsiType, id);
 848            switch (action) {
 849                case CREATE:
 850                    log.debug("New {} created with ID {}, caching new instance", xsiType, id);
 851                    for (final UserGroupI group : groups) {
 852                        usernames.addAll(group.getUsernames());
 853                    }
 854                    log.debug("Handling create group event with ID '{}' for users: {}", id);
 855                    return !initGroups(groups).isEmpty();
 856
 857                case UPDATE:
 858                    log.debug("The {} object {} was updated, caching updated instance", xsiType, id);
 859                    for (final UserGroupI group : groups) {
 860                        usernames.addAll(group.getUsernames());
 861                        evict(group.getId());
 862                    }
 863                    if (properties.containsKey(OPERATION) && StringUtils.equals((String) properties.get(OPERATION), OPERATION_REMOVE_USERS)) {
 864                        //noinspection unchecked
 865                        usernames.addAll((Collection<? extends String>) properties.get(USERS));
 866                    }
 867                    log.debug("Handling update group event with ID '{}' for users: {}", id);
 868                    return !initGroups(groups).isEmpty();
 869
 870                case XftItemEventI.DELETE:
 871                    if (StringUtils.equals(XnatProjectdata.SCHEMA_ELEMENT_NAME, xsiType)) {
 872                        final List<String> groupIds = getTagGroups(id);
 873                        if (CollectionUtils.isNotEmpty(groupIds)) {
 874                            log.info("Found {} groups cached for deleted project {}", groupIds.size(), id);
 875                            for (final String groupId : groupIds) {
 876                                evictGroup(groupId, usernames);
 877                            }
 878                        }
 879                    } else {
 880                        evictGroup(id, usernames);
 881                    }
 882                    break;
 883
 884                default:
 885                    log.warn("I was informed that the '{}' action happened to the {} object with ID '{}'. I don't know what to do with this action.", action, xsiType, id);
 886            }
 887        } catch (ItemNotFoundException e) {
 888            log.warn("While handling action {}, I couldn't find a group for type {} ID {}.", action, xsiType, id);
 889        } finally {
 890            for (final String username : usernames) {
 891                try {
 892                    final String cacheId = getCacheIdForUserGroups(username);
 893                    initUserGroupIds(cacheId, username);
 894                    initReadableCountsForUser(username);
 895                } catch (UserNotFoundException e) {
 896                    log.warn("While handling action {} for type {} ID {}, I couldn't find a user with username {}.", action, xsiType, id, username);
 897                }
 898            }
 899        }
 900        return false;
 901    }
 902
 903    private boolean handleElementSecurityEvents(final XftItemEventI event) {
 904        log.debug("Handling {} event for '{}' IDs {}. Updating guest browseable element displays...", event.getAction(), event.getXsiType(), event.getIds());
 905        final Map<String, ElementDisplay> displays = resetGuestBrowseableElementDisplays();
 906
 907        if (log.isTraceEnabled()) {
 908            log.trace("Got back {} browseable element displays for guest user after refresh: {}", displays.size(), StringUtils.join(displays.keySet(), ", "));
 909        }
 910
 911        log.debug("Evicting all action and user element cache IDs");
 912        for (final String cacheId : Iterables.concat(getCacheIdsForActions(), getCacheIdsForUserElements())) {
 913            log.trace("Evicting cache entry with ID '{}'", cacheId);
 914            evict(cacheId);
 915        }
 916
 917        for (final String dataType : event.getIds()) {
 918            final List<String> groupIds = getGroupIdsForDataType(dataType);
 919            log.debug("Found {} groups that reference the '{}' data type, updating cache entries for: {}", groupIds.size(), dataType, StringUtils.join(groupIds, ", "));
 920            for (final String groupId : groupIds) {
 921                log.trace("Evicting group '{}' due to change in element securities for data type {}", groupId, dataType);
 922                evict(groupId);
 923            }
 924        }
 925
 926        return true;
 927    }
 928
 929    private boolean handleExperimentEvents(final XftItemEventI event) {
 930        final String action  = event.getAction();
 931        final String xsiType = event.getXsiType();
 932        log.debug("Handling experiment {} event for {} {}", XftItemEventI.ACTIONS.get(action), xsiType, event.getId());
 933        final String target, origin;
 934        switch (action) {
 935            case CREATE:
 936                target = _template.queryForObject(QUERY_GET_EXPERIMENT_PROJECT, new MapSqlParameterSource("experimentId", event.getId()), String.class);
 937                origin = null;
 938                break;
 939
 940            case SHARE:
 941                target = (String) event.getProperties().get("target");
 942                origin = null;
 943                break;
 944
 945            case MOVE:
 946                origin = (String) event.getProperties().get("origin");
 947                target = (String) event.getProperties().get("target");
 948                break;
 949
 950            case XftItemEventI.DELETE:
 951                target = (String) event.getProperties().get("target");
 952                origin = null;
 953                break;
 954
 955            default:
 956                log.warn("I was informed that the '{}' action happened to experiment '{}' with ID '{}'. I don't know what to do with this action.", action, xsiType, event.getId());
 957                return false;
 958        }
 959
 960        final Map<String, ElementDisplay> displays = getGuestBrowseableElementDisplays();
 961        log.debug("Found {} elements for guest user: {}", displays.size(), StringUtils.join(displays.keySet(), ", "));
 962
 963        // If the data type of the experiment isn't in the guest list AND the target project is public,
 964        // OR if the origin project is both specified and public (meaning the data type might be REMOVED
 965        // from the guest browseable element displays), then we update the guest browseable element displays.
 966        final boolean hasEventXsiType        = displays.containsKey(xsiType);
 967        final boolean isTargetProjectPublic  = Permissions.isProjectPublic(_template, target);
 968        final boolean hasOriginProject       = StringUtils.isNotBlank(origin);
 969        final boolean isMovedFromPublicToNon = !isTargetProjectPublic && hasOriginProject && Permissions.isProjectPublic(_template, origin);
 970
 971        // We need to add the XSI type if guest doesn't already have it and the target project is public.
 972        final boolean needsPublicXsiTypeAdded = !hasEventXsiType && isTargetProjectPublic;
 973
 974        // We need to check if the XSI type should be removed if guest has XSI type and item was moved from public to non-public.
 975        final boolean needsXsiTypeChecked = hasEventXsiType && isMovedFromPublicToNon;
 976
 977        if (needsPublicXsiTypeAdded || needsXsiTypeChecked) {
 978            if (needsPublicXsiTypeAdded) {
 979                log.debug("Updating guest browseable element displays: guest doesn't have the event XSI type '{}' and the target project {} is public.", xsiType, target);
 980            } else {
 981                log.debug("Updating guest browseable element displays: guest has the event XSI type '{}' and item was moved from public project {} to non-public project {}.", xsiType, origin, target);
 982            }
 983            resetGuestBrowseableElementDisplays();
 984        } else {
 985            log.debug("Not updating guest browseable element displays: guest {} '{}' and {}",
 986                      hasEventXsiType ? "already has the event XSI type " : "doesn't have the event XSI type",
 987                      xsiType,
 988                      isTargetProjectPublic ? "target project is public" : "target project is not public");
 989        }
 990        initReadableCountsForUsers(hasOriginProject ? getProjectUsers(target) : getProjectUsers(target, origin));
 991        if (StringUtils.equalsAny(action, CREATE, XftItemEventI.DELETE)) {
 992            resetTotalCounts();
 993        }
 994        return true;
 995    }
 996
 997    private void handleCacheRemoveEvent(final Ehcache cache, final Element element, final String event) {
 998        if (isGroupsAndPermissionsCacheEvent(cache)) {
 999            if (element == null) {
1000                log.debug("Got a {} event for cache {}, no specific element affected", event, cache.getName());
1001                return;
1002            }
1003            final Object objectValue = element.getObjectValue();
1004            log.debug("Got a {} event for cache {} on ID {} with value of type {}", event, cache.getName(), element.getObjectKey(), objectValue != null ? objectValue.getClass().getName() : "<null>");
1005        }
1006    }
1007
1008    @SuppressWarnings({"UnusedReturnValue", "SameParameterValue"})
1009    private synchronized List<String> updateUserProjectAccess(final String username) {
1010        final List<String> projectIds = new ArrayList<>();
1011        for (final String access : Arrays.asList(SecurityManager.READ, SecurityManager.EDIT, SecurityManager.DELETE)) {
1012            projectIds.addAll(updateUserProjectAccess(username, access));
1013        }
1014        return projectIds;
1015    }
1016
1017    @SuppressWarnings({"UnusedReturnValue", "SameParameterValue"})
1018    private synchronized List<String> updateUserProjectAccess(final String username, final String access) {
1019        return updateUserProjectAccess(username, access, getCacheIdForUserProjectAccess(username, access));
1020    }
1021
1022    private synchronized List<String> updateUserProjectAccess(final String username, final String access, final String cacheId) {
1023        final List<String> projectIds;
1024        switch (access) {
1025            case SecurityManager.READ:
1026                projectIds = getUserReadableProjects(username);
1027                break;
1028            case SecurityManager.EDIT:
1029                projectIds = getUserEditableProjects(username);
1030                break;
1031            case SecurityManager.DELETE:
1032                projectIds = getUserOwnedProjects(username);
1033                break;
1034            default:
1035                throw new RuntimeException("Unknown access level '" + access + "'. Must be one of " + SecurityManager.READ + ", " + SecurityManager.EDIT + ", or " + SecurityManager.DELETE + ".");
1036        }
1037        cacheObject(cacheId, projectIds);
1038        return ImmutableList.copyOf(projectIds);
1039    }
1040
1041    private List<String> getProjectOwners(final String projectId) {
1042        return _template.queryForList(QUERY_PROJECT_OWNERS, new MapSqlParameterSource("projectId", projectId), String.class);
1043    }
1044
1045    private ListMultimap<String, ElementDisplay> getActionElementDisplays(final String username) {
1046        final String cacheId = getCacheIdForActionElements(username);
1047
1048        // Check whether the action elements are cached and, if so, return that.
1049        final ListMultimap<String, ElementDisplay> cachedActions = getCachedListMultimap(cacheId);
1050        if (cachedActions != null) {
1051            log.debug("Found a cache entry for user '{}' action elements by ID '{}'", username, cacheId);
1052            return cachedActions;
1053        }
1054
1055        return initActionElementDisplays(username);
1056    }
1057
1058    private Long getUserReadableWorkflowCount(final UserI user) {
1059        return _template.queryForObject(QUERY_USER_READABLE_WORKFLOW_COUNT, new MapSqlParameterSource("username", user.getUsername()), Long.class);
1060    }
1061
1062    @Nonnull
1063    private Map<String, UserGroupI> getMutableGroupsForUser(final String username) throws UserNotFoundException {
1064        final List<String>            groupIds = getGroupIdsForUser(username);
1065        final Map<String, UserGroupI> groups   = new HashMap<>();
1066        for (final String groupId : groupIds) {
1067            final UserGroupI group = get(groupId);
1068            if (group != null) {
1069                log.trace("Adding group {} to groups for user {}", groupId, username);
1070                groups.put(groupId, group);
1071            } else {
1072                log.info("User '{}' is associated with the group ID '{}', but I couldn't find that actual group", username, groupId);
1073            }
1074        }
1075        return groups;
1076    }
1077
1078    @Nonnull
1079    private Map<String, ElementAccessManager> getElementAccessManagers(final String username) {
1080        if (StringUtils.isBlank(username)) {
1081            return Collections.emptyMap();
1082        }
1083        final String                            cacheId                     = getCacheIdForUserElementAccessManagers(username);
1084        final Map<String, ElementAccessManager> cachedElementAccessManagers = getCachedMap(cache

Large files files are truncated, but you can click here to view the full file