org.ejbca.core.ejb.services.ServiceSessionBean.java Source code

Java tutorial

Introduction

Here is the source code for org.ejbca.core.ejb.services.ServiceSessionBean.java

Source

/*************************************************************************
 *                                                                       *
 *  EJBCA Community: The OpenSource Certificate Authority                *
 *                                                                       *
 *  This software is free software; you can redistribute it and/or       *
 *  modify it under the terms of the GNU Lesser General Public           *
 *  License as published by the Free Software Foundation; either         *
 *  version 2.1 of the License, or any later version.                    *
 *                                                                       *
 *  See terms of license at gnu.org.                                     *
 *                                                                       *
 *************************************************************************/

package org.ejbca.core.ejb.services;

import java.io.Serializable;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.ejb.EJB;
import javax.ejb.EJBException;
import javax.ejb.FinderException;
import javax.ejb.SessionContext;
import javax.ejb.Stateless;
import javax.ejb.Timeout;
import javax.ejb.Timer;
import javax.ejb.TimerService;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.cesecore.audit.enums.EventStatus;
import org.cesecore.audit.log.InternalSecurityEventsLoggerSessionLocal;
import org.cesecore.audit.log.SecurityEventsLoggerSessionLocal;
import org.cesecore.authentication.tokens.AlwaysAllowLocalAuthenticationToken;
import org.cesecore.authentication.tokens.AuthenticationToken;
import org.cesecore.authentication.tokens.UsernamePrincipal;
import org.cesecore.authorization.control.AccessControlSessionLocal;
import org.cesecore.authorization.control.StandardRules;
import org.cesecore.certificates.ca.CaSessionLocal;
import org.cesecore.certificates.certificate.CertificateStoreSessionLocal;
import org.cesecore.certificates.certificateprofile.CertificateProfileSessionLocal;
import org.cesecore.certificates.crl.CrlCreateSessionLocal;
import org.cesecore.certificates.crl.CrlStoreSessionLocal;
import org.cesecore.configuration.GlobalConfigurationSessionLocal;
import org.cesecore.jndi.JndiConstants;
import org.cesecore.keys.token.CryptoTokenManagementSessionLocal;
import org.cesecore.util.ProfileID;
import org.ejbca.core.ejb.approval.ApprovalSessionLocal;
import org.ejbca.core.ejb.audit.enums.EjbcaEventTypes;
import org.ejbca.core.ejb.audit.enums.EjbcaModuleTypes;
import org.ejbca.core.ejb.audit.enums.EjbcaServiceTypes;
import org.ejbca.core.ejb.authentication.web.WebAuthenticationProviderSessionLocal;
import org.ejbca.core.ejb.authorization.ComplexAccessControlSessionLocal;
import org.ejbca.core.ejb.ca.auth.EndEntityAuthenticationSessionLocal;
import org.ejbca.core.ejb.ca.caadmin.CAAdminSessionLocal;
import org.ejbca.core.ejb.ca.publisher.PublisherQueueSessionLocal;
import org.ejbca.core.ejb.ca.publisher.PublisherSessionLocal;
import org.ejbca.core.ejb.ca.sign.SignSessionLocal;
import org.ejbca.core.ejb.crl.PublishingCrlSessionLocal;
import org.ejbca.core.ejb.hardtoken.HardTokenSessionLocal;
import org.ejbca.core.ejb.keyrecovery.KeyRecoverySessionLocal;
import org.ejbca.core.ejb.ra.CertificateRequestSessionLocal;
import org.ejbca.core.ejb.ra.EndEntityAccessSessionLocal;
import org.ejbca.core.ejb.ra.EndEntityManagementSessionLocal;
import org.ejbca.core.ejb.ra.raadmin.AdminPreferenceSessionLocal;
import org.ejbca.core.ejb.ra.raadmin.EndEntityProfileSessionLocal;
import org.ejbca.core.model.InternalEjbcaResources;
import org.ejbca.core.model.services.BaseWorker;
import org.ejbca.core.model.services.IInterval;
import org.ejbca.core.model.services.IWorker;
import org.ejbca.core.model.services.ServiceConfiguration;
import org.ejbca.core.model.services.ServiceExecutionFailedException;
import org.ejbca.core.model.services.ServiceExistsException;

/**
 * Session bean that handles adding and editing services as displayed in EJBCA. This bean manages the service configuration as stored in the database,
 * but is not used for running the services.
 * 
 * @version $Id: ServiceSessionBean.java 19968 2014-10-09 13:13:58Z mikekushner $
 */
@Stateless(mappedName = JndiConstants.APP_JNDI_PREFIX + "ServiceSessionRemote")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class ServiceSessionBean implements ServiceSessionLocal, ServiceSessionRemote {

    private static final Logger log = Logger.getLogger(ServiceSessionBean.class);

    /** Internal localization of logs and errors */
    private static final InternalEjbcaResources intres = InternalEjbcaResources.getInstance();

    /**
     * Constant indicating the Id of the "service loader" service. Used in a clustered environment to periodically load available services
     */
    private static final Integer SERVICELOADER_ID = 0;

    private static final long SERVICELOADER_PERIOD = 5 * 60 * 1000;

    @Resource
    private SessionContext sessionContext;
    private TimerService timerService; // When the sessionContext is injected, the timerService should be looked up.

    @EJB
    private AccessControlSessionLocal authorizationSession;
    @EJB
    private SecurityEventsLoggerSessionLocal auditSession;
    @EJB
    private InternalSecurityEventsLoggerSessionLocal internalAuditSession;
    @EJB
    private ServiceDataSessionLocal serviceDataSession;

    private ServiceSessionLocal serviceSession;

    // Additional dependencies from the services we executeServiceInTransaction
    @EJB
    private ApprovalSessionLocal approvalSession;
    @EJB
    private EndEntityAuthenticationSessionLocal authenticationSession;
    @EJB
    private CAAdminSessionLocal caAdminSession;
    @EJB
    private CaSessionLocal caSession;
    @EJB
    private CertificateProfileSessionLocal certificateProfileSession;
    @EJB
    private CertificateStoreSessionLocal certificateStoreSession;
    @EJB
    private CrlCreateSessionLocal crlCreateSession;
    @EJB
    private CrlStoreSessionLocal crlStoreSession;
    @EJB
    private EndEntityAccessSessionLocal endEntityAccessSession;
    @EJB
    private EndEntityProfileSessionLocal endEntityProfileSession;
    @EJB
    private HardTokenSessionLocal hardTokenSession;
    @EJB
    private KeyRecoverySessionLocal keyRecoverySession;
    @EJB
    private AdminPreferenceSessionLocal raAdminSession;
    @EJB
    private GlobalConfigurationSessionLocal globalConfigurationSession;
    @EJB
    private SignSessionLocal signSession;
    @EJB
    private EndEntityManagementSessionLocal endEntityManagementSession;
    @EJB
    private PublisherQueueSessionLocal publisherQueueSession;
    @EJB
    private PublisherSessionLocal publisherSession;
    @EJB
    private CertificateRequestSessionLocal certificateRequestSession;
    @EJB
    private WebAuthenticationProviderSessionLocal webAuthenticationSession;
    @EJB
    private ComplexAccessControlSessionLocal complexAccessControlSession;
    @EJB
    private PublishingCrlSessionLocal publishingCrlSession;
    @EJB
    private CryptoTokenManagementSessionLocal cryptoTokenSession;

    // The administrator that the services should be run as. Internal, allow all.
    private AuthenticationToken intAdmin = new AlwaysAllowLocalAuthenticationToken(
            new UsernamePrincipal("ServiceSession"));

    @PostConstruct
    public void ejbCreate() {
        timerService = sessionContext.getTimerService();
        serviceSession = sessionContext.getBusinessObject(ServiceSessionLocal.class);
    }

    @Override
    public void addService(AuthenticationToken admin, String name, ServiceConfiguration serviceConfiguration)
            throws ServiceExistsException {
        if (log.isTraceEnabled()) {
            log.trace(">addService(name: " + name + ")");
        }
        addService(admin, findFreeServiceId(), name, serviceConfiguration);
        log.trace("<addService()");
    }

    @Override
    public void addService(AuthenticationToken admin, int id, String name,
            ServiceConfiguration serviceConfiguration) throws ServiceExistsException {
        if (log.isTraceEnabled()) {
            log.trace(">addService(name: " + name + ", id: " + id + ")");
        }
        boolean success = addServiceInternal(admin, id, name, serviceConfiguration);
        if (success) {
            final String msg = intres.getLocalizedMessage("services.serviceadded", name);
            final Map<String, Object> details = new LinkedHashMap<String, Object>();
            details.put("msg", msg);
            auditSession.log(EjbcaEventTypes.SERVICE_ADD, EventStatus.SUCCESS, EjbcaModuleTypes.SERVICE,
                    EjbcaServiceTypes.EJBCA, admin.toString(), null, null, null, details);
        } else {
            final String msg = intres.getLocalizedMessage("services.erroraddingservice", name);
            log.info(msg);
            throw new ServiceExistsException(msg);
        }
        log.trace("<addService()");
    }

    private boolean addServiceInternal(AuthenticationToken admin, int id, String name,
            ServiceConfiguration serviceConfiguration) throws ServiceExistsException {
        boolean success = false;
        if (isAuthorizedToEditService(admin)) {
            if (serviceDataSession.findByName(name) == null) {
                if (serviceDataSession.findById(Integer.valueOf(id)) == null) {
                    serviceDataSession.addServiceData(id, name, serviceConfiguration);
                    success = true;
                }
            }
        } else {
            final String msg = intres.getLocalizedMessage("services.notauthorizedtoadd", name);
            log.info(msg);
        }
        return success;
    }

    @Override
    public void cloneService(AuthenticationToken admin, String oldname, String newname)
            throws ServiceExistsException {
        if (log.isTraceEnabled()) {
            log.trace(">cloneService(name: " + oldname + ")");
        }
        ServiceConfiguration servicedata = null;
        ServiceData htp = serviceDataSession.findByName(oldname);
        if (htp == null) {
            String msg = "Error cloning service: No such service found.";
            log.error(msg);
            throw new EJBException(msg);
        }
        try {
            servicedata = (ServiceConfiguration) htp.getServiceConfiguration().clone();
            if (isAuthorizedToEditService(admin)) {
                addServiceInternal(admin, findFreeServiceId(), newname, servicedata);
                final String msg = intres.getLocalizedMessage("services.servicecloned", newname, oldname);
                final Map<String, Object> details = new LinkedHashMap<String, Object>();
                details.put("msg", msg);
                auditSession.log(EjbcaEventTypes.SERVICE_ADD, EventStatus.SUCCESS, EjbcaModuleTypes.SERVICE,
                        EjbcaServiceTypes.EJBCA, admin.toString(), null, null, null, details);
            } else {
                final String msg = intres.getLocalizedMessage("services.notauthorizedtoedit", oldname);
                log.info(msg);
            }
        } catch (CloneNotSupportedException e) {
            log.error("Error cloning service: ", e);
            throw new EJBException(e);
        }
        log.trace("<cloneService()");
    }

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    @Override
    public boolean removeService(AuthenticationToken admin, String name) {
        if (log.isTraceEnabled()) {
            log.trace(">removeService(name: " + name + ")");
        }
        boolean retval = false;
        try {
            ServiceData htp = serviceDataSession.findByName(name);
            if (htp == null) {
                throw new FinderException("Cannot find service " + name);
            }
            ServiceConfiguration serviceConfiguration = htp.getServiceConfiguration();
            if (isAuthorizedToEditService(admin)) {
                IWorker worker = getWorker(serviceConfiguration, name, htp.getRunTimeStamp(),
                        htp.getNextRunTimeStamp());
                if (worker != null) {
                    serviceSession.cancelTimer(htp.getId());
                }
                serviceDataSession.removeServiceData(htp.getId());
                final String msg = intres.getLocalizedMessage("services.serviceremoved", name);
                final Map<String, Object> details = new LinkedHashMap<String, Object>();
                details.put("msg", msg);
                auditSession.log(EjbcaEventTypes.SERVICE_REMOVE, EventStatus.SUCCESS, EjbcaModuleTypes.SERVICE,
                        EjbcaServiceTypes.EJBCA, admin.toString(), null, null, null, details);
                retval = true;
            } else {
                final String msg = intres.getLocalizedMessage("services.notauthorizedtoedit", name);
                log.info(msg);
            }
        } catch (Exception e) {
            final String msg = intres.getLocalizedMessage("services.errorremovingservice", name);
            final Map<String, Object> details = new LinkedHashMap<String, Object>();
            details.put("msg", msg);
            details.put("error", e.getMessage());
            auditSession.log(EjbcaEventTypes.SERVICE_REMOVE, EventStatus.FAILURE, EjbcaModuleTypes.SERVICE,
                    EjbcaServiceTypes.EJBCA, admin.toString(), null, null, null, details);
        }
        log.trace("<removeService)");
        return retval;
    }

    @Override
    public void renameService(AuthenticationToken admin, String oldname, String newname)
            throws ServiceExistsException {
        if (log.isTraceEnabled()) {
            log.trace(">renameService(from " + oldname + " to " + newname + ")");
        }
        boolean success = false;
        if (serviceDataSession.findByName(newname) == null) {
            ServiceData htp = serviceDataSession.findByName(oldname);
            if (htp != null) {
                if (isAuthorizedToEditService(admin)) {
                    htp.setName(newname);
                    success = true;
                } else {
                    final String msg = intres.getLocalizedMessage("services.notauthorizedtoedit", oldname);
                    log.info(msg);
                }
            }
        }
        if (success) {
            final String msg = intres.getLocalizedMessage("services.servicerenamed", oldname, newname);
            final Map<String, Object> details = new LinkedHashMap<String, Object>();
            details.put("msg", msg);
            auditSession.log(EjbcaEventTypes.SERVICE_RENAME, EventStatus.SUCCESS, EjbcaModuleTypes.SERVICE,
                    EjbcaServiceTypes.EJBCA, admin.toString(), null, null, null, details);
        } else {
            final String msg = intres.getLocalizedMessage("services.errorrenamingservice", oldname, newname);
            log.info(msg);
            throw new ServiceExistsException(msg);
        }
        log.trace("<renameService()");
    }

    @Override
    public Collection<Integer> getAuthorizedVisibleServiceIds(AuthenticationToken admin) {
        Collection<Integer> allVisibleServiceIds = new ArrayList<Integer>();
        // If superadmin return all visible services
        if (authorizationSession.isAuthorizedNoLogging(admin, StandardRules.ROLE_ROOT.resource())) {
            Collection<Integer> allServiceIds = getServiceIdToNameMap().keySet();
            for (int id : allServiceIds) {
                // Remove hidden services here..
                if (!getServiceConfiguration(admin, id).isHidden()) {
                    allVisibleServiceIds.add(Integer.valueOf(id));
                }
            }
        } else {
            if (log.isDebugEnabled()) {
                log.debug("Authorization denied for admin " + admin + " for resouce " + StandardRules.ROLE_ROOT);
            }
        }
        return allVisibleServiceIds;
    }

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    @Override
    public ServiceConfiguration getService(String name) {
        if (log.isTraceEnabled()) {
            log.trace(">getService: " + name);
        }
        ServiceConfiguration returnval = null;
        ServiceData serviceData = serviceDataSession.findByName(name);
        if (serviceData != null) {
            returnval = serviceData.getServiceConfiguration();
        }
        if (log.isTraceEnabled()) {
            log.trace("<getService: " + name);
        }
        return returnval;
    }

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    @Override
    public int getServiceId(String name) {
        int returnval = 0;
        ServiceData serviceData = serviceDataSession.findByName(name);
        if (serviceData != null) {
            returnval = serviceData.getId();
        }
        return returnval;
    }

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    @Override
    public void activateServiceTimer(AuthenticationToken admin, String name) {
        if (log.isTraceEnabled()) {
            log.trace(">activateServiceTimer(name: " + name + ")");
        }
        ServiceData htp = serviceDataSession.findByName(name);
        if (htp != null) {
            ServiceConfiguration serviceConfiguration = htp.getServiceConfiguration();
            if (isAuthorizedToEditService(admin)) {
                IWorker worker = getWorker(serviceConfiguration, name, htp.getRunTimeStamp(),
                        htp.getNextRunTimeStamp());
                if (worker != null) {
                    serviceSession.cancelTimer(htp.getId());
                    if (serviceConfiguration.isActive() && worker.getNextInterval() != IInterval.DONT_EXECUTE) {
                        addTimer(worker.getNextInterval() * 1000, htp.getId());
                    }
                }
            } else {
                final String msg = intres.getLocalizedMessage("services.notauthorizedtoedit", name);
                log.info(msg);
            }
        } else {
            log.error("Can not find service: " + name);
        }
        log.trace("<activateServiceTimer()");
    }

    private int findFreeServiceId() {
        final ProfileID.DB db = new ProfileID.DB() {
            @Override
            public boolean isFree(int i) {
                return ServiceSessionBean.this.serviceDataSession.findById(Integer.valueOf(i)) == null;
            }
        };
        return ProfileID.getNotUsedID(db);
    }

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    @Override
    public String getServiceName(int id) {
        if (log.isTraceEnabled()) {
            log.trace(">getServiceName(id: " + id + ")");
        }
        String returnval = null;
        ServiceData serviceData = serviceDataSession.findById(id);
        if (serviceData != null) {
            returnval = serviceData.getName();
        }
        if (log.isTraceEnabled()) {
            log.trace("<getServiceName()");
        }
        return returnval;
    }

    /**
     * Method implemented from the TimerObject and is the main method of this session bean. It calls the work object for each object.
     * 
     * @param timer timer whose expiration caused this notification.
     */
    @Timeout
    // Glassfish 2.1.1:
    // "Timeout method ....timeoutHandler(javax.ejb.Timer)must have TX attribute of TX_REQUIRES_NEW or TX_REQUIRED or TX_NOT_SUPPORTED"
    // JBoss 5.1.0.GA: We cannot mix timer updates with our EJBCA DataSource transactions.
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public void timeoutHandler(Timer timer) {
        if (log.isTraceEnabled()) {
            log.trace(">ejbTimeout");
        }
        final long startOfTimeOut = System.currentTimeMillis();
        long serviceInterval = IInterval.DONT_EXECUTE;
        Integer timerInfo = (Integer) timer.getInfo();
        if (timerInfo.equals(SERVICELOADER_ID)) {
            if (log.isDebugEnabled()) {
                log.debug("Running the internal Service loader.");
            }
            load();
        } else {
            String serviceName = null;
            try {
                serviceName = serviceDataSession.findNameById(timerInfo);
            } catch (Throwable t) { // NOPMD: we really need to catch everything to not risk hanging somewhere in limbo
                log.warn("Exception finding service name: ", t); // if this throws, there is a failed database or similar
                // Unexpected error (probably database related). We need to reschedule the service w a default interval.
                addTimer(30 * 1000, timerInfo);
            }
            if (serviceName == null) {
                final String msg = intres.getLocalizedMessage("services.servicenotfound", timerInfo);
                log.info(msg);
            } else {
                // Get interval of worker
                try {
                    serviceInterval = serviceSession.getServiceInterval(timerInfo);
                } catch (Throwable t) { // NOPMD: we really need to catch everything to not risk hanging somewhere in limbo
                    log.warn("Exception getting service interval: ", t); // if this throws, there is a failed database or similar
                    // Unexpected error (probably database related). We need to reschedule the service w a default interval.
                    addTimer(30 * 1000, timerInfo);
                }
                // Reschedule timer
                IWorker worker = null;
                if (serviceInterval != IInterval.DONT_EXECUTE) {
                    Timer nextTrigger = addTimer(serviceInterval * 1000, timerInfo);
                    try {
                        // Try to acquire lock / see if this node should run
                        worker = serviceSession.getWorkerIfItShouldRun(timerInfo,
                                nextTrigger.getNextTimeout().getTime());
                    } catch (Throwable t) { // NOPMD: we really need to catch everything to not risk hanging somewhere in limbo
                        if (log.isDebugEnabled()) {
                            log.debug("Exception: ", t); // Don't spam log with stacktraces in normal production cases
                        }
                    }
                    if (worker != null) {
                        try {
                            serviceSession.executeServiceInNoTransaction(worker, serviceName);
                        } catch (RuntimeException e) {
                            /*
                             * If the service worker fails with a RuntimeException we need to
                             * swallow this here. If we allow it to propagate outside the
                             * ejbTimeout method it is up to the application server config how it
                             * should be retried, but we have already scheduled a new try
                             * previously in this method. We still want to log this as an ERROR
                             * since it is some kind of catastrophic failure..
                             */
                            log.error("Service worker execution failed.", e);
                        }
                    } else {
                        if (log.isDebugEnabled()) {
                            Object o = timerInfo;
                            if (serviceName != null) {
                                o = serviceName;
                            }
                            final String msg = intres.getLocalizedMessage("services.servicerunonothernode", o);
                            log.debug(msg);
                        }
                    }
                    if (System.currentTimeMillis() - startOfTimeOut > serviceInterval * 1000) {
                        log.warn("Service '" + serviceName + "' took longer than it's configured service interval ("
                                + serviceInterval + ")."
                                + " This can trigger simultanious service execution on several nodes in a cluster."
                                + " Increase interval or lower each invocations work load.");
                    }
                }
            }
        }
        if (log.isTraceEnabled()) {
            log.trace("<ejbTimeout");
        }
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    @Override
    public IWorker getWorkerIfItShouldRun(Integer serviceId, long nextTimeout) {
        IWorker worker = null;
        ServiceData serviceData = serviceDataSession.findById(serviceId);
        ServiceConfiguration serviceConfiguration = serviceData.getServiceConfiguration();
        if (!serviceConfiguration.isActive()) {
            if (log.isDebugEnabled()) {
                log.debug("Service " + serviceId + " is inactive.");
            }
            return null; // Don't return an inactive worker to run
        }
        String serviceName = serviceData.getName();
        final String hostname = getHostName();
        if (shouldRunOnThisNode(hostname, Arrays.asList(serviceConfiguration.getPinToNodes()))) {
            long oldRunTimeStamp = serviceData.getRunTimeStamp();
            long oldNextRunTimeStamp = serviceData.getNextRunTimeStamp();
            worker = getWorker(serviceConfiguration, serviceName, oldRunTimeStamp, oldNextRunTimeStamp);
            if (worker.getNextInterval() == IInterval.DONT_EXECUTE) {
                if (log.isDebugEnabled()) {
                    log.debug("Service has interval IInterval.DONT_EXECUTE.");
                }
                return null; // Don't return an inactive worker to run
            }
            Date runDateCheck = new Date(oldNextRunTimeStamp); // nextRunDateCheck will typically be the same (or just a millisecond earlier) as now
                                                               // here
            Date currentDate = new Date();
            if (log.isDebugEnabled()) {
                Date nextRunDate = new Date(nextTimeout);
                log.debug("nextRunDate is:  " + nextRunDate);
                log.debug("runDateCheck is: " + runDateCheck);
                log.debug("currentDate is:  " + currentDate);
            }
            /*
             * Check if the current date is after when the service should run. If a
             * service on another cluster node has updated this timestamp already,
             * then it will return false and this service will not run. This is a
             * semaphore (not the best one admitted) so that services in a cluster
             * only runs on one node and don't compete with each other. If a worker
             * on one node for instance runs for a very long time, there is a chance
             * that another worker on another node will break this semaphore and run
             * as well.
             */
            if (currentDate.after(runDateCheck)) {
                /*
                 * We only update the nextRunTimeStamp if the service is allowed to run on this node.
                 * 
                 * However, we need to make sure that no other node has already acquired the semaphore
                 * if our current database allows non-repeatable reads.
                 */
                if (!serviceDataSession.updateTimestamps(serviceId, oldRunTimeStamp, oldNextRunTimeStamp,
                        runDateCheck.getTime(), nextTimeout)) {
                    log.debug(
                            "Another node had already updated the database at this point. This node will not run.");
                    worker = null; // Failed to update the database.
                }
            } else {
                worker = null; // Don't return a worker, since this node should not run
            }
        } else {
            worker = null;
            if (log.isDebugEnabled()) {
                log.debug("Service " + serviceName + " will not run on this node: \"" + hostname + "\", Pinned to: "
                        + Arrays.toString(serviceConfiguration.getPinToNodes()));
            }
        }
        return worker;
    }

    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    @Override
    public void executeServiceInNoTransaction(IWorker worker, String serviceName) {
        try {
            // Awkward way of letting POJOs get interfaces, but shows dependencies on the EJB level for all used classes. Injection wont work, since
            // we have circular dependencies!
            Map<Class<?>, Object> ejbs = new HashMap<Class<?>, Object>();
            ejbs.put(ApprovalSessionLocal.class, approvalSession);
            ejbs.put(EndEntityAuthenticationSessionLocal.class, authenticationSession);
            ejbs.put(AccessControlSessionLocal.class, authorizationSession);
            ejbs.put(CAAdminSessionLocal.class, caAdminSession);
            ejbs.put(CaSessionLocal.class, caSession);
            ejbs.put(CertificateProfileSessionLocal.class, certificateProfileSession);
            ejbs.put(CertificateStoreSessionLocal.class, certificateStoreSession);
            ejbs.put(CrlCreateSessionLocal.class, crlCreateSession);
            ejbs.put(CrlStoreSessionLocal.class, crlStoreSession);
            ejbs.put(EndEntityProfileSessionLocal.class, endEntityProfileSession);
            ejbs.put(HardTokenSessionLocal.class, hardTokenSession);
            ejbs.put(SecurityEventsLoggerSessionLocal.class, auditSession);
            ejbs.put(InternalSecurityEventsLoggerSessionLocal.class, internalAuditSession);
            ejbs.put(KeyRecoverySessionLocal.class, keyRecoverySession);
            ejbs.put(AdminPreferenceSessionLocal.class, raAdminSession);
            ejbs.put(GlobalConfigurationSessionLocal.class, globalConfigurationSession);
            ejbs.put(SignSessionLocal.class, signSession);
            ejbs.put(EndEntityManagementSessionLocal.class, endEntityManagementSession);
            ejbs.put(PublisherQueueSessionLocal.class, publisherQueueSession);
            ejbs.put(PublisherSessionLocal.class, publisherSession);
            ejbs.put(CertificateRequestSessionLocal.class, certificateRequestSession);
            ejbs.put(EndEntityAccessSessionLocal.class, endEntityAccessSession);
            ejbs.put(WebAuthenticationProviderSessionLocal.class, webAuthenticationSession);
            ejbs.put(ComplexAccessControlSessionLocal.class, complexAccessControlSession);
            ejbs.put(PublishingCrlSessionLocal.class, publishingCrlSession);
            ejbs.put(CryptoTokenManagementSessionLocal.class, cryptoTokenSession);
            worker.work(ejbs);
            final String msg = intres.getLocalizedMessage("services.serviceexecuted", serviceName);
            log.info(msg);
        } catch (ServiceExecutionFailedException e) {
            final String msg = intres.getLocalizedMessage("services.serviceexecutionfailed", serviceName);
            log.info(msg, e);
        }
    }

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    @Override
    public void changeService(AuthenticationToken admin, String name, ServiceConfiguration serviceConfiguration,
            boolean noLogging) {
        if (log.isTraceEnabled()) {
            log.trace(">changeService(name: " + name + ")");
        }
        if (isAuthorizedToEditService(admin)) {
            ServiceData oldservice = serviceDataSession.findByName(name);
            if (oldservice != null) {
                final Map<Object, Object> diff = oldservice.getServiceConfiguration().diff(serviceConfiguration);
                if (serviceDataSession.updateServiceConfiguration(name, serviceConfiguration)) {
                    final String msg = intres.getLocalizedMessage("services.serviceedited", name);
                    if (noLogging) {
                        log.info(msg);
                    } else {
                        final Map<String, Object> details = new LinkedHashMap<String, Object>();
                        details.put("msg", msg);
                        for (Map.Entry<Object, Object> entry : diff.entrySet()) {
                            details.put(entry.getKey().toString(), entry.getValue().toString());
                        }
                        auditSession.log(EjbcaEventTypes.SERVICE_EDIT, EventStatus.SUCCESS,
                                EjbcaModuleTypes.SERVICE, EjbcaServiceTypes.EJBCA, intAdmin.toString(), null, null,
                                null, details);
                    }
                } else {
                    String msg = intres.getLocalizedMessage("services.serviceedited", name);
                    if (noLogging) {
                        log.error(msg);
                    } else {
                        final Map<String, Object> details = new LinkedHashMap<String, Object>();
                        details.put("msg", msg);
                        auditSession.log(EjbcaEventTypes.SERVICE_EDIT, EventStatus.FAILURE,
                                EjbcaModuleTypes.SERVICE, EjbcaServiceTypes.EJBCA, intAdmin.toString(), null, null,
                                null, details);
                    }
                }
            } else {
                log.error("Can not find service to change: " + name);
            }
        } else {
            String msg = intres.getLocalizedMessage("services.notauthorizedtoedit", name);
            log.info(msg);
        }
        log.trace("<changeService()");
    }

    // We don't want the appserver to persist/update the timer in the same transaction if they are stored in different non XA DataSources
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    @Override
    public void load() {
        // Get all services
        Collection<Timer> currentTimers = timerService.getTimers();
        Iterator<Timer> iter = currentTimers.iterator();
        HashSet<Serializable> existingTimers = new HashSet<Serializable>();
        while (iter.hasNext()) {
            Timer timer = iter.next();
            try {
                Serializable info = timer.getInfo();
                existingTimers.add(info);
            } catch (Throwable e) { // NOPMD: we really need to catch everything to not risk hanging somewhere in limbo
                // EJB 2.1 only?: We need this try because weblogic seems to ... suck ...
                log.debug("Error invoking timer.getInfo(): ", e);
            }
        }

        // Get new services and add timeouts
        Map<Integer, Long> newTimeouts = serviceSession.getNewServiceTimeouts(existingTimers);
        for (Integer id : newTimeouts.keySet()) {
            addTimer(newTimeouts.get(id), id);
        }

        if (!existingTimers.contains(SERVICELOADER_ID)) {
            // load the service timer
            addTimer(SERVICELOADER_PERIOD, SERVICELOADER_ID);
        }
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    @Override
    public Map<Integer, Long> getNewServiceTimeouts(HashSet<Serializable> existingTimers) {
        Map<Integer, Long> ret = new HashMap<Integer, Long>();
        HashMap<Integer, String> idToNameMap = getServiceIdToNameMap();
        Collection<Integer> allServices = idToNameMap.keySet();
        Iterator<Integer> iter2 = allServices.iterator();
        while (iter2.hasNext()) {
            Integer id = iter2.next();
            ServiceData htp = serviceDataSession.findById(id);
            if (htp != null) {
                if (!existingTimers.contains(id)) {
                    ServiceConfiguration serviceConfiguration = htp.getServiceConfiguration();
                    IWorker worker = getWorker(serviceConfiguration, idToNameMap.get(id), htp.getRunTimeStamp(),
                            htp.getNextRunTimeStamp());
                    if (worker != null && serviceConfiguration.isActive()
                            && worker.getNextInterval() != IInterval.DONT_EXECUTE) {
                        ret.put(id, Long.valueOf((worker.getNextInterval()) * 1000));
                    }
                }
            } else {
                // Service does not exist, strange, but no panic.
                log.debug("Can not find service with id " + id);
            }
        }
        return ret;
    }

    // We don't want the appserver to persist/update the timer in the same transaction if they are stored in different non XA DataSources
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    @Override
    public void unload() {
        log.debug("Unloading all timers.");
        // Get all services
        for (Timer timer : (Collection<Timer>) timerService.getTimers()) {
            try {
                timer.cancel();
            } catch (Exception e) {
                /*
                 * EJB 2.1 only?: We need to catch this because Weblogic 10
                 * throws an exception if we have not scheduled this timer, so
                 * we don't have anything to cancel. Only weblogic though...
                 */
                log.info("Caught exception canceling timer: " + e.getMessage());
            }
        }
    }

    /**
     * Adds a timer to the bean
     * 
     * @param id the id of the timer
     */
    // We don't want the appserver to persist/update the timer in the same transaction if they are stored in different non XA DataSources. This method
    // should not be run from within a transaction.
    private Timer addTimer(long interval, Integer id) {
        if (log.isDebugEnabled()) {
            log.debug("addTimer: " + id);
        }
        return timerService.createTimer(interval, id);
    }

    /**
     * Cancels all existing timeouts for this id.
     * 
     * @param id the id of the timer
     */
    // We don't want the appserver to persist/update the timer in the same transaction if they are stored in different non XA DataSources. This method
    // should not be run from within a transaction.
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    @Override
    public void cancelTimer(Integer id) {
        if (log.isDebugEnabled()) {
            log.debug("cancelTimer: " + id);
        }
        for (Timer next : (Collection<Timer>) timerService.getTimers()) {
            try {
                if (id.equals(next.getInfo())) {
                    next.cancel();
                    break;
                }
            } catch (Exception e) {
                /*
                 * EJB 2.1 only?: We need to catch this because Weblogic 10
                 * throws an exception if we have not scheduled this timer, so
                 * we don't have anything to cancel. Only weblogic though...
                 */
                log.error("Caught exception canceling timer: " + e.getMessage(), e);
            }
        }
    }

    /**
     * Method that creates a worker from the service configuration.
     * 
     * @param serviceConfiguration
     * @param serviceName
     * @param runTimeStamp the time this service runs
     * @param nextRunTimeStamp the time this service will run next time
     * @return a worker object or null if the worker is misconfigured.
     */
    private IWorker getWorker(ServiceConfiguration serviceConfiguration, String serviceName, long runTimeStamp,
            long nextRunTimeStamp) {
        IWorker worker = null;
        try {
            String clazz = serviceConfiguration.getWorkerClassPath();
            if (StringUtils.isNotEmpty(clazz)) {
                worker = (IWorker) Thread.currentThread().getContextClassLoader().loadClass(clazz).newInstance();
                worker.init(intAdmin, serviceConfiguration, serviceName, runTimeStamp, nextRunTimeStamp);
            } else {
                log.info("Worker has empty classpath for service " + serviceName);
            }
        } catch (Exception e) {
            // Only display a real error if it is a worker that we are actually
            // using
            if (serviceConfiguration.isActive()) {
                log.error("Worker is misconfigured, check the classpath", e);
            } else {
                log.info("Worker is misconfigured, check the classpath: " + e.getMessage());
            }
        }
        return worker;
    }

    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    @Override
    public long getServiceInterval(Integer serviceId) {
        long ret = IInterval.DONT_EXECUTE;
        ServiceData htp = serviceDataSession.findById(serviceId);
        if (htp != null) {
            ServiceConfiguration serviceConfiguration = htp.getServiceConfiguration();
            if (serviceConfiguration.isActive()) {
                IWorker worker = getWorker(serviceConfiguration, "temp", 0, 0); // A bit dirty, but it works..
                if (worker != null) {
                    ret = worker.getNextInterval();
                }
            } else {
                if (log.isDebugEnabled()) {
                    log.debug("Service " + serviceId + " is inactive.");
                }
            }
        }
        return ret;
    }

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    @Override
    public ServiceConfiguration getServiceConfiguration(AuthenticationToken admin, int id) {
        if (log.isTraceEnabled()) {
            log.trace(">getServiceConfiguration: " + id);
        }
        ServiceConfiguration returnval = null;
        try {
            ServiceData serviceData = serviceDataSession.findById(Integer.valueOf(id));
            if (serviceData != null) {
                returnval = serviceData.getServiceConfiguration();
            } else {
                if (log.isDebugEnabled()) {
                    log.debug("Returnval is null for service id: " + id);
                }
            }
        } catch (Exception e) {
            // return null if we cant find it, if it is not due to underlying
            // database error
            log.debug("Got an Exception for service with id " + id + ": " + e.getMessage());
            /*
             * If we don't re-throw here it will be treated as the service id
             * does not exist and the service will not be rescheduled to run.
             */
            throw new EJBException(e);
        }
        if (log.isTraceEnabled()) {
            log.trace("<getServiceConfiguration: " + id);
        }
        return returnval;
    }

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    @Override
    public HashMap<Integer, String> getServiceIdToNameMap() {
        HashMap<Integer, String> returnval = new HashMap<Integer, String>();
        Collection<ServiceData> result = serviceDataSession.findAll();
        for (ServiceData next : result) {
            returnval.put(next.getId(), next.getName());
        }
        return returnval;
    }

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    @Override
    public List<String> getServicesUsingCertificateProfile(Integer certificateProfileId) {
        List<String> result = new ArrayList<String>();
        //Since the service types are embedded in the data objects there is no more elegant way to to this.
        List<ServiceData> allServices = serviceDataSession.findAll();
        for (ServiceData service : allServices) {
            String certificateProfiles = service.getServiceConfiguration().getWorkerProperties()
                    .getProperty(BaseWorker.PROP_CERTIFICATE_PROFILE_IDS_TO_CHECK);
            if (certificateProfiles != null && !certificateProfiles.equals("")) {
                for (String certificateProfile : certificateProfiles.split(";")) {
                    if (certificateProfile.equals(certificateProfileId.toString())) {
                        result.add(service.getName());
                        break;
                    }
                }
            }
        }
        return result;
    }

    /**
     * Method to check if an admin is authorized to edit a service The following checks are performed.
     * 
     * 1. Deny If the service is hidden and the admin is internal EJBCA 2. Allow If the admin is an super administrator 3. Deny all other
     * 
     * @return true if the administrator is authorized
     */
    private boolean isAuthorizedToEditService(AuthenticationToken admin) {

        if (authorizationSession.isAuthorizedNoLogging(admin, StandardRules.ROLE_ROOT.resource())) {
            return true;
        }

        return false;
    }

    /**
     * Return true if the service should run on the node given the list of nodes it is pinned to. An empty list means that the service is not pinned
     * to any particular node and should run on all.
     * 
     * @param nodes list of nodes the service is pinned to
     * @return true if the service should run on this node
     */
    private boolean shouldRunOnThisNode(final String hostname, final List<String> nodes) {
        final boolean result;
        if (nodes == null || nodes.isEmpty()) {
            result = true;
        } else if (hostname == null) {
            result = false;
        } else {
            result = nodes.contains(hostname);
        }
        return result;
    }

    /**
     * @return The host's name or null if it could not be determined.
     */
    private String getHostName() {
        String hostname = null;
        try {
            InetAddress addr = InetAddress.getLocalHost();
            // Get hostname
            hostname = addr.getHostName();
        } catch (UnknownHostException e) {
            log.error("Hostname could not be determined", e);
        }
        return hostname;
    }

}