/*_############################################################################
  _## 
  _##  SNMP4J-AgentX - SubagentXConfigManager.java  
  _## 
  _##  Copyright (C) 2005-2026  Frank Fock (SNMP4J.org)
  _##  
  _##  This program is free software; you can redistribute it and/or modify
  _##  it under the terms of the GNU General Public License version 2 as 
  _##  published by the Free Software Foundation.
  _##
  _##  This program is distributed in the hope that it will be useful,
  _##  but WITHOUT ANY WARRANTY; without even the implied warranty of
  _##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  _##  GNU General Public License for more details.
  _##
  _##  You should have received a copy of the GNU General Public License
  _##  along with this program; if not, write to the Free Software
  _##  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 
  _##  MA  02110-1301  USA
  _##  
  _##########################################################################*/

package org.snmp4j.agent.agentx.subagent;

import java.io.*;
import java.util.*;

import org.snmp4j.*;
import org.snmp4j.agent.*;
import org.snmp4j.agent.agentx.AgentXCommandListener;
import org.snmp4j.agent.agentx.AgentXMessageDispatcher;
import org.snmp4j.agent.agentx.AgentXProtocol;
import org.snmp4j.agent.io.*;
import org.snmp4j.agent.mo.MOPriorityProvider;
import org.snmp4j.agent.mo.MOTableRow;
import org.snmp4j.agent.mo.util.VariableProvider;
import org.snmp4j.agent.request.*;
import org.snmp4j.log.*;
import org.snmp4j.mp.*;
import org.snmp4j.smi.*;
import org.snmp4j.util.*;
import org.snmp4j.agent.mo.util.MOTableSizeLimit;
import org.snmp4j.agent.mo.MOFactory;
import org.snmp4j.agent.mo.DefaultMOFactory;

/**
 * The {@code SubagentXConfigManager} is the main component of a SNMP4J AgentX Subagent.
 * It puts together agent configuration and agent components like command
 * processor, message dispatcher, managed objects and server.
 *
 * @author Frank Fock
 * @version 3.0.0
 * @since 3.0.0
 */
public abstract class SubagentXConfigManager implements Runnable, VariableProvider {

    private static final LogAdapter logger =
            LogFactory.getLogger(SubagentXConfigManager.class);

    protected AgentXCommandListener agent;
    protected WorkerPool workerPool;

    protected MOServer[] servers;
    protected AgentXMessageDispatcher dispatcher;
    protected NotificationOriginator notificationOriginator;

    protected MOInputFactory configuration;
    protected MOPersistenceProvider persistenceProvider;
    protected int persistenceImportMode = ImportMode.UPDATE_CREATE;
    protected MOPriorityProvider priorityProvider;

    protected MOFactory moFactory = DefaultMOFactory.getInstance();
    protected OctetString defaultContext;
    protected AgentState runState = new AgentState();

    protected MOTableSizeLimit<MOTableRow> tableSizeLimit;

    protected List<AgentStateListener<SubagentXConfigManager>> agentStateListeners = new ArrayList<>(2);

    /**
     * Creates a SNMP agent configuration which can be run by calling
     * {@link #run()} later.
     *
     * @param agentXCommandListener
     *         the {@link AgentXCommandListener} that handles the AgentX packages and that is able to close all
     *         {@link org.snmp4j.agent.agentx.AgentXSession}s.
     * @param messageDispatcher
     *         the MessageDispatcher to use. The message dispatcher must be configured
     *         outside, i.e. transport mappings have to be added before this
     *         constructor is being called.
     * @param moServers
     *         the managed object server(s) that serve the managed objects available
     *         to this agent.
     * @param workerPool
     *         the {@code WorkerPool} to be used to process incoming request.
     * @param configurationFactory
     *         a {@code MOInputFactory} that creates a {@code MOInput} stream
     *         with containing serialized ManagedObject information with the agent's
     *         configuration or {@code null} otherwise.
     * @param persistenceProvider
     *         the primary {@link MOPersistenceProvider} to be used to load
     *         and store persistent MOs.
     * @param moFactory
     *         the {@link MOFactory} to be used to create {@link ManagedObject}s
     *         created by this config manager. If {@code null} the
     *         {@link DefaultMOFactory} will be used.
     */
    public SubagentXConfigManager(AgentXCommandListener agentXCommandListener,
                                  AgentXMessageDispatcher messageDispatcher,
                                  MOServer[] moServers,
                                  WorkerPool workerPool,
                                  MOInputFactory configurationFactory,
                                  MOPersistenceProvider persistenceProvider,
                                  MOFactory moFactory) {
        this.agent = agentXCommandListener;
        this.dispatcher = messageDispatcher;
        this.servers = moServers;
        this.workerPool = workerPool;
        this.configuration = configurationFactory;
        this.persistenceProvider = persistenceProvider;
        this.moFactory = (moFactory == null) ? this.moFactory : moFactory;
    }

    /**
     * Gets the worker pool of this agent.
     * @return
     *    the internal worker pool.
     */
    public WorkerPool getWorkerPool() {
        return workerPool;
    }

    /**
     * Initializes, configures, restores agent state, and then launches the
     * SNMP agent depending on its current run state. For example, if
     * {@link #initialize()} has not yet been called it will be called before
     * the agent is being configured in the next step.
     * <p>
     * See also {@link #initialize()}, {@link #configure()},
     * {@link #restoreState()}, and {@link #launch()}.
     */
    public void run() {
        if (runState.getState() < org.snmp4j.agent.AgentState.STATE_INITIALIZED) {
            initialize();
        }
        if (runState.getState() < org.snmp4j.agent.AgentState.STATE_CONFIGURED) {
            configure();
        }
        if (runState.getState() < org.snmp4j.agent.AgentState.STATE_RESTORED) {
            restoreState();
        }
        if (runState.getState() < org.snmp4j.agent.AgentState.STATE_RUNNING) {
            launch();
        }
    }

    public synchronized void addAgentStateListener(AgentStateListener<SubagentXConfigManager> agentStateListener) {
        this.agentStateListeners.add(agentStateListener);
    }

    public synchronized boolean removeAgentStateListener(AgentStateListener<SubagentXConfigManager> agentStateListener) {
        return this.agentStateListeners.remove(agentStateListener);
    }

    protected synchronized void fireAgentStateChange() {
        for (AgentStateListener<SubagentXConfigManager> agentStateListener : agentStateListeners) {
            agentStateListener.agentStateChanged(this, this.runState);
        }
    }

    /**
     * Get the {@link MOServer} associated with the given context.
     * @param context
     *    a SNMPv3 context or {@code null} (or empty {@link OctetString}) for the default context.
     * @return
     *    the associated {@link MOServer} for the context that acutally also supports the context or {@code null}
     *    otherwise.
     */
    public synchronized MOServer getServer(OctetString context) {
        for (MOServer s : servers) {
            if (s.isContextSupported(context)) {
                return s;
            }
        }
        return null;
    }

    public MOServer[] getServers() {
        return servers;
    }

    /**
     * Returns the state of the agent.
     *
     * @return a {@link org.snmp4j.agent.AgentState} instance.
     */
    public org.snmp4j.agent.AgentState getState() {
        return runState;
    }

    /**
     * Launch the agent by registering and launching (i.e., set to listen mode)
     * transport mappings.
     */
    protected void launch() {
        if (tableSizeLimit != null) {
            for (MOServer server : servers) {
                DefaultMOServer.unregisterTableRowListener(server, tableSizeLimit);
                DefaultMOServer.registerTableRowListener(server, tableSizeLimit);
            }
        }
        dispatcher.removeCommandListener(agent);
        dispatcher.addCommandListener(agent);
        try {
            launchTransportMappings();
        } catch (IOException ex) {
            String txt =
                    "Could not put all transport mappings in listen mode: " +
                            ex.getMessage();
            logger.error(txt, ex);
            runState.addError(new ErrorDescriptor(txt, runState.getState(),
                    org.snmp4j.agent.AgentState.STATE_RUNNING, ex));
            return;
        }
        try {
            launchAgentXSessions();
        }
        catch (IOException iox) {
            String txt =
                    "Could not establish AgentX connection to a master agent: " +
                            iox.getMessage();
            logger.error(txt, iox);
            runState.addError(new ErrorDescriptor(txt, runState.getState(),
                    org.snmp4j.agent.AgentState.STATE_RUNNING, iox));
            return;

        }
        runState.advanceState(org.snmp4j.agent.AgentState.STATE_RUNNING);
        fireLaunchNotifications();
    }

    /**
     * Establish AgentX connection to one or more AgentX master agents by implementing this method.
     * @throws IOException if the session(s) cannot be established due to IO error and the agent should not advance
     * to the state {@link AgentState#STATE_RUNNING}.
     */
    public abstract void launchAgentXSessions() throws IOException;

    /**
     * Fire notifications after agent start, i.e. sending a coldStart trap.
     */
    protected void fireLaunchNotifications() {
        if (notificationOriginator != null) {
            notificationOriginator.notify(new OctetString(), SnmpConstants.coldStart,
                    new VariableBinding[0]);
        }
    }

    /**
     * Continues processing of SNMP requests by coupling message dispatcher and
     * agent. To succeed, the current state of the agent must be
     * {@link org.snmp4j.agent.AgentState#STATE_SUSPENDED}.
     *
     * @return {@code true} if the running state could be restored,
     * {@code false} otherwise.
     */
    public boolean continueProcessing() {
        if (runState.getState() == org.snmp4j.agent.AgentState.STATE_SUSPENDED) {
            dispatcher.removeCommandListener(agent);
            dispatcher.addCommandListener(agent);
            runState.setState(org.snmp4j.agent.AgentState.STATE_RUNNING);
            return true;
        }
        return false;
    }

    /**
     * Suspends processing of SNMP requests. This call decouples message
     * dispatcher and agent. All transport mappings remain unchanged and thus
     * all ports remain opened.
     */
    public void suspendProcessing() {
        dispatcher.removeCommandListener(agent);
        runState.setState(org.snmp4j.agent.AgentState.STATE_SUSPENDED);
    }

    /**
     * Shutdown the agent by closing the internal SNMP session - including the
     * transport mappings provided through the configured
     * {@link MessageDispatcher} and then store the agent state to persistent
     * storage (if available).
     */
    public void shutdown() {
        logger.info("Shutdown agent: suspending request processing");

        agent.closeAllSessions(AgentXProtocol.REASON_SHUTDOWN);

        suspendProcessing();
        try {
            if (dispatcher != null) {
                logger.info("Shutdown agent: closing transport mappings");
                stopTransportMappings(dispatcher.getTransportMappings());
            }
        } catch (IOException ex) {
            logger.warn("Failed to close SNMP session: " + ex.getMessage());
        }
        logger.info("Shutdown agent: saving state");
        if (!saveState() && (persistenceProvider != null)) {
            logger.error("Agent state could not be saved!");
        }
        if (tableSizeLimit != null) {
            for (MOServer server : servers) {
                DefaultMOServer.unregisterTableRowListener(server, tableSizeLimit);
            }
        }
        logger.info("Shutdown agent: unregistering MIB objects");
        unregisterMIBs(null);
        logger.info("Shutdown agent: closing persistence provider");
        try {
            persistenceProvider.close();
        } catch (Exception e) {
            logger.warn("Shutdown agent: Failed to close persistence provider: "+e.getMessage());
        }
        runState.setState(org.snmp4j.agent.AgentState.STATE_SHUTDOWN);
        logger.info("Shutdown agent: finished");
    }

    /**
     * Registers a shutdown hook {@code Thread} at the {@link Runtime}
     * instance.
     */
    public void registerShutdownHook() {
        Runtime.getRuntime().addShutdownHook(new Thread() {
            public void run() {
                shutdown();
            }
        });
    }

    /**
     * Launch the transport mappings. By default {@link #launchTransportMappings(Collection)} is called with the
     * transport mappings of the dispatcher.
     * @throws IOException
     *    if a transport mapping throws an {@link IOException} of {@link TransportMapping#listen()}.
     */
    protected void launchTransportMappings() throws IOException {
        launchTransportMappings(dispatcher.getTransportMappings());
    }

    /**
     * Puts a list of transport mappings into listen mode.
     *
     * @param transportMappings
     *         a list of {@link TransportMapping} instances.
     *
     * @throws IOException
     *         if a transport cannot listen to incoming messages.
     */
    protected static void launchTransportMappings(Collection<? extends TransportMapping<?>> transportMappings)
            throws IOException {
        ArrayList<? extends TransportMapping<?>> l = new ArrayList<>(transportMappings);
        for (TransportMapping<?> tm : l) {
            if (!tm.isListening()) {
                tm.listen();
            }
        }
    }

    /**
     * Closes a list of transport mappings.
     *
     * @param transportMappings
     *         a list of {@link TransportMapping} instances.
     *
     * @throws IOException
     *         if a transport cannot be closed.
     */
    protected static void stopTransportMappings(Collection<? extends TransportMapping<?>> transportMappings)
            throws IOException {
        ArrayList<TransportMapping<?>> l = new ArrayList<>(transportMappings);
        for (TransportMapping<?> tm : l) {
            tm.close();
        }
    }


    /**
     * Save the state of the agent persistently - if necessary persistent
     * storage is available.
     *
     * @return {@code true} if state has been saved successfully, {@code false} is returned if an error occurred or no
     * {@link #persistenceProvider} is set. The error details can be found in the {@link #runState} object.
     */
    public boolean saveState() {
        if (persistenceProvider != null) {
            try {
                persistenceProvider.store(persistenceProvider.getDefaultURI(), priorityProvider);
                runState.advanceState(org.snmp4j.agent.AgentState.STATE_SAVED);
                return true;
            } catch (IOException ex) {
                String txt = "Failed to save agent state: " + ex.getMessage();
                logger.error(txt, ex);
                runState.addError(new ErrorDescriptor(txt, runState.getState(),
                        AgentState.STATE_SAVED, ex));
            }
        }
        return false;
    }

    /**
     * Restore a previously persistently saved state - if available.
     *
     * @return {@code true} if the agent state could be restored successfully,
     * {@code false} otherwise.
     */
    public boolean restoreState() {
        if (persistenceProvider != null) {
            try {
                if (logger.isInfoEnabled()) {
                    logger.info("Restoring persistent data (mode=" + persistenceImportMode + ") from " +
                            persistenceProvider.getDefaultURI());
                }
                persistenceProvider.restore(persistenceProvider.getDefaultURI(),
                        persistenceImportMode);
                runState.advanceState(org.snmp4j.agent.AgentState.STATE_RESTORED);
                return true;
            } catch (FileNotFoundException fnf) {
                String txt = "Saved agent state not found: " + fnf.getMessage();
                logger.warn(txt);
            } catch (IOException ex) {
                String txt = "Fai" +
                        "led to load agent state: " + ex.getMessage();
                logger.error(txt, ex);
                runState.addError(new ErrorDescriptor(txt, runState.getState(),
                        AgentState.STATE_RESTORED, ex));
            }
        }
        return false;
    }

    /**
     * Configures components and managed objects.
     */
    public void configure() {
        if (configuration != null) {
            MOInput config = configuration.createMOInput();
            if (config == null) {
                logger.debug("No configuration returned by configuration factory " +
                        configuration);
                return;
            }
            MOServerPersistence serverPersistence = new MOServerPersistence(servers);
            try {
                serverPersistence.loadData(config);
            } catch (IOException ex) {
                String txt = "Failed to load agent configuration: " + ex.getMessage();
                logger.error(txt, ex);
                runState.addError(new ErrorDescriptor(txt, runState.getState(),
                        AgentState.STATE_CONFIGURED, ex));
                throw new RuntimeException(txt, ex);
            } finally {
                try {
                    config.close();
                } catch (IOException ex1) {
                    logger.warn("Failed to close config input stream: " + ex1.getMessage());
                }
            }
        }
        runState.advanceState(org.snmp4j.agent.AgentState.STATE_CONFIGURED);
    }

    /**
     * Hook method for overriding in sub-classes that is called by {@link #initialize()} before
     * {@link #registerMIBs(OctetString)}.
     * This method does nothing by default.
     */
    protected void linkCounterListener() {

    }

    /**
     * Initialize the agent by registering all MIB modules.
     */
    public void initialize() {
        linkCounterListener();
        try {
            registerMIBs(getDefaultContext());
        } catch (DuplicateRegistrationException drex) {
            logger.error("Duplicate MO registration: " + drex.getMessage(), drex);
        }
        runState.advanceState(org.snmp4j.agent.AgentState.STATE_INITIALIZED);
    }

    /**
     * Sets the table size limits for the tables in this agent. If this method is
     * called while the agent's registration is being changed, a
     * {@code ConcurrentModificationException} might be thrown.
     *
     * @param sizeLimits
     *         a set of properties as defined by {@link MOTableSizeLimit}.
     *
     * @since 1.4
     */
    public void setTableSizeLimits(Properties sizeLimits) {
        if ((tableSizeLimit != null) && (servers != null)) {
            for (MOServer server : servers) {
                DefaultMOServer.unregisterTableRowListener(server, tableSizeLimit);
            }
        }
        tableSizeLimit = new MOTableSizeLimit<MOTableRow>(sizeLimits);
        if (getState().getState() == org.snmp4j.agent.AgentState.STATE_RUNNING) {
            for (MOServer server : servers) {
                DefaultMOServer.registerTableRowListener(server, tableSizeLimit);
            }
        }
    }

    /**
     * Sets the table size limit for the tables in this agent. If this method is
     * called while the agent's registration is being changed, a
     * {@code ConcurrentModificationException} might be thrown.
     *
     * @param sizeLimit
     *         the maximum size (numer of rows) of tables allowed for this agent.
     *
     * @since 1.4
     */
    public void setTableSizeLimit(int sizeLimit) {
        if ((tableSizeLimit != null) && (servers != null)) {
            for (MOServer server : servers) {
                DefaultMOServer.unregisterTableRowListener(server, tableSizeLimit);
            }
        }
        tableSizeLimit = new MOTableSizeLimit<MOTableRow>(sizeLimit);
        if (getState().getState() == org.snmp4j.agent.AgentState.STATE_RUNNING) {
            for (MOServer server : servers) {
                DefaultMOServer.registerTableRowListener(server, tableSizeLimit);
            }
        }
    }

    /**
     * Returns the default context - which is the context that is used by the
     * base agent to register its MIB objects. By default it is {@code null}
     * which causes the objects to be registered virtually for all contexts.
     * In that case, subagents for example my not register their own objects
     * under the same subtree(s) in any context. To allow subagents to register
     * their own instances of those MIB modules, an empty {@code OctetString}
     * should be used as default context instead.
     *
     * @return {@code null} or an {@code OctetString} (normally the empty
     * string) denoting the context used for registering default MIBs.
     */
    public OctetString getDefaultContext() {
        return defaultContext;
    }

    /**
     * This method can be overwritten by a subagent to specify the contexts
     * each MIB module (group) will be registered to.
     *
     * @param mibGroup
     *         a group of {@link ManagedObject}s (i.e., a MIB module).
     * @param defaultContext
     *         the context to be used by default (i.e., the {@code null} context)
     *
     * @return the context for which the module should be registered.
     */
    protected OctetString getContext(MOGroup mibGroup,
                                     OctetString defaultContext) {
        return defaultContext;
    }


    /**
     * Register the initialized MIB modules in the specified context of the agent.
     *
     * @param context
     *         the context to register the internal MIB modules. This should be
     *         {@code null} by default.
     *
     * @throws DuplicateRegistrationException
     *         if some of the MIB modules
     *         registration regions conflict with already registered regions.
     */
    protected void registerMIBs(OctetString context) throws
            DuplicateRegistrationException {
    }

    /**
     * Unregister the initialized MIB modules from the default context of the
     * agent.
     *
     * @param context
     *         the context where the MIB modules have been previously registered.
     */
    protected void unregisterMIBs(OctetString context) {

    }

    /**
     * Gets the priority provider defining the order of storing and restoring persistent data.
     * @return
     *    a {@link MOPriorityProvider} instance.
     */
    public MOPriorityProvider getPriorityProvider() {
        return priorityProvider;
    }

    /**
     * Sets the {@link MOPriorityProvider} that defines in which order {@link ManagedObject}s of this sub-agent are
     * stored and restored. A defined order can be important for initializing dependent objects and to avoid security
     * issues during restore operations.
     * @param priorityProvider
     *    a {@link MOPriorityProvider} or {@code null} if the default lexicographic order by OID should be applied.
     * @since 3.5.0
     */
    public void setPriorityProvider(MOPriorityProvider priorityProvider) {
        this.priorityProvider = priorityProvider;
    }

    /**
     * Sets the import mode for the {@link MOPersistenceProvider}.
     *
     * @param importMode
     *         one of the import modes defined by {@link ImportMode}.
     */
    public void setPersistenceImportMode(int importMode) {
        this.persistenceImportMode = importMode;
    }

    /**
     * Returns the currently active import mode for the
     * {@link MOPersistenceProvider}.
     *
     * @return one of the import modes defined by {@link ImportMode}.
     */
    public int getPersistenceImportMode() {
        return persistenceImportMode;
    }

    @SuppressWarnings("unchecked")
    public Variable getVariable(String name) {
        OID oid;
        OctetString context = null;
        int pos = name.indexOf(':');
        if (pos >= 0) {
            context = new OctetString(name.substring(0, pos));
            oid = new OID(name.substring(pos + 1, name.length()));
        } else {
            oid = new OID(name);
        }
        MOServer server = getServer(context);
        final DefaultMOContextScope scope =
                new DefaultMOContextScope(context, oid, true, oid, true);
        MOQuery query = new MOQueryWithSource(scope, false, this);
        ManagedObject<SubRequest<?>> mo = server.lookup(query, null, null, ManagedObject.class);
        if (mo != null) {
            final VariableBinding vb = new VariableBinding(oid);
            final RequestStatus status = new RequestStatus();
            SubRequest<?> req = new SubRequest<SnmpRequest.SnmpSubRequest>() {
                private boolean completed;
                private MOQuery query;

                public boolean hasError() {
                    return false;
                }

                public void setErrorStatus(int errorStatus) {
                    status.setErrorStatus(errorStatus);
                }

                public int getErrorStatus() {
                    return status.getErrorStatus();
                }

                public RequestStatus getStatus() {
                    return status;
                }

                public MOScope getScope() {
                    return scope;
                }

                public VariableBinding getVariableBinding() {
                    return vb;
                }

                public Request<?,?,?> getRequest() {
                    return null;
                }

                public Object getUndoValue() {
                    return null;
                }

                public void setUndoValue(Object undoInformation) {
                }

                public void completed() {
                    completed = true;
                }

                public boolean isComplete() {
                    return completed;
                }

                public void setTargetMO(ManagedObject<? super SnmpRequest.SnmpSubRequest> managedObject) {
                }

                public ManagedObject<? super SnmpRequest.SnmpSubRequest> getTargetMO() {
                    return null;
                }

                public int getIndex() {
                    return 0;
                }

                public void setQuery(MOQuery query) {
                    this.query = query;
                }

                public MOQuery getQuery() {
                    return query;
                }

                public SubRequestIterator<SnmpRequest.SnmpSubRequest> repetitions() {
                    return null;
                }

                public void updateNextRepetition() {
                }

                public Object getUserObject() {
                    return null;
                }

                public void setUserObject(Object userObject) {
                }

            };
            mo.get(req);
            return vb.getVariable();
        }
        return null;
    }


    public class AgentState implements org.snmp4j.agent.AgentState {

        private int state = STATE_CREATED;

        /**
         * Contains a list of ErrorDescription objects describing errors occured
         * since agent launched for the first time.
         */
        private final List<ErrorDescriptor> errorsOccurred = new LinkedList<ErrorDescriptor>();

        @Override
        public int getState() {
            return state;
        }

        /**
         * Sets the new state independent from the current state.
         *
         * @param newState
         *         the new state.
         */
        @Override
        public void setState(int newState) {
            boolean stateChanged = this.state != newState;
            this.state = newState;
            logger.info("Agent state set to " + newState + " ("+(stateChanged ? "": "un")+"changed)");
            fireAgentStateChange();
        }

        /**
         * Advance the state to the given state. If the current state is greater than
         * the provided state, the current state will not be changed.
         *
         * @param newState
         *         the new minimum state.
         */
        @Override
        public void advanceState(int newState) {
            if (state < newState) {
                state = newState;
                logger.info("Agent state advanced to " + newState);
                fireAgentStateChange();
            }
        }

        /**
         * Add an error description to the internal error list.
         *
         * @param error
         *         an ErrorDescriptor instance to add.
         */
        @Override
        public void addError(org.snmp4j.agent.AgentState.ErrorDescriptor error) {
            errorsOccurred.add(error);
        }

        /**
         * Get the error descriptors associated with this agent state.
         *
         * @return the errors descriptor list.
         */
        @Override
        public List<org.snmp4j.agent.AgentState.ErrorDescriptor> getErrors() {
            return new ArrayList<>(errorsOccurred);
        }
    }

    public static class ErrorDescriptor implements org.snmp4j.agent.AgentState.ErrorDescriptor {
        private final Exception exception;
        private final int sourceState;
        private final int targetState;
        private final String description;

        ErrorDescriptor(String descr, int sourceState, int targetState,
                        Exception ex) {
            this.description = descr;
            this.sourceState = sourceState;
            this.targetState = targetState;
            this.exception = ex;
        }

        public String getDescription() {
            return description;
        }

        public int getSourceState() {
            return sourceState;
        }

        public int getTargetState() {
            return targetState;
        }

        public Exception getException() {
            return exception;
        }
    }
}
