All Downloads are FREE. Search and download functionalities are using the official Maven repository.

se.arkalix.core.plugin.HttpJsonEventSubscriptionPlugin Maven / Gradle / Ivy

package se.arkalix.core.plugin;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import se.arkalix.core.plugin.dto.ErrorException;
import se.arkalix.core.plugin.dto.EventIncomingDto;
import se.arkalix.core.plugin.dto.SystemDetails;
import se.arkalix.core.plugin.internal.ArEventSubscription;
import se.arkalix.description.ProviderDescription;
import se.arkalix.net.http.HttpStatus;
import se.arkalix.net.http.service.HttpService;
import se.arkalix.plugin.Plug;
import se.arkalix.plugin.Plugin;
import se.arkalix.security.access.AccessPolicy;
import se.arkalix.util.concurrent.Future;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import static se.arkalix.descriptor.EncodingDescriptor.JSON;

/**
 * HTTP/JSON event subscription plugin.
 * 

* This plugin helps manage subscriptions that last for the entire lifetime of * one or more systems. When a system given this plugin starts up, the plugin * looks up the "event-subscribe" and "event-unsubscribe" services, out of * which it is assumed to only exist one each, and uses the former service to * subscribe to all desired events. When the systems in question shuts down, * the latter service is used to unsubscribe to the same events. Events are * received by automatically setting up an {@link HttpService} that specifies * one endpoint for each kind of event of interest. *

* Use of this plugin requires either that the mentioned services are preloaded * into the service cache of built systems, or that a plugin that performs * service resolution is loaded before this plugin. If a preloaded service * cache is used, make sure that its entries do not expire during the lifetime * of any subscribing systems. */ @SuppressWarnings("unused") public class HttpJsonEventSubscriptionPlugin implements Plugin { private static final Logger logger = LoggerFactory.getLogger(HttpJsonEventSubscriptionPlugin.class); private final Collection subscriptions; private final String basePath; private HttpJsonEventSubscriptionPlugin(final Builder builder) { basePath = Objects.requireNonNull(builder.basePath, "Expected basePath"); subscriptions = Objects.requireNonNull(builder.subscriptions, "Expected subscriptions").values(); } @Override public void onAttach(final Plug plug) { if (logger.isInfoEnabled()) { logger.info("HTTP/JSON event subscription plugin attached to \"{}\"", plug.system().name()); } } @Override public void afterAttach(final Plug plug) { if (logger.isInfoEnabled()) { logger.info("Registering event subscriptions of the \"{}\" system ...", plug.system().name()); } final var system = plug.system(); system.consume() .using(HttpJsonEventSubscribe.factory()) .flatMap(eventSubscribe -> { if (logger.isInfoEnabled()) { final var service = eventSubscribe.service(); final var provider = eventSubscribe.service().provider(); logger.info("Registering subscriptions via \"{}\" provided by \"{}\" {} for the \"{}\" system ...", service.name(), provider.name(), provider.socketAddress(), plug.system().name()); } final var subscriber = SystemDetails.from(system); final var eventSubscriber = new HttpService() .name("event-subscriber") .basePath(basePath) .accessPolicy(AccessPolicy.whitelist(eventSubscribe.service().provider().name())) .encodings(JSON); for (final var subscription : subscriptions) { eventSubscriber.post("/" + subscription.topic(), (request, response) -> request.bodyAs(EventIncomingDto.class) .ifSuccess(event -> { try { subscription.publish(event.metadata(), event.data()); } finally { response.status(HttpStatus.OK); } }) .mapCatch(Throwable.class, fault -> { if (logger.isWarnEnabled()) { logger.warn("Failed to read received " + "event " + subscription, fault); } response.status(HttpStatus.BAD_REQUEST); return null; })); } return system.provide(eventSubscriber) .ifSuccess(serviceHandle -> { for (final var subscription : subscriptions) { final var sendToUri = basePath + "/" + subscription.topic(); final var subscriptionRequest = subscription.toSubscriberRequest(subscriber, sendToUri); eventSubscribe.subscribe(subscriptionRequest) .flatMapCatch(ErrorException.class, fault -> { final var error = fault.error(); if ("INVALID_PARAMETER".equals(error.type())) { return system.consume() .using(HttpJsonEventUnsubscribe.factory()) .flatMap(eventUnsubscribe -> eventUnsubscribe .unsubscribe(subscription.topic(), system)) .flatMap(ignored -> eventSubscribe.subscribe(subscriptionRequest)) .pass(null); } return Future.failure(fault); }) .ifSuccess(ignored -> { if (logger.isInfoEnabled()) { logger.info("Registered {} for the \"{}\" system", subscription.toString(), plug.system().name()); } }) .onFailure(fault -> { if (logger.isWarnEnabled()) { logger.warn("Failed to register " + subscription, fault); } }); } }) .mapFault(Throwable.class, fault -> new Exception("" + "Failed to setup event receiver for the \"" + plug.system().name() + "\" system", fault)); }) .ifSuccess(ignored -> { if (logger.isInfoEnabled()) { logger.info("Registered all event subscriptions of " + "the \"{}\" system ...", plug.system().name()); } }) .onFailure(fault -> { if (logger.isErrorEnabled()) { logger.error("Failed to register the event subscriptions " + "of the \"" + plug.system().name() + "\" system", fault); } }); } @Override public void beforeDetach(final Plug plug) { if (logger.isInfoEnabled()) { logger.info("Unregistering event subscriptions of the \"{}\" system ...", plug.system().name()); } final var system = plug.system(); system.consume() .using(HttpJsonEventUnsubscribe.factory()) .ifSuccess(consumer -> { for (final var subscription : subscriptions) { consumer.unsubscribe(subscription.topic(), system) .onFailure(fault -> { if (logger.isWarnEnabled()) { logger.warn("Failed to unregister " + subscription, fault); } }); } }) .onFailure(fault -> { if (logger.isWarnEnabled()) { logger.warn("Failed to unregister the event subscriptions " + "of the \"" + plug.system().name() + "\" system", fault); } }); } @Override public void onDetach(final Plug plug) { if (logger.isInfoEnabled()) { logger.info("HTTP/JSON event subscription plugin detached from \"{}\"", plug.system().name()); } } @Override public void onDetach(final Plug plug, final Throwable cause) { if (logger.isErrorEnabled()) { logger.error("HTTP/JSON event subscription plugin forcibly " + "detached from \"" + plug.system().name() + "\"", cause); } } /** * Builder used to construct {@link HttpJsonEventSubscriptionPlugin} * instances. */ public static class Builder { private final Map subscriptions = new HashMap<>(); private String basePath; /** * Sets base path to be associated with {@link HttpService} that will * be set up to receive incoming events. Must be specified. * * @param basePath HTTP URI base path. * @return This builder. */ public Builder basePath(final String basePath) { if (basePath != null && (basePath.length() < 2 || basePath.charAt(0) != '/')) { throw new IllegalArgumentException("Valid base paths must " + "begin with a forward slash (/) and contain at least one " + "segment"); } this.basePath = basePath; return this; } /** * Adds new desired subscription to builder. * * @param topic Topic, or "eventType", that must be matched by * received events. Topics are case insensitive and can * only be subscribed to once. * @param handler Handler to receive matching events. * @return This builder. */ public Builder subscribe(final String topic, final ArEventSubscriptionHandler handler) { return subscribe(topic, null, null, handler); } /** * Adds new desired subscription to builder. * * @param topic Topic, or "eventType", that must be matched by * received events. Topics are case insensitive and can * only be subscribed to once. * @param metadata Metadata pairs that must be matched by received * events. * @param handler Handler to receive matching events. * @return This builder. */ public Builder subscribe( final String topic, final Map metadata, final ArEventSubscriptionHandler handler) { return subscribe(topic, metadata, null, handler); } /** * Adds new desired subscription to builder. * * @param topic Topic, or "eventType", that must be matched by * received events. Topics are case insensitive and * can only be subscribed to once. * @param providers Event providers to receive events from. * @param handler Handler to receive matching events. * @return This builder. */ public Builder subscribe( final String topic, final Collection providers, final ArEventSubscriptionHandler handler) { return subscribe(topic, null, providers, handler); } /** * Adds new desired subscription to builder. * * @param topic Topic, or "eventType", that must be matched by * received events. Topics are case insensitive and * can only be subscribed to once. * @param metadata Metadata pairs that must be matched by received * events. * @param providers Event providers to receive events from. * @param handler Handler to receive matching events. * @return This builder. */ public Builder subscribe( String topic, final Map metadata, final Collection providers, final ArEventSubscriptionHandler handler) { topic = Objects.requireNonNull(topic, "Expected topic").toLowerCase(); final var existing = subscriptions.putIfAbsent(topic, new ArEventSubscription.Builder() .topic(topic) .metadata(metadata) .providers(providers) .handler(handler) .build()); if (existing != null) { throw new IllegalArgumentException("A subscription already " + "exists for the topic \"" + topic + "\""); } return this; } /** * Finalizes construction of {@link HttpJsonEventSubscriptionPlugin}. * * @return New {@link HttpJsonEventSubscriptionPlugin}. */ public HttpJsonEventSubscriptionPlugin build() { return new HttpJsonEventSubscriptionPlugin(this); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy