/** * Copyright (c) Codice Foundation * <p> * This 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 3 of the * License, or any later version. * <p> * 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 * Lesser General Public License for more details. A copy of the GNU Lesser General Public License * is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. */ package ddf.catalog.pubsub; import java.net.URI; import java.util.Dictionary; import java.util.HashMap; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.apache.lucene.store.Directory; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceRegistration; import org.osgi.service.event.Event; import org.osgi.service.event.EventAdmin; import org.osgi.service.event.EventConstants; import org.osgi.service.event.EventHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ddf.catalog.CatalogFramework; import ddf.catalog.data.Metacard; import ddf.catalog.event.EventProcessor; import ddf.catalog.event.InvalidSubscriptionException; import ddf.catalog.event.Subscription; import ddf.catalog.event.SubscriptionExistsException; import ddf.catalog.event.SubscriptionNotFoundException; import ddf.catalog.operation.CreateResponse; import ddf.catalog.operation.DeleteResponse; import ddf.catalog.operation.Update; import ddf.catalog.operation.UpdateResponse; import ddf.catalog.plugin.PluginExecutionException; import ddf.catalog.plugin.PostIngestPlugin; import ddf.catalog.plugin.PreDeliveryPlugin; import ddf.catalog.plugin.PreSubscriptionPlugin; import ddf.catalog.pubsub.criteria.contextual.ContextualEvaluator; import ddf.catalog.pubsub.internal.PubSubConstants; import ddf.catalog.pubsub.internal.PubSubThread; import ddf.catalog.pubsub.internal.SubscriptionFilterVisitor; import ddf.catalog.pubsub.predicate.Predicate; import ddf.catalog.util.impl.Requests; public class EventProcessorImpl implements EventProcessor, EventHandler, PostIngestPlugin { public static final double EQUATORIAL_RADIUS_IN_METERS = 6378137.0; private static final Logger LOGGER = LoggerFactory.getLogger(EventProcessorImpl.class); protected EventAdmin eventAdmin; protected BundleContext bundleContext; protected List<PreSubscriptionPlugin> preSubscription; protected List<PreDeliveryPlugin> preDelivery; protected CatalogFramework catalog; private Map<String, ServiceRegistration> existingSubscriptions; private final ExecutorService threadPool = Executors.newCachedThreadPool(); public EventProcessorImpl() { LOGGER.debug("INSIDE: EventProcessorImpl default constructor"); } public EventProcessorImpl(BundleContext bundleContext, EventAdmin eventAdmin, List<PreSubscriptionPlugin> preSubscription, List<PreDeliveryPlugin> preDelivery, CatalogFramework catalog) { LOGGER.trace("ENTERING: EventProcessorImpl constructor"); this.bundleContext = bundleContext; this.eventAdmin = eventAdmin; this.preSubscription = preSubscription; this.preDelivery = preDelivery; this.catalog = catalog; this.existingSubscriptions = new HashMap<>(); if (this.preSubscription == null) { LOGGER.debug("preSubscription plugins list is NULL"); } else { LOGGER.debug("preSubscription plugin list size = {}", this.preSubscription.size()); } if (this.preDelivery == null) { LOGGER.debug("preDelivery plugins list is NULL"); } else { LOGGER.debug("preDelivery plugin list size = {}", this.preDelivery.size()); } LOGGER.trace("EXITING: EventProcessorImpl constructor"); } /** * Processes an entry by adding properties from the metacard to the event. Then the eventAdmin * is used to post the metacard properties as a single event. * * @param metacard - the metacard to process * @param operation - The type of event {@link ddf.catalog.pubsub.internal.PubSubConstants} * @param eventAdmin - OSGi EventAdmin service used post events */ public static void processEntry(Metacard metacard, String operation, EventAdmin eventAdmin) { String methodName = "processEntry"; LOGGER.debug("ENTERING: " + methodName); if (metacard != null) { LOGGER.debug("Input Metacard:{}\n", metacard.toString()); LOGGER.debug("catalog ID = {}", metacard.getId()); LOGGER.debug("operation = {}", operation); HashMap<String, Object> properties = new HashMap<>(3, 1); // Common headers properties.put(PubSubConstants.HEADER_OPERATION_KEY, operation); properties.put(PubSubConstants.HEADER_ENTRY_KEY, metacard); // ENTRY ID INFORMATION // TODO: probably don't need to pass this through since they can get the metacard properties.put(PubSubConstants.HEADER_ID_KEY, metacard.getId()); try { URI uri = metacard.getResourceURI(); if (uri != null) { String productUri = uri.toString(); LOGGER.debug( "Processing incoming entry. Adding DAD URI to event properties: {}", productUri); // TODO: probably just get this info from the Metacard, Probably don't need to // create new property for this properties.put(PubSubConstants.HEADER_DAD_KEY, productUri); } } catch (Exception e) { LOGGER.debug("Unable to obtain resource URL, will not be considered in subscription", e); } // CONTENT TYPE INFORMATION String type = metacard.getContentTypeName(); String contentType = "UNKNOWN"; if (type != null) { contentType = type; } else { LOGGER.debug("contentType is null"); } String version = metacard.getContentTypeVersion(); contentType = contentType + "," + (version == null ? "" : version); LOGGER.debug("contentType = {}", contentType); properties.put(PubSubConstants.HEADER_CONTENT_TYPE_KEY, contentType); // CONTEXTUAL INFORMATION if (metacard.getMetadata() != null) { try { // Build Lucene search index on entry's entire metadata using // default XPaths (specified // in ContextualEvaluator) - this index will be used by all // contextual predicates that do // *NOT* specify any textPaths. (Building index here optimizes // code so that this index is // not built for every contextual subscription that has no // textPaths.) Directory index = ContextualEvaluator.buildIndex(metacard.getMetadata()); // Build contextual info to be sent in event for this entry. // Include the default Lucene search // index and the entry's metadata (in case subscription has // textPaths, then it can create Lucene // search indices on the metadata using its textPaths) Map<String, Object> contextualMap = new HashMap<>(2, 1); contextualMap.put("DEFAULT_INDEX", index); contextualMap.put("METADATA", metacard.getMetadata()); properties.put(PubSubConstants.HEADER_CONTEXTUAL_KEY, contextualMap); } catch (Exception e) { LOGGER.info("Exception updating context map", e); } } if (eventAdmin != null) { eventAdmin.postEvent(new Event(PubSubConstants.PUBLISHED_EVENT_TOPIC_NAME, properties)); } else { LOGGER.debug("Unable to post event since eventAdmin is null."); } } else { LOGGER.debug("Unable to post null metacard."); } LOGGER.debug("EXITING: {}", methodName); } public void init() { String methodName = "init"; LOGGER.debug("ENTERING: {}", methodName); LOGGER.debug("EXITING: {}", methodName); } public void destroy() { String methodName = "destroy"; LOGGER.debug("ENTERING: {}", methodName); LOGGER.debug("EXITING: {}", methodName); } /** * By default the Felix EventAdmin implementation has a timeout of 5000 ms. Your event handler * has to return from the handle event method in this time frame. If it does not, it gets * Blacklisted. Therefore, this method processes its events in a separate thread than the * EventAdmin who called it. */ public void handleEvent(Event event) { String methodName = "handleEvent"; LOGGER.debug("ENTERING: {}", methodName); LOGGER.debug("Received event: {}", event.getTopic()); if (!existingSubscriptions.isEmpty()) { String topic = event.getTopic(); Metacard entry = (Metacard) event.getProperty(EventProcessor.EVENT_METACARD); LOGGER.debug("metacard ID = {}", entry.getId()); new PubSubThread(entry, topic, eventAdmin).start(); } else { LOGGER.debug( "No existing subscriptions, so no need to handle event since there is no one listening ..."); } LOGGER.debug("EXITING: {}", methodName); } @Override public String createSubscription(Subscription subscription) throws InvalidSubscriptionException { String uuid = UUID.randomUUID() .toString(); try { createSubscription(subscription, uuid); } catch (SubscriptionExistsException e) { // This is extremely unlikely to happen. A UUID should never match // another subscription ID LOGGER.debug("UUID matched previously registered subscription.", e); throw new InvalidSubscriptionException(e); } return uuid; } @Override public void createSubscription(Subscription subscription, String subscriptionId) throws InvalidSubscriptionException, SubscriptionExistsException { String methodName = "createSubscription"; LOGGER.debug("ENTERING: {}", methodName); LOGGER.debug("Creating Evaluation Criteria... "); try { for (PreSubscriptionPlugin plugin : preSubscription) { LOGGER.debug("Processing subscription with preSubscription plugin"); subscription = plugin.process(subscription); } SubscriptionFilterVisitor visitor = new SubscriptionFilterVisitor(); Predicate finalPredicate = (Predicate) subscription.accept(visitor, null); LOGGER.debug("predicate from filter visitor: {}", finalPredicate); String[] topics = new String[] {PubSubConstants.PUBLISHED_EVENT_TOPIC_NAME}; Dictionary<String, String[]> props = new Hashtable<>(1, 1); props.put(EventConstants.EVENT_TOPIC, topics); ServiceRegistration serviceRegistration = bundleContext.registerService(EventHandler.class.getName(), new PublishedEventHandler(finalPredicate, subscription, preDelivery, catalog, threadPool), props); existingSubscriptions.put(subscriptionId, serviceRegistration); LOGGER.debug("Subscription {} created.", subscriptionId); } catch (Exception e) { LOGGER.info("Error while creating subscription predicate: ", e); throw new InvalidSubscriptionException(e); } LOGGER.debug("EXITING: {}", methodName); } @Override public void updateSubscription(Subscription subscription, String subscriptionId) throws SubscriptionNotFoundException { String methodName = "updateSubscription"; LOGGER.debug("ENTERING: {}", methodName); try { deleteSubscription(subscriptionId); createSubscription(subscription, subscriptionId); LOGGER.debug("Updated {}", subscriptionId); } catch (Exception e) { LOGGER.info("Could not update subscription", e); throw new SubscriptionNotFoundException(e); } LOGGER.debug("EXITING: {}", methodName); } @Override public void deleteSubscription(String subscriptionId) throws SubscriptionNotFoundException { String methodName = "deleteSubscription"; LOGGER.debug("ENTERING: {}", methodName); try { LOGGER.debug("Removing subscription: {}", subscriptionId); ServiceRegistration sr = (ServiceRegistration) existingSubscriptions.get(subscriptionId); if (sr != null) { sr.unregister(); LOGGER.debug("Removal complete"); existingSubscriptions.remove(subscriptionId); } else { LOGGER.debug("Unable to find existing subscription: {}. May already be deleted.", subscriptionId); } } catch (Exception e) { LOGGER.debug("Could not delete subscription for {}", subscriptionId); LOGGER.info("Exception deleting subscription", e); } LOGGER.debug("EXITING: " + methodName); } @Override public void notifyCreated(Metacard newMetacard) { LOGGER.trace("ENTERING: notifyCreated"); postEvent(EventProcessor.EVENTS_TOPIC_CREATED, newMetacard, null); LOGGER.trace("EXITING: notifyCreated"); } @Override public void notifyUpdated(Metacard newMetacard, Metacard oldMetacard) { LOGGER.trace("ENTERING: notifyUpdated"); postEvent(EventProcessor.EVENTS_TOPIC_UPDATED, newMetacard, oldMetacard); LOGGER.trace("EXITING: notifyUpdated"); } @Override public void notifyDeleted(Metacard oldMetacard) { LOGGER.trace("ENTERING: notifyDeleted"); postEvent(EventProcessor.EVENTS_TOPIC_DELETED, oldMetacard, null); LOGGER.trace("EXITING: notifyDeleted"); } /** * Posts a Metacard to a given topic * * @param topic - The topic to post the event * @param card - The Metacard that will be posted to the topic */ protected void postEvent(String topic, Metacard card, Metacard oldCard) { String methodName = "postEvent"; LOGGER.debug("ENTERING: {}", methodName); LOGGER.debug("Posting to topic: {}", topic); Dictionary<String, Object> properties = new Hashtable<>(2, 1); properties.put(EventProcessor.EVENT_METACARD, card); properties.put(EventProcessor.EVENT_TIME, System.currentTimeMillis()); Event event = new Event(topic, properties); eventAdmin.postEvent(event); LOGGER.debug("EXITING: {}", methodName); } @Override public CreateResponse process(CreateResponse createResponse) throws PluginExecutionException { LOGGER.trace("ENTERING: process (CreateResponse"); if (Requests.isLocal(createResponse.getRequest())) { List<Metacard> createdMetacards = createResponse.getCreatedMetacards(); for (Metacard currMetacard : createdMetacards) { postEvent(EventProcessor.EVENTS_TOPIC_CREATED, currMetacard, null); } } LOGGER.trace("EXITING: process (CreateResponse)"); return createResponse; } @Override public UpdateResponse process(UpdateResponse updateResponse) throws PluginExecutionException { LOGGER.trace("ENTERING: process (UpdateResponse"); if (Requests.isLocal(updateResponse.getRequest())) { List<Update> updates = updateResponse.getUpdatedMetacards(); for (Update currUpdate : updates) { postEvent(EventProcessor.EVENTS_TOPIC_UPDATED, currUpdate.getNewMetacard(), currUpdate.getOldMetacard()); } } LOGGER.trace("EXITING: process (UpdateResponse)"); return updateResponse; } @Override public DeleteResponse process(DeleteResponse deleteResponse) throws PluginExecutionException { LOGGER.trace("ENTERING: process (DeleteResponse"); if (Requests.isLocal(deleteResponse.getRequest())) { List<Metacard> deletedMetacards = deleteResponse.getDeletedMetacards(); for (Metacard currMetacard : deletedMetacards) { postEvent(EventProcessor.EVENTS_TOPIC_DELETED, currMetacard, null); } } LOGGER.trace("EXITING: process (DeleteResponse)"); return deleteResponse; } public static enum DateType { modified, effective, expiration, created } }