// 
// Decompiled by Procyon v0.6.0
// 

package com.nimbusds.jose.jwk.source;

import java.io.IOException;
import java.util.concurrent.TimeUnit;
import com.nimbusds.jose.util.events.Event;
import com.nimbusds.jose.KeySourceException;
import com.nimbusds.jose.util.cache.CachedObject;
import com.nimbusds.jose.jwk.JWKSet;
import java.util.Objects;
import java.util.concurrent.Executors;
import com.nimbusds.jose.util.events.EventListener;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.locks.ReentrantLock;
import com.nimbusds.jose.shaded.jcip.ThreadSafe;
import com.nimbusds.jose.proc.SecurityContext;

@ThreadSafe
public class RefreshAheadCachingJWKSetSource<C extends SecurityContext> extends CachingJWKSetSource<C>
{
    private final long refreshAheadTime;
    private final ReentrantLock lazyLock;
    private final ExecutorService executorService;
    private final boolean shutdownExecutorOnClose;
    private final ScheduledExecutorService scheduledExecutorService;
    private final boolean shutdownScheduledExecutorOnClose;
    private volatile long cacheExpiration;
    private ScheduledFuture<?> scheduledRefreshFuture;
    private final EventListener<CachingJWKSetSource<C>, C> eventListener;
    
    public static ExecutorService createDefaultExecutorService() {
        return Executors.newSingleThreadExecutor();
    }
    
    public static ScheduledExecutorService createDefaultScheduledExecutorService() {
        return Executors.newSingleThreadScheduledExecutor();
    }
    
    public RefreshAheadCachingJWKSetSource(final JWKSetSource<C> source, final long timeToLive, final long cacheRefreshTimeout, final long refreshAheadTime, final boolean scheduled, final EventListener<CachingJWKSetSource<C>, C> eventListener) {
        this(source, timeToLive, cacheRefreshTimeout, refreshAheadTime, createDefaultExecutorService(), true, eventListener, scheduled ? createDefaultScheduledExecutorService() : null, scheduled);
    }
    
    public RefreshAheadCachingJWKSetSource(final JWKSetSource<C> source, final long timeToLive, final long cacheRefreshTimeout, final long refreshAheadTime, final boolean scheduled, final ExecutorService executorService, final boolean shutdownExecutorOnClose, final EventListener<CachingJWKSetSource<C>, C> eventListener) {
        this(source, timeToLive, cacheRefreshTimeout, refreshAheadTime, executorService, shutdownExecutorOnClose, eventListener, scheduled ? createDefaultScheduledExecutorService() : null, scheduled);
    }
    
    public RefreshAheadCachingJWKSetSource(final JWKSetSource<C> source, final long timeToLive, final long cacheRefreshTimeout, final long refreshAheadTime, final ExecutorService executorService, final boolean shutdownExecutorOnClose, final EventListener<CachingJWKSetSource<C>, C> eventListener, final ScheduledExecutorService scheduledExecutorService, final boolean shutdownScheduledExecutorOnClose) {
        super(source, timeToLive, cacheRefreshTimeout, eventListener);
        this.lazyLock = new ReentrantLock();
        if (refreshAheadTime + cacheRefreshTimeout > timeToLive) {
            throw new IllegalArgumentException("The sum of the refresh-ahead time (" + refreshAheadTime + "ms) and the cache refresh timeout (" + cacheRefreshTimeout + "ms) must not exceed the time-to-lived time (" + timeToLive + "ms)");
        }
        this.refreshAheadTime = refreshAheadTime;
        Objects.requireNonNull(executorService, "The executor service must not be null");
        this.executorService = executorService;
        this.shutdownExecutorOnClose = shutdownExecutorOnClose;
        this.shutdownScheduledExecutorOnClose = shutdownScheduledExecutorOnClose;
        this.scheduledExecutorService = scheduledExecutorService;
        this.eventListener = eventListener;
    }
    
    @Override
    public JWKSet getJWKSet(final JWKSetCacheRefreshEvaluator refreshEvaluator, final long currentTime, final C context) throws KeySourceException {
        final CachedObject<JWKSet> cache = this.getCachedJWKSet();
        if (cache == null) {
            return this.loadJWKSetBlocking(JWKSetCacheRefreshEvaluator.noRefresh(), currentTime, context);
        }
        final JWKSet jwkSet = cache.get();
        if (refreshEvaluator.requiresRefresh(jwkSet)) {
            return this.loadJWKSetBlocking(refreshEvaluator, currentTime, context);
        }
        if (cache.isExpired(currentTime)) {
            return this.loadJWKSetBlocking(JWKSetCacheRefreshEvaluator.referenceComparison(jwkSet), currentTime, context);
        }
        this.refreshAheadOfExpiration(cache, false, currentTime, context);
        return cache.get();
    }
    
    @Override
    CachedObject<JWKSet> loadJWKSetNotThreadSafe(final JWKSetCacheRefreshEvaluator refreshEvaluator, final long currentTime, final C context) throws KeySourceException {
        final CachedObject<JWKSet> cache = super.loadJWKSetNotThreadSafe(refreshEvaluator, currentTime, context);
        if (this.scheduledExecutorService != null) {
            this.scheduleRefreshAheadOfExpiration(cache, currentTime, context);
        }
        return cache;
    }
    
    void scheduleRefreshAheadOfExpiration(final CachedObject<JWKSet> cache, final long currentTime, final C context) {
        if (this.scheduledRefreshFuture != null) {
            this.scheduledRefreshFuture.cancel(false);
        }
        final long delay = cache.getExpirationTime() - currentTime - this.refreshAheadTime - this.getCacheRefreshTimeout();
        if (delay > 0L) {
            final RefreshAheadCachingJWKSetSource<C> that = this;
            final Runnable command = new Runnable() {
                @Override
                public void run() {
                    try {
                        RefreshAheadCachingJWKSetSource.this.refreshAheadOfExpiration(cache, true, System.currentTimeMillis(), context);
                    }
                    catch (final Exception e) {
                        if (RefreshAheadCachingJWKSetSource.this.eventListener != null) {
                            RefreshAheadCachingJWKSetSource.this.eventListener.notify(new ScheduledRefreshFailed(that, e, context));
                        }
                    }
                }
            };
            this.scheduledRefreshFuture = this.scheduledExecutorService.schedule(command, delay, TimeUnit.MILLISECONDS);
            if (this.eventListener != null) {
                this.eventListener.notify((Event<CachingJWKSetSource<C>, C>)new RefreshScheduledEvent((RefreshAheadCachingJWKSetSource<SecurityContext>)this, context));
            }
        }
        else if (this.eventListener != null) {
            this.eventListener.notify((Event<CachingJWKSetSource<C>, C>)new RefreshNotScheduledEvent((RefreshAheadCachingJWKSetSource<SecurityContext>)this, context));
        }
    }
    
    void refreshAheadOfExpiration(final CachedObject<JWKSet> cache, final boolean forceRefresh, final long currentTime, final C context) {
        if ((cache.isExpired(currentTime + this.refreshAheadTime) || forceRefresh) && this.cacheExpiration < cache.getExpirationTime() && this.lazyLock.tryLock()) {
            try {
                this.lockedRefresh(cache, currentTime, context);
            }
            finally {
                this.lazyLock.unlock();
            }
        }
    }
    
    void lockedRefresh(final CachedObject<JWKSet> cache, final long currentTime, final C context) {
        if (this.cacheExpiration < cache.getExpirationTime()) {
            this.cacheExpiration = cache.getExpirationTime();
            final RefreshAheadCachingJWKSetSource<C> that = this;
            final Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        if (RefreshAheadCachingJWKSetSource.this.eventListener != null) {
                            RefreshAheadCachingJWKSetSource.this.eventListener.notify(new ScheduledRefreshInitiatedEvent((CachingJWKSetSource)that, context));
                        }
                        final JWKSet jwkSet = RefreshAheadCachingJWKSetSource.this.loadJWKSetBlocking(JWKSetCacheRefreshEvaluator.forceRefresh(), currentTime, context);
                        if (RefreshAheadCachingJWKSetSource.this.eventListener != null) {
                            RefreshAheadCachingJWKSetSource.this.eventListener.notify(new ScheduledRefreshCompletedEvent((CachingJWKSetSource)that, jwkSet, context));
                        }
                    }
                    catch (final Throwable e) {
                        RefreshAheadCachingJWKSetSource.this.cacheExpiration = -1L;
                        if (RefreshAheadCachingJWKSetSource.this.eventListener != null) {
                            RefreshAheadCachingJWKSetSource.this.eventListener.notify(new UnableToRefreshAheadOfExpirationEvent(that, context));
                        }
                    }
                }
            };
            this.executorService.execute(runnable);
        }
    }
    
    public ExecutorService getExecutorService() {
        return this.executorService;
    }
    
    public ScheduledExecutorService getScheduledExecutorService() {
        return this.scheduledExecutorService;
    }
    
    ReentrantLock getLazyLock() {
        return this.lazyLock;
    }
    
    ScheduledFuture<?> getScheduledRefreshFuture() {
        return this.scheduledRefreshFuture;
    }
    
    @Override
    public void close() throws IOException {
        final ScheduledFuture<?> currentScheduledRefreshFuture = this.scheduledRefreshFuture;
        if (currentScheduledRefreshFuture != null) {
            currentScheduledRefreshFuture.cancel(true);
        }
        super.close();
        if (this.shutdownExecutorOnClose) {
            this.executorService.shutdownNow();
            try {
                this.executorService.awaitTermination(this.getCacheRefreshTimeout(), TimeUnit.MILLISECONDS);
            }
            catch (final InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        if (this.scheduledExecutorService != null && this.shutdownScheduledExecutorOnClose) {
            this.scheduledExecutorService.shutdownNow();
            try {
                this.scheduledExecutorService.awaitTermination(this.getCacheRefreshTimeout(), TimeUnit.MILLISECONDS);
            }
            catch (final InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
    
    public static class RefreshScheduledEvent<C extends SecurityContext> extends AbstractJWKSetSourceEvent<CachingJWKSetSource<C>, C>
    {
        public RefreshScheduledEvent(final RefreshAheadCachingJWKSetSource<C> source, final C context) {
            super(source, context);
        }
    }
    
    public static class RefreshNotScheduledEvent<C extends SecurityContext> extends AbstractJWKSetSourceEvent<CachingJWKSetSource<C>, C>
    {
        public RefreshNotScheduledEvent(final RefreshAheadCachingJWKSetSource<C> source, final C context) {
            super(source, context);
        }
    }
    
    public static class ScheduledRefreshFailed<C extends SecurityContext> extends AbstractJWKSetSourceEvent<CachingJWKSetSource<C>, C>
    {
        private final Exception exception;
        
        public ScheduledRefreshFailed(final CachingJWKSetSource<C> source, final Exception exception, final C context) {
            super(source, context);
            Objects.requireNonNull(exception);
            this.exception = exception;
        }
        
        public Exception getException() {
            return this.exception;
        }
    }
    
    public static class ScheduledRefreshInitiatedEvent<C extends SecurityContext> extends AbstractJWKSetSourceEvent<CachingJWKSetSource<C>, C>
    {
        private ScheduledRefreshInitiatedEvent(final CachingJWKSetSource<C> source, final C context) {
            super(source, context);
        }
    }
    
    public static class ScheduledRefreshCompletedEvent<C extends SecurityContext> extends AbstractJWKSetSourceEvent<CachingJWKSetSource<C>, C>
    {
        private final JWKSet jwkSet;
        
        private ScheduledRefreshCompletedEvent(final CachingJWKSetSource<C> source, final JWKSet jwkSet, final C context) {
            super(source, context);
            Objects.requireNonNull(jwkSet);
            this.jwkSet = jwkSet;
        }
        
        public JWKSet getJWKSet() {
            return this.jwkSet;
        }
    }
    
    public static class UnableToRefreshAheadOfExpirationEvent<C extends SecurityContext> extends AbstractJWKSetSourceEvent<CachingJWKSetSource<C>, C>
    {
        public UnableToRefreshAheadOfExpirationEvent(final CachingJWKSetSource<C> source, final C context) {
            super(source, context);
        }
    }
}
