org.jasig.cas.authentication.principal.CachingPrincipalAttributesRepository.java Source code

Java tutorial

Introduction

Here is the source code for org.jasig.cas.authentication.principal.CachingPrincipalAttributesRepository.java

Source

/*
 * Licensed to Apereo under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Apereo licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License.  You may obtain a
 * copy of the License at the following location:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.jasig.cas.authentication.principal;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.jasig.services.persondir.IPersonAttributeDao;
import org.jasig.services.persondir.IPersonAttributes;
import org.jasig.services.persondir.support.merger.IAttributeMerger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PreDestroy;
import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.expiry.CreatedExpiryPolicy;
import javax.cache.expiry.Duration;
import javax.validation.constraints.NotNull;
import java.io.Closeable;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * Wrapper around an attribute repository where attributes cached for a configurable period.
 * @author Misagh Moayyed
 * @since 4.1
 */
public final class CachingPrincipalAttributesRepository implements PrincipalAttributesRepository, Closeable {
    private static final long serialVersionUID = 6350244643948535906L;

    private static final TimeUnit DEFAULT_CACHE_EXPIRATION_UNIT = TimeUnit.HOURS;
    private static final long DEFAULT_CACHE_EXPIRATION_DURATION = 2;

    private static final Logger LOGGER = LoggerFactory.getLogger(CachingPrincipalAttributesRepository.class);

    private final IPersonAttributeDao attributeRepository;

    private final Cache<String, Map<String, Object>> cache;

    private final String cacheName = this.getClass().getSimpleName().concat(UUID.randomUUID().toString());

    private Duration duration;

    /**
     * The merging strategy that deals with existing principal attributes
     * and those that are retrieved from the source. By default, existing attributes
     * are ignored and the source is always consulted.
     */
    private IAttributeMerger mergingStrategy;

    /**
     * Init the caching repository, solely used for serialization purposes
     * and nothing else.
     */
    private CachingPrincipalAttributesRepository() {
        this.attributeRepository = null;
        this.cache = null;
        this.duration = null;
    }

    /**
     * Instantiates a new caching attributes principal factory.
     * Caches the attributes based on duration units of {@link #DEFAULT_CACHE_EXPIRATION_DURATION}
     * and {@link #DEFAULT_CACHE_EXPIRATION_UNIT}.
     * @param attributeRepository the attribute repository
     */
    public CachingPrincipalAttributesRepository(final IPersonAttributeDao attributeRepository) {
        this(attributeRepository, DEFAULT_CACHE_EXPIRATION_DURATION);
    }

    /**
     * Instantiates a new caching attributes principal factory.
     * Caches the attributes based on duration units of {@link #DEFAULT_CACHE_EXPIRATION_DURATION}
     * and the given time.
     * @param attributeRepository the attribute repository
     * @param expiryDuration the expiry duration based on the unit of {@link #DEFAULT_CACHE_EXPIRATION_DURATION}
     */
    public CachingPrincipalAttributesRepository(final IPersonAttributeDao attributeRepository,
            final long expiryDuration) {
        this(attributeRepository, DEFAULT_CACHE_EXPIRATION_UNIT, expiryDuration);
    }

    /**
     * Instantiates a new caching attributes principal factory.
     *
     * @param attributeRepository the attribute repository
     * @param timeUnit the time unit
     * @param expiryDuration the expiry duration
     */
    public CachingPrincipalAttributesRepository(final IPersonAttributeDao attributeRepository,
            final TimeUnit timeUnit, final long expiryDuration) {
        this(attributeRepository, new Duration(timeUnit, expiryDuration));
    }

    /**
     * Instantiates a new caching attributes principal factory.
     *
     * @param attributeRepository the attribute repository
     * @param duration the duration
     */
    private CachingPrincipalAttributesRepository(final IPersonAttributeDao attributeRepository,
            final Duration duration) {
        this(attributeRepository, createCacheConfiguration(duration));
        this.duration = duration;
    }

    /**
     * Instantiates a new caching attributes principal factory.
     *
     * @param attributeRepository the attribute repository
     * @param config the config
     */
    private CachingPrincipalAttributesRepository(final IPersonAttributeDao attributeRepository,
            final MutableConfiguration<String, Map<String, Object>> config) {
        this(attributeRepository, config, Caching.getCachingProvider().getCacheManager());
    }

    /**
     * Instantiates a new caching attributes principal factory.
     *
     * @param attributeRepository the attribute repository
     * @param config the config
     * @param cacheProviderFullClassName the cache provider full class name
     */
    private CachingPrincipalAttributesRepository(final IPersonAttributeDao attributeRepository,
            final MutableConfiguration<String, Map<String, Object>> config,
            final String cacheProviderFullClassName) {
        this(attributeRepository, config, Caching.getCachingProvider(cacheProviderFullClassName).getCacheManager());
    }

    /**
     * Instantiates a new caching attributes principal factory.
     *
     * @param attributeRepository the attribute repository
     * @param config the config
     * @param manager the manager
     */
    private CachingPrincipalAttributesRepository(final IPersonAttributeDao attributeRepository,
            final MutableConfiguration<String, Map<String, Object>> config, final CacheManager manager) {
        this.attributeRepository = attributeRepository;
        this.cache = manager.createCache(this.cacheName, config);
    }

    /**
     * Instantiates a new caching attributes principal factory.
     *
     * @param attributeRepository the attribute repository
     * @param cache the cache
     */
    private CachingPrincipalAttributesRepository(final IPersonAttributeDao attributeRepository,
            final Cache<String, Map<String, Object>> cache) {
        this.attributeRepository = attributeRepository;
        this.cache = cache;
    }

    /**
     * Gets attribute repository.
     *
     * @return the attribute repository
     */
    public IPersonAttributeDao getAttributeRepository() {
        return this.attributeRepository;
    }

    public Duration getDuration() {
        return this.duration;
    }

    /**
     * Gets cache configuration.
     *
     * @return the configuration
     */
    @JsonIgnore
    public MutableConfiguration<String, Map<String, Object>> getConfiguration() {
        return this.cache.getConfiguration(MutableConfiguration.class);
    }

    /**
     * The merging strategy that deals with existing principal attributes
     * and those that are retrieved from the source. By default, existing attributes
     * are ignored and the source is always consulted.
     * @param mergingStrategy the strategy to use for conflicts
     */
    public void setMergingStrategy(final IAttributeMerger mergingStrategy) {
        this.mergingStrategy = mergingStrategy;
    }

    public IAttributeMerger getMergingStrategy() {
        return mergingStrategy;
    }

    /**
     * Prep cache configuration.
     *
     * @param expiryDuration the expiry duration
     * @return the mutable configuration
     */
    protected static MutableConfiguration<String, Map<String, Object>> createCacheConfiguration(
            final Duration expiryDuration) {
        final MutableConfiguration<String, Map<String, Object>> config = new MutableConfiguration<>();
        config.setStatisticsEnabled(true);
        config.setManagementEnabled(true);
        config.setStoreByValue(true);
        config.setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(expiryDuration));
        return config;
    }

    /**
     * Set/add the received attributes into the cache.
     * @param id the principal id that controls the grouping of the attributes in the cache.
     * @param attributes principal attributes to add to the cache
     */
    private void addPrincipalAttributesIntoCache(final String id, final Map<String, Object> attributes) {
        synchronized (this.cache) {
            if (attributes.isEmpty()) {

                this.cache.remove(id);
                LOGGER.debug("No attributes are provided, so removed principal id [{}] from the cache", id);
            } else {
                this.cache.put(id, attributes);
                LOGGER.debug("Cached [{}] attributes for the principal id [{}]", attributes.size(), id);
            }
        }
    }

    @Override
    public Map<String, Object> getAttributes(@NotNull final Principal p) {
        final Map<String, Object> cachedAttributes = this.cache.get(p.getId());
        if (cachedAttributes != null) {
            LOGGER.debug("Found [{}] cached attributes for principal [{}]", cachedAttributes.size(), p.getId());
            return cachedAttributes;
        }

        final Map<String, List<Object>> sourceAttributes = retrievePersonAttributesToPrincipalAttributes(p.getId());
        LOGGER.debug("Found [{}] attributes for principal [{}] from the attribute repository.",
                sourceAttributes.size(), p.getId());

        if (this.mergingStrategy == null) {
            LOGGER.debug(
                    "No merging strategy found, so attributes retrieved from the repository will be used instead.");
            final Map<String, Object> finalAttributes = convertPersonAttributesToPrincipalAttributes(
                    sourceAttributes);
            addPrincipalAttributesIntoCache(p.getId(), finalAttributes);
            return finalAttributes;
        }

        final Map<String, List<Object>> principalAttributes = convertPrincipalAttributesToPersonAttributes(p);

        LOGGER.debug("Merging current principal attributes with that of the repository via strategy [{}]",
                this.mergingStrategy.getClass().getSimpleName());
        final Map<String, List<Object>> mergedAttributes = this.mergingStrategy.mergeAttributes(principalAttributes,
                sourceAttributes);

        final Map<String, Object> finalAttributes = convertPersonAttributesToPrincipalAttributes(mergedAttributes);
        addPrincipalAttributesIntoCache(p.getId(), finalAttributes);
        return finalAttributes;

    }

    /***
     * Convert principal attributes to person attributes.
     * @param p  the principal carrying attributes
     * @return person attributes
     */
    private Map<String, List<Object>> convertPrincipalAttributesToPersonAttributes(final Principal p) {
        final Map<String, List<Object>> convertedAttributes = new HashMap<>(p.getAttributes().size());
        final Map<String, Object> principalAttributes = p.getAttributes();

        for (final Map.Entry<String, Object> entry : principalAttributes.entrySet()) {
            final Object values = entry.getValue();
            final String key = entry.getKey();
            if (values instanceof List) {
                convertedAttributes.put(key, (List) values);
            } else {
                convertedAttributes.put(key, Collections.singletonList(values));
            }
        }
        return convertedAttributes;
    }

    /**
     * Convert person attributes to principal attributes.
     * @param attributes person attributes
     * @return principal attributes
     */
    private Map<String, Object> convertPersonAttributesToPrincipalAttributes(
            final Map<String, List<Object>> attributes) {
        final Map<String, Object> convertedAttributes = new HashMap<>();
        for (final Map.Entry<String, List<Object>> entry : attributes.entrySet()) {
            final List<Object> values = entry.getValue();
            convertedAttributes.put(entry.getKey(), values.size() == 1 ? values.get(0) : values);
        }
        return convertedAttributes;
    }

    /**
     * Obtains attributes first from the repository by calling
     * {@link org.jasig.services.persondir.IPersonAttributeDao#getPerson(String)}.
     *
     * @param id the person id to locate in the attribute repository
     * @return the map of attributes
     */
    private Map<String, List<Object>> retrievePersonAttributesToPrincipalAttributes(final String id) {

        final IPersonAttributes attrs = this.attributeRepository.getPerson(id);
        if (attrs == null) {
            LOGGER.debug("Could not find principal [{}] in the repository so no attributes are returned.", id);
            return Collections.emptyMap();
        }

        final Map<String, List<Object>> attributes = attrs.getAttributes();
        if (attributes == null) {
            LOGGER.debug("Principal [{}] has no attributes and so none are returned.", id);
            return Collections.emptyMap();
        }
        return attributes;
    }

    @Override
    @PreDestroy
    public void close() throws IOException {
        this.cache.close();
        this.cache.getCacheManager().close();
    }

    @Override
    protected void finalize() throws Throwable {
        close();
        super.finalize();
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this).append("attributeRepository", attributeRepository).append("cache", cache)
                .append("cacheName", cacheName).append("durationTimeUnit", duration.getTimeUnit())
                .append("durationAmount", duration.getDurationAmount()).append("mergingStrategy", mergingStrategy)
                .toString();
    }

    @Override
    public boolean equals(final Object obj) {
        if (obj == null) {
            return false;
        }
        if (obj == this) {
            return true;
        }
        if (obj.getClass() != getClass()) {
            return false;
        }
        final CachingPrincipalAttributesRepository rhs = (CachingPrincipalAttributesRepository) obj;
        final EqualsBuilder builder = new EqualsBuilder();
        return builder.append(this.duration, rhs.duration).isEquals();
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder(13, 133).append(duration).toHashCode();
    }

}