package org.pac4j.core.credentials.authenticator;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.pac4j.core.context.WebContext;
import org.pac4j.core.credentials.Credentials;
import org.pac4j.core.exception.CredentialsException;
import org.pac4j.core.exception.HttpAction;
import org.pac4j.core.profile.CommonProfile;
import org.pac4j.core.util.CommonHelper;
import org.pac4j.core.util.InitializableWebObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;

/**
 * An authenticator that caches the result of an authentication event locally.
 * The authentication is delegated to a pluggable component.
 *
 * Add the <code>guava</code> dependency to use this class.
 *
 * @author Misagh Moayyed
 * @since 1.8
 */
public class LocalCachingAuthenticator<T extends Credentials> extends InitializableWebObject implements Authenticator<T> {

    protected final Logger logger = LoggerFactory.getLogger(getClass());

    private Authenticator<T> delegate;
    private long cacheSize;
    private long timeout;
    private TimeUnit timeUnit;

    private Cache<T, CommonProfile> cache;

    public LocalCachingAuthenticator() {}

    public LocalCachingAuthenticator(final Authenticator<T> delegate, final long cacheSize,
                                     final long timeout, final TimeUnit timeUnit) {
        this.delegate = delegate;
        this.cacheSize = cacheSize;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
    }

    @Override
    public void validate(final T credentials, final WebContext context) throws HttpAction, CredentialsException {
        init(context);

        try {
            final CommonProfile profile = this.cache.get(credentials, () -> {
                logger.debug("Delegating authentication to {}...", delegate);
                delegate.validate(credentials, context);
                return credentials.getUserProfile();
            });
            credentials.setUserProfile(profile);
            logger.debug("Found cached credential. Using cached profile {}...", profile);
        } catch (Exception e) {
            throw new CredentialsException(e);
        }

    }

    @Override
    protected void internalInit(final WebContext context) {
        CommonHelper.assertNotNull("delegate", this.delegate);
        CommonHelper.assertTrue(cacheSize > 0, "cacheSize must be > 0");
        CommonHelper.assertTrue(timeout > 0, "timeout must be > 0");
        CommonHelper.assertNotNull("timeUnit", this.timeUnit);

        if (delegate instanceof InitializableWebObject) {
            ((InitializableWebObject) delegate).init(context);
        }

        this.cache = CacheBuilder.newBuilder().maximumSize(cacheSize)
                .expireAfterWrite(timeout, timeUnit).build();
    }


    public void removeFromCache(final T credentials) {
        this.cache.invalidate(credentials);
    }

    public boolean isCached(final T credentials) {
        return this.cache.getIfPresent(credentials) != null;
    }

    public boolean clearCache() {
        this.cache.invalidateAll();
        return this.cache.asMap().isEmpty();
    }

    public Authenticator<T> getDelegate() {
        return delegate;
    }

    public void setDelegate(Authenticator<T> delegate) {
        this.delegate = delegate;
    }

    public long getCacheSize() {
        return cacheSize;
    }

    public void setCacheSize(long cacheSize) {
        this.cacheSize = cacheSize;
    }

    public long getTimeout() {
        return timeout;
    }

    public void setTimeout(long timeout) {
        this.timeout = timeout;
    }

    public TimeUnit getTimeUnit() {
        return timeUnit;
    }

    public void setTimeUnit(TimeUnit timeUnit) {
        this.timeUnit = timeUnit;
    }

    @Override
    public String toString() {
        return CommonHelper.toString(this.getClass(), "delegate", this.delegate, "cacheSize", this.cacheSize,
                "timeout", this.timeout, "timeUnit", this.timeUnit);
    }
}

@@ -1695,32 +1695,8 @@
     
-try %7B%0A            final 
 Comm
@@ -1729,16 +1729,25 @@
 ache.get
+IfPresent
 (credent
@@ -1754,48 +1754,97 @@
 ials
-, () -%3E %7B%0A                logger.debug(%22
+);%0A        if (profile == null) %7B%0A            logger.debug(%22No cached credentials found. 
 Dele
@@ -1883,28 +1883,24 @@
  delegate);%0A
-    
             
@@ -1952,26 +1952,25 @@
         
-    return
+profile =
  credent
@@ -2008,11 +2008,135 @@
     
-%7D);
+logger.debug(%22Caching credential. Using profile %7B%7D...%22, profile);%0A            cache.put(credentials, profile);%0A        %7D else %7B
 %0A   
@@ -2263,32 +2263,32 @@
 ...%22, profile);%0A
+
         %7D catch 
@@ -2284,88 +2284,8 @@
    %7D
- catch (Exception e) %7B%0A            throw new CredentialsException(e);%0A        %7D%0A
 %0A   

Improve exception handling in caching authenticator