org.mule.module.pubsubhubbub.handler.PublisherHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.mule.module.pubsubhubbub.handler.PublisherHandler.java

Source

/**
 * Mule PubSubHubbub Connector
 *
 * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
 *
 * The software in this package is published under the terms of the CPAL v1.0
 * license, a copy of which has been included with this distribution in the
 * LICENSE.txt file.
 */

package org.mule.module.pubsubhubbub.handler;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeoutException;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.resource.spi.work.Work;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mule.api.MuleContext;
import org.mule.api.MuleMessage;
import org.mule.api.retry.RetryCallback;
import org.mule.api.retry.RetryContext;
import org.mule.api.retry.RetryPolicyTemplate;
import org.mule.module.pubsubhubbub.Constants;
import org.mule.module.pubsubhubbub.PuSHResponse;
import org.mule.module.pubsubhubbub.Utils;
import org.mule.module.pubsubhubbub.data.DataStore;
import org.mule.module.pubsubhubbub.data.TopicSubscription;
import org.mule.module.pubsubhubbub.rome.PerRequestUserAgentHttpClientFeedFetcher;
import org.mule.transport.http.HttpConnector;
import org.mule.transport.http.HttpConstants;

import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.fetcher.FeedFetcher;
import com.sun.syndication.fetcher.FetcherEvent;
import com.sun.syndication.fetcher.FetcherListener;
import com.sun.syndication.io.FeedException;
import com.sun.syndication.io.WireFeedOutput;

public class PublisherHandler extends AbstractHubActionHandler implements FetcherListener {
    public static class ContentFetchWork implements Work {
        private static final Log LOG = LogFactory.getLog(ContentFetchWork.class);

        private final DataStore dataStore;
        private final FeedFetcher feedFetcher;
        private final URI hubUrl;

        protected ContentFetchWork(final DataStore dataStore, final FeedFetcher feedFetcher, final URI hubUrl) {
            this.dataStore = dataStore;
            this.feedFetcher = feedFetcher;
            this.hubUrl = hubUrl;
        }

        public void run() {
            // we ignore the result here and rely on the feed fetcher event EVENT_TYPE_FEED_RETRIEVED to fire if the
            // feed has been actually retrieved from the web instead of being just read from cache
            try {
                PerRequestUserAgentHttpClientFeedFetcher.setRequestUserAgent(String
                        .format(Constants.USER_AGENT_FORMAT, hubUrl, dataStore.getTotalSubscriberCount(hubUrl)));
                feedFetcher.retrieveFeed(hubUrl.toURL());
            } catch (final Exception e) {
                LOG.error("Failed to fetch content from: " + hubUrl, e);
            } finally {
                PerRequestUserAgentHttpClientFeedFetcher.removeRequestUserAgent();
            }
        }

        public void release() {
            // NOOP
        }
    }

    public static class DistributeContentRetryCallback implements RetryCallback {
        private static final Log LOG = LogFactory.getLog(DistributeContentRetryCallback.class);

        private final MuleContext muleContext;
        private final DataStore dataStore;
        protected final ContentDistributionContext contentDistributionContext;

        protected DistributeContentRetryCallback(final MuleContext muleContext, final DataStore dataStore,
                final ContentDistributionContext contentDistributionContext) {
            this.muleContext = muleContext;
            this.dataStore = dataStore;
            this.contentDistributionContext = contentDistributionContext;
        }

        public String getWorkDescription() {
            return "Distributing content to " + contentDistributionContext.getCallbackUrl();
        }

        public void doWork(final RetryContext context) throws Exception {
            final Map<String, Object> headers = new HashMap<String, Object>();
            addHeaders(headers);

            final MuleMessage response = muleContext.getClient().send(
                    contentDistributionContext.getCallbackUrl().toString(), contentDistributionContext.getPayload(),
                    headers, (int) Constants.SUBSCRIBER_TIMEOUT_MILLIS);

            if (response == null) {
                throw new TimeoutException(
                        "Failed to send content to: " + contentDistributionContext.getCallbackUrl());
            }

            final String getResponseStatusCode = response.getInboundProperty(HttpConnector.HTTP_STATUS_PROPERTY,
                    "nil");

            if (!StringUtils.startsWith(getResponseStatusCode, "2")) {
                throw new IllegalArgumentException("Received status " + getResponseStatusCode + " from: "
                        + contentDistributionContext.getCallbackUrl());
            }

            final String onBehalfOf = response.getInboundProperty(Constants.HUB_ON_BEHALF_OF_HEADER, "");
            if (StringUtils.isNotBlank(onBehalfOf)) {
                final int subscriberCount = Integer.valueOf(onBehalfOf);
                dataStore.storeSubscriberCount(contentDistributionContext.getTopicUrl(),
                        contentDistributionContext.getCallbackUrl(), subscriberCount);
                LOG.info("Successfully distributed content to " + subscriberCount + " subscriber(s) at: "
                        + contentDistributionContext.getCallbackUrl());
            } else {
                LOG.info("Successfully distributed content to: " + contentDistributionContext.getCallbackUrl());
            }
        }

        protected void addHeaders(final Map<String, Object> headers) {
            headers.put(HttpConstants.HEADER_CONTENT_TYPE, contentDistributionContext.getContentType());
        }
    }

    public static final class DistributeAuthenticatedContentRetryCallback extends DistributeContentRetryCallback {
        private final String signature;

        protected DistributeAuthenticatedContentRetryCallback(final MuleContext muleContext,
                final DataStore dataStore, final ContentDistributionContext contentDistributionContext,
                final byte[] secret) throws Exception {
            super(muleContext, dataStore, contentDistributionContext);
            signature = computeSignature(secret);
        }

        @Override
        protected void addHeaders(final Map<String, Object> headers) {
            super.addHeaders(headers);
            headers.put(Constants.HUB_SIGNATURE_HEADER, "sha1=" + signature);
        }

        private String computeSignature(final byte[] secret)
                throws GeneralSecurityException, UnsupportedEncodingException {
            final SecretKeySpec secretKey = new SecretKeySpec(secret, "HmacSHA1");
            final Mac mac = Mac.getInstance("HmacSHA1");
            mac.init(secretKey);
            final byte[] rawHmac = mac.doFinal(contentDistributionContext.getPayload().getBytes());
            return new String(Base64.encodeBase64(rawHmac));
        }
    }

    public static class ContentDistributionContext {
        private final URI topicUrl;
        private final String contentType;
        private final String payload;
        private final URI callbackUrl;

        protected ContentDistributionContext(final URI topicUrl, final String contentType, final String payload,
                final URI callbackUrl) {
            this.topicUrl = topicUrl;
            this.contentType = contentType;
            this.payload = payload;
            this.callbackUrl = callbackUrl;
        }

        public URI getTopicUrl() {
            return topicUrl;
        }

        public String getContentType() {
            return contentType;
        }

        public String getPayload() {
            return payload;
        }

        public URI getCallbackUrl() {
            return callbackUrl;
        }
    }

    private final FeedFetcher feedFetcher;

    public PublisherHandler(final MuleContext muleContext, final DataStore dataStore,
            final RetryPolicyTemplate retryPolicyTemplate) {
        super(muleContext, dataStore, retryPolicyTemplate);

        feedFetcher = new PerRequestUserAgentHttpClientFeedFetcher(dataStore);
        feedFetcher.setPreserveWireFeed(true);
        feedFetcher.addFetcherEventListener(this);
    }

    @Override
    public PuSHResponse handle(final Map<String, List<String>> formParams) {
        final List<URI> hubUrls = Utils.getMandatoryUrlParameters(Constants.HUB_URL_PARAM, formParams);
        for (final URI hubUrl : hubUrls) {
            try {
                getMuleContext().getWorkManager()
                        .scheduleWork(new ContentFetchWork(getDataStore(), feedFetcher, hubUrl));
            } catch (final Exception e) {
                final String errorMessage = "Failed to schedule content fetch for: " + hubUrl;
                getLogger().error(errorMessage, e);
                return PuSHResponse.serverError(errorMessage);
            }
        }

        return PuSHResponse.noContent();
    }

    public void fetcherEvent(final FetcherEvent event) {
        if (StringUtils.equals(event.getEventType(), FetcherEvent.EVENT_TYPE_FEED_UNCHANGED)) {
            getLogger().info("Content distribution skipped for unchanged feed: " + event.getUrlString());
            return;
        }

        if (!StringUtils.equals(event.getEventType(), FetcherEvent.EVENT_TYPE_FEED_RETRIEVED)) {
            return;
        }

        try {
            final URI topicUrl = new URI(event.getUrlString());
            processRetrievedFeed(topicUrl, event.getFeed());
        } catch (final Exception e) {
            getLogger().error("Failed to process feed retrieved from: " + event.getUrlString(), e);
        }
    }

    private void processRetrievedFeed(final URI topicUrl, final SyndFeed feed) throws Exception {
        final List<SyndEntry> newEntries = findNewFeedEntries(topicUrl, feed);

        if (newEntries.isEmpty()) {
            getLogger().info(
                    "Publisher has requested content distribution but no new feed entries have been found for: "
                            + topicUrl);
            return;
        }

        final Set<TopicSubscription> topicSubscriptions = getDataStore().getTopicSubscriptions(topicUrl);
        if (!topicSubscriptions.isEmpty()) {
            final String payload = createDistributedPayload(feed, newEntries);
            final String contentType = getDistributedContentType(feed);
            distributeContent(topicUrl, contentType, payload, topicSubscriptions);
        } else {
            getLogger().info("No subscriber found for content distribution of: " + topicUrl);
        }

        storeNewFeedEntries(topicUrl, newEntries);
    }

    private String getDistributedContentType(final SyndFeed feed) {
        return StringUtils.containsIgnoreCase(feed.getFeedType(), "rss") ? Constants.RSS_CONTENT_TYPE
                : Constants.ATOM_CONTENT_TYPE;
    }

    private String createDistributedPayload(final SyndFeed templateFeed, final List<SyndEntry> newEntries)
            throws CloneNotSupportedException, FeedException {
        final SyndFeed cloned = (SyndFeed) templateFeed.clone();
        cloned.setEntries(newEntries);
        return new WireFeedOutput().outputString(cloned.createWireFeed());
    }

    private void storeNewFeedEntries(final URI topicUrl, final List<SyndEntry> newEntries) {
        for (final SyndEntry entry : newEntries) {
            getDataStore().storeTopicFeedId(topicUrl, getFeedEntryId(entry));
        }
    }

    private List<SyndEntry> findNewFeedEntries(final URI topicUrl, final SyndFeed feed) {
        final Set<String> knownFeedIds = getDataStore().getTopicFeedIds(topicUrl);
        final List<SyndEntry> newEntries = new ArrayList<SyndEntry>();
        @SuppressWarnings("unchecked")
        final List<SyndEntry> retrievedEntries = feed.getEntries();
        for (final SyndEntry entry : retrievedEntries) {
            if (!knownFeedIds.contains(getFeedEntryId(entry))) {
                newEntries.add(entry);
            }
        }
        return newEntries;
    }

    private String getFeedEntryId(final SyndEntry entry) {
        // we use the URI as feed entry ID as it is present for both RSS and ATOM entries
        return entry.getUri();
    }

    private void distributeContent(final URI topicUrl, final String contentType, final String payload,
            final Set<TopicSubscription> topicSubscriptions) throws Exception {
        for (final TopicSubscription topicSubscription : topicSubscriptions) {
            final ContentDistributionContext contentDistributionContext = new ContentDistributionContext(topicUrl,
                    contentType, payload, topicSubscription.getCallbackUrl());

            if (topicSubscription.getSecret() != null) {
                getRetryPolicyTemplate().execute(
                        new DistributeAuthenticatedContentRetryCallback(getMuleContext(), getDataStore(),
                                contentDistributionContext, topicSubscription.getSecret()),
                        getMuleContext().getWorkManager());
            } else {
                getRetryPolicyTemplate().execute(new DistributeContentRetryCallback(getMuleContext(),
                        getDataStore(), contentDistributionContext), getMuleContext().getWorkManager());
            }
        }
    }
}