com.abiquo.bond.api.OutboundAPIClient.java Source code

Java tutorial

Introduction

Here is the source code for com.abiquo.bond.api.OutboundAPIClient.java

Source

/**
 * The Abiquo Platform
 * Cloud management application for hybrid clouds
 * Copyright (C) 2008 - Abiquo Holdings S.L.
 *
 * This application 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 under
 * version 3 of the License
 *
 * This software 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
 * LESSER GENERAL PUBLIC LICENSE v.3 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */
package com.abiquo.bond.api;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.TimeUnit;

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

import com.abiquo.bond.api.EventDispatcher.ShutdownLevel;
import com.abiquo.bond.api.event.APIEvent;
import com.abiquo.bond.api.plugin.BackupPluginInterface;
import com.abiquo.bond.api.plugin.PluginException;
import com.abiquo.bond.api.plugin.PluginInterface;
import com.abiquo.bond.api.response.ResponsesHandler;
import com.abiquo.event.model.Event;
import com.abiquo.model.rest.RESTLink;
import com.google.common.base.Enums;
import com.google.common.base.Optional;
import com.google.common.base.Throwables;

/**
 * This class is the central controller for the Outbound API client. It's main functions are:
 * <ul>
 * <li>Connect to the Outbound API ('M') server
 * <li>Load plugins that will handle the messages received from the M server
 * <li>Pass messages received from the M server to the plugins
 * </ul>
 * A client wrapper class should
 * <ul>
 * <li>Create an instance of this class.
 * <li>Call <code>getLoadFailures</code> to check for any failures during plugin loading
 * <li>If the result of the plugin loading is satisfactory, the plugins should be configured. How
 * this is done is specific to each plugin.
 * <li>Call <code>startPluginThreads</code> to get the plugins ready to process messages
 * <li>Call <code>connectToM</code>. This will start pulling messages down from the M server.
 * <li>To stop messages being passed to the plugins, call <code>disconnect</code>
 * </ul>
 */
public class OutboundAPIClient implements CommsHandler, EventStoreHandler {
    private final static Logger logger = LoggerFactory.getLogger(OutboundAPIClient.class);

    private MConnector mConnector;

    private ConfigurationData config;

    /*
     * References to all the plugins loaded by this class
     */
    private Set<PluginInterface> handlers;

    private Set<BackupPluginInterface> handlersWithResponses = new HashSet<>();

    private List<Throwable> failures = new ArrayList<>();

    private EventTranslator translator;

    private EventStore eventstore;

    private List<APIEvent> msgq = new ArrayList<>();

    private boolean processingEventStore = true;

    private NameToVMLinks mapNameToVMLinks;

    private APIConnection apiconn;

    private RESTLink currUserEditLink;

    private WrapperNotification handlerNotifications;

    private ResponsesHandler responses;

    private EventDispatcher eventDispatcher;

    private boolean shutdown = false;

    /**
     * The purpose of the constructor is to identify and load plugins.
     *
     * @param data Configuration data for the client. See {@link ConfigurationData} for details of
     *            what data is required.
     * @param version The version indicated by the client
     * @throws OutboundAPIClientException
     */
    public OutboundAPIClient(final ConfigurationData data, final File propertiesFile, final String version)
            throws OutboundAPIClientException {
        this.config = new ConfigurationData(data);

        apiconn = new APIConnection(config.getMServer(), config.getMUser(), config.getMUserPassword());
        currUserEditLink = apiconn.getCurrentUserLink();
        String apiVersion = apiconn.getAPIVersion().trim();
        if (!apiVersion.trim().equalsIgnoreCase(version)) {
            throw new OutboundAPIClientException(String.format(
                    "Api version indicated to start plugin (%s) mismatch with api version in use (%s)", version,
                    apiVersion));
        }

        // Create a cache of the REST links associated with each VM
        mapNameToVMLinks = new NameToVMLinks(config.getMServer(), config.getMUser(), config.getMUserPassword());

        Properties properties = new Properties();
        try {
            FileInputStream inputStream = new FileInputStream(propertiesFile);
            properties.load(inputStream);
        } catch (IOException e) {
            Throwables.propagate(e);
        }

        long timePeriod = new Long(properties.getProperty("avbc_responses_handler_period", "10")).longValue();
        String timeUnitValue = properties.getProperty("avbc_responses_handler_timeunit", "minutes");
        Optional<TimeUnit> timeUnitEnum = Enums.getIfPresent(TimeUnit.class, timeUnitValue.toUpperCase());
        if (!timeUnitEnum.isPresent()) {
            logger.warn("{} is not a valid java.util.concurrent.TimeUnit, using MINUTES as default", timeUnitValue);
        }
        TimeUnit timeUnit = timeUnitEnum.or(TimeUnit.MINUTES);

        // Set up response handlers to fetch data from the third party applications and update
        // Abiquo server with it
        // We need to get the repeat time from the configuration at some point - for now default it
        // to 10 minutes.
        responses = new ResponsesHandler(config.getMServer(), config.getMUser(), config.getMUserPassword(),
                mapNameToVMLinks, timePeriod, timeUnit);

        // Find and load any plugins on the classpath that support the returning of data from the
        // third party app to Abiquo. At the moment this just means Backup plugins
        Set<PluginInterface> plugins = new HashSet<>();
        ServiceLoader<BackupPluginInterface> bsl = ServiceLoader.load(BackupPluginInterface.class);
        Iterator<BackupPluginInterface> biterator = bsl.iterator();
        while (biterator.hasNext()) {
            try {
                BackupPluginInterface handler = biterator.next();
                logger.info("Loaded backup plugin: " + handler.getName());
                plugins.add(handler);
                handlersWithResponses.add(handler);
                // responses.addBackupPlugin(handler);
            } catch (Throwable t) {
                logger.error("Loading of plugin failed", t);
                failures.add(t);
            }
        }

        // Find and load all other plugins
        ServiceLoader<PluginInterface> sl = ServiceLoader.load(PluginInterface.class);
        Iterator<PluginInterface> iterator = sl.iterator();
        while (iterator.hasNext()) {
            try {
                PluginInterface handler = iterator.next();
                logger.info("Loaded plugin: " + handler.getName());
                plugins.add(handler);
            } catch (Throwable t) {
                logger.error("Loading of plugin failed", t);
                failures.add(t);
            }
        }

        handlers = Collections.unmodifiableSet(plugins);

        eventDispatcher = new EventDispatcher(handlers, 1);

        // Initialise the class that will fecth events from the permanent store that may have been
        // missed since the last time the client was run
        eventstore = new EventStore(config.getMServer(), config.getMUser(), config.getMUserPassword(),
                currUserEditLink, mapNameToVMLinks);
    }

    /**
     * Allows the client to pass messages back to the client wrapper without interrupting the flow
     * of the program.
     *
     * @param handler notification handler supplied by the client wrapper
     */
    public void setNotificationHandler(final WrapperNotification handler) {
        handlerNotifications = handler;
        if (eventstore != null) {
            eventstore.setNotificationHandler(handler);
        }
        if (responses != null) {
            responses.setNotificationHandler(handler);
        }
        if (mapNameToVMLinks != null) {
            mapNameToVMLinks.setNotificationHandler(handler);
        }
    }

    /**
     * Pass a notification and exception back to the client wrapper if a notification handler has
     * been set
     *
     * @param msg the notification to be return to the client wrapper
     * @param t the exception to be return to the client wrapper
     */
    private void notifyWrapper(final String msg, final Throwable t) {
        if (handlerNotifications != null) {
            handlerNotifications.notification(msg, t);
        }
    }

    /**
     * When the wrapper class is closing down it should call this method to get the timestamp of the
     * last outbound api event that has been handled. This timestamp can then be used the next time
     * the client starts up to fecth messages from the permanent event store.
     *
     * @return Timestamp of last processed outbound api event
     */
    public LocalDateTime getLastEventTimestamp() {
        return eventDispatcher.getLastEventTimestamp();
    }

    /**
     * Start each successfully loaded plugin
     */
    public void startPlugins() {
        translator = new EventTranslator(config.getMServer(), config.getMUser(), config.getMUserPassword(),
                currUserEditLink, mapNameToVMLinks);

        for (BackupPluginInterface plugin : handlersWithResponses) {
            responses.addBackupPlugin(plugin);
        }

        failures.clear();
        for (PluginInterface handler : handlers) {
            try {
                handler.startup();
            } catch (PluginException e) {
                logger.error("Startup of plugin failed", e);
                failures.add(e);
            }
        }
    }

    /**
     * Wait for all the plugin threads to complete
     */
    public boolean isRunning() {
        return !shutdown;
    }

    /**
     * Returns a Set of instances of successfully loaded plugins
     *
     * @return Set of PluginInterface instances
     */
    public Set<PluginInterface> getPluginList() {
        return handlers;
    }

    /**
     * Returns a List of exceptions thrown by plugins during loading
     *
     * @return List of Throwables
     */
    public List<Throwable> getLoadFailures() {
        return Collections.unmodifiableList(failures);
    }

    /**
     * Opens an HTTP connection to the M server
     *
     * @throws OutboundAPIClientException if any errors occur during the connection attempt
     */
    public void run() throws OutboundAPIClientException {
        try {
            // Open the connection to the M server
            @SuppressWarnings("unchecked")
            Class<? extends MConnector> connectorclass = (Class<? extends MConnector>) Class
                    .forName(config.getConnector());
            Constructor<? extends MConnector> constructor = connectorclass.getConstructor(CommsHandler.class);
            mConnector = constructor.newInstance(this);
            mConnector.connect(config.getMServer(), config.getMUser(), config.getMUserPassword());

            // As the connector is now running we can ignore anything in the event store after this
            // time.
            eventstore.setMsgTimeLimit();

            // Check the Event log for any messages missed since the program last ran
            LocalDateTime lastmsg = config.getLastProcessedEvent();
            if (lastmsg != null) {
                eventstore.getMissedEvents(lastmsg, this);

                List<APIEvent> msgqcopy = new ArrayList<>();
                while (processingEventStore) {
                    synchronized (msgq) {
                        if (msgq.isEmpty()) {
                            processingEventStore = false;
                        } else {
                            msgqcopy.addAll(msgq);
                            msgq.clear();
                        }
                    }
                    for (APIEvent event : msgqcopy) {
                        handleMessage(event);
                    }
                }
            } else {
                processingEventStore = false;
            }
        } catch (ReflectiveOperationException | IllegalArgumentException | SecurityException e) {
            throw new OutboundAPIClientException("Configuration error", e);
        }
    }

    /**
     * Closes the connection to the M server
     */
    public void close() {
        // Stop each of the plugins
        for (PluginInterface plugin : handlers) {
            plugin.cancel();
        }

        // Wait for the plugins to stop
        eventDispatcher.shutdown(ShutdownLevel.AWAIT_RUNNING_TASKS);

        // Disconnect from the Outbound API
        if (mConnector != null) {
            mConnector.disconnect();
        }
        shutdown = true;
    }

    @Override
    public void handleMessage(final APIEvent apievent) {
        eventDispatcher.dispatchEvent(apievent);
    }

    @Override
    public void handleMessage(final String msg) {
        APIEvent apievent = null;
        try {
            Event event = AbiquoObjectMapper.OBJECT_MAPPER.instance().readValue(msg, Event.class);
            logger.debug("Received event: Type:{} Action:{} Time:{}", new Object[] { event.getType(),
                    event.getAction(),
                    LocalDateTime.ofInstant(Instant.ofEpochMilli(event.getTimestamp()), ZoneId.systemDefault()) });
            Optional<APIEvent> optapievent = translator.translate(event);
            if (optapievent.isPresent()) {
                apievent = optapievent.get();
                if (processingEventStore) {
                    synchronized (msgq) {
                        msgq.add(apievent);
                    }
                } else {
                    handleMessage(apievent);
                }
            }
        } catch (IOException | OutboundAPIClientException e) {
            logger.warn("Exception whilst translating message from outbound api", e);
            notifyWrapper("Exception whilst translating message from outbound api", e);
        }
    }

    @Override
    public void handleHeaders(final String headers) {
        logger.info("HTTP headers from 'M' server connection: {}", headers);
    }

    @Override
    public void handleTransportType(final Transport transport) {
        if (transport.equals(config.getTransport())) {
            logger.info("Negotiated requested transport ({}) with 'M' server", transport);
        } else {
            logger.info("Negotiated alternative transport ({}) with 'M' server", transport);
            config.setNegotiatedTransport(transport);
        }
    }
}