/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF 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
 *
 *     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.apache.shiro.session.mgt.quartz;

import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleTrigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.StdSchedulerFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.shiro.session.mgt.DefaultSessionManager;
import org.apache.shiro.session.mgt.SessionValidationScheduler;
import org.apache.shiro.session.mgt.ValidatingSessionManager;

import static org.quartz.SimpleScheduleBuilder.simpleSchedule;

/**
 * An implementation of the {@link org.apache.shiro.session.mgt.SessionValidationScheduler SessionValidationScheduler}
 * that uses Quartz to schedule a job to call {@link org.apache.shiro.session.mgt.ValidatingSessionManager#validateSessions()} on
 * a regular basis.
 *
 * @since 0.1
 */
public class QuartzSessionValidationScheduler implements SessionValidationScheduler {

    //TODO - complete JavaDoc

    /*--------------------------------------------
    |             C O N S T A N T S             |
    ============================================*/
    /**
     * The default interval at which sessions will be validated (1 hour);
     * This can be overridden by calling {@link #setSessionValidationInterval(long)}
     */
    public static final long DEFAULT_SESSION_VALIDATION_INTERVAL = DefaultSessionManager.DEFAULT_SESSION_VALIDATION_INTERVAL;

    /**
     * The name assigned to the quartz job.
     */
    private static final String JOB_NAME = "SessionValidationJob";

    /*--------------------------------------------
    |    I N S T A N C E   V A R I A B L E S    |
    ============================================*/
    private static final Logger LOGGER = LoggerFactory.getLogger(QuartzSessionValidationScheduler.class);

    /**
     * The configured Quartz scheduler to use to schedule the Quartz job.  If no scheduler is
     * configured, the scheduler will be retrieved by calling {@link StdSchedulerFactory#getDefaultScheduler()}
     */
    private Scheduler scheduler;

    private boolean schedulerImplicitlyCreated;

    private boolean enabled;

    /**
     * The session manager used to validate sessions.
     */
    private ValidatingSessionManager sessionManager;

    /**
     * The session validation interval in milliseconds.
     */
    private long sessionValidationInterval = DEFAULT_SESSION_VALIDATION_INTERVAL;

    /*--------------------------------------------
    |         C O N S T R U C T O R S           |
    ============================================*/

    /**
     * Default constructor.
     */
    public QuartzSessionValidationScheduler() {
    }

    /**
     * Constructor that specifies the session manager that should be used for validating sessions.
     *
     * @param sessionManager the <tt>SessionManager</tt> that should be used to validate sessions.
     */
    public QuartzSessionValidationScheduler(ValidatingSessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }

    /*--------------------------------------------
    |  A C C E S S O R S / M O D I F I E R S    |
    ============================================*/

    protected Scheduler getScheduler() throws SchedulerException {
        if (scheduler == null) {
            scheduler = StdSchedulerFactory.getDefaultScheduler();
            schedulerImplicitlyCreated = true;
        }
        return scheduler;
    }

    public void setScheduler(Scheduler scheduler) {
        this.scheduler = scheduler;
    }

    public void setSessionManager(ValidatingSessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }

    public boolean isEnabled() {
        return this.enabled;
    }

    /**
     * Specifies how frequently (in milliseconds) this Scheduler will call the
     * {@link ValidatingSessionManager#validateSessions() ValidatingSessionManager#validateSessions()} method.
     *
     * <p>Unless this method is called, the default value is {@link #DEFAULT_SESSION_VALIDATION_INTERVAL}.
     *
     * @param sessionValidationInterval
     */
    public void setSessionValidationInterval(long sessionValidationInterval) {
        this.sessionValidationInterval = sessionValidationInterval;
    }

    /*--------------------------------------------
    |               M E T H O D S               |
    ============================================*/

    /**
     * Starts session validation by creating a Quartz simple trigger, linking it to
     * the {@link QuartzSessionValidationJob}, and scheduling it with the Quartz scheduler.
     */
    public void enableSessionValidation() {

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Scheduling session validation job using Quartz with "
                    + "session validation interval of [" + sessionValidationInterval + "]ms...");
        }

        try {
            TriggerBuilder<SimpleTrigger> triggerBuilder =
                    TriggerBuilder.newTrigger()
                            .withIdentity(getClass().getName(), Scheduler.DEFAULT_GROUP)
                            .withSchedule(simpleSchedule()
                                    .withIntervalInMilliseconds(sessionValidationInterval)
                                    .withRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY));
            SimpleTrigger trigger = triggerBuilder.build();

            JobDetail detail = JobBuilder.newJob(QuartzSessionValidationJob.class)
                    .withIdentity(JOB_NAME, Scheduler.DEFAULT_GROUP).build();
            detail.getJobDataMap().put(QuartzSessionValidationJob.SESSION_MANAGER_KEY, sessionManager);

            Scheduler scheduler = getScheduler();

            scheduler.scheduleJob(detail, trigger);
            if (schedulerImplicitlyCreated) {
                scheduler.start();
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Successfully started implicitly created Quartz Scheduler instance.");
                }
            }
            this.enabled = true;

            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Session validation job successfully scheduled with Quartz.");
            }

        } catch (SchedulerException e) {
            if (LOGGER.isErrorEnabled()) {
                LOGGER.error("Error starting the Quartz session validation job.  Session validation may not occur.", e);
            }
        }
    }

    @SuppressWarnings("checkstyle:NPathComplexity")
    public void disableSessionValidation() {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Stopping Quartz session validation job...");
        }

        Scheduler scheduler;
        try {
            scheduler = getScheduler();
            if (scheduler == null) {
                if (LOGGER.isWarnEnabled()) {
                    LOGGER.warn("getScheduler() method returned a null Quartz scheduler, which is unexpected.  Please "
                            + "check your configuration and/or implementation.  Returning quietly since there is no "
                            + "validation job to remove (scheduler does not exist).");
                }
                return;
            }
        } catch (SchedulerException e) {
            if (LOGGER.isWarnEnabled()) {
                LOGGER.warn("Unable to acquire Quartz Scheduler.  Ignoring and returning (already stopped?)", e);
            }
            return;
        }

        try {
            scheduler.unscheduleJob(TriggerKey.triggerKey(JOB_NAME, Scheduler.DEFAULT_GROUP));
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Quartz session validation job stopped successfully.");
            }
        } catch (SchedulerException e) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Could not cleanly remove SessionValidationJob from Quartz scheduler.  "
                        + "Ignoring and stopping.", e);
            }
        }

        this.enabled = false;

        if (schedulerImplicitlyCreated) {
            try {
                scheduler.shutdown();
            } catch (SchedulerException e) {
                if (LOGGER.isWarnEnabled()) {
                    LOGGER.warn("Unable to cleanly shutdown implicitly created Quartz Scheduler instance.", e);
                }
            } finally {
                setScheduler(null);
                schedulerImplicitlyCreated = false;
            }
        }


    }
}
