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

io.serialized.client.feed.FeedClient Maven / Gradle / Ivy

package io.serialized.client.feed;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.serialized.client.SerializedClientConfig;
import io.serialized.client.SerializedOkHttpClient;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Response;
import org.apache.commons.lang3.Validate;

import java.io.Closeable;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.time.temporal.ValueRange;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;

import static java.lang.String.format;
import static java.util.Objects.requireNonNull;

public class FeedClient implements Closeable {

  private static final String SEQUENCE_NUMBER_HEADER = "Serialized-SequenceNumber-Current";
  private static final ValueRange SUBSCRIPTION_POLL_DELAY_VALUE_RANGE = ValueRange.of(1, 60);

  private final SerializedOkHttpClient client;
  private final HttpUrl apiRoot;
  private final Set executors = new HashSet<>();

  private FeedClient(Builder builder) {
    this.client = new SerializedOkHttpClient(builder.httpClient, builder.objectMapper);
    this.apiRoot = builder.apiRoot;
  }

  public static Builder feedClient(SerializedClientConfig config) {
    return new Builder(config);
  }

  public FeedRequest request() {
    return new FeedRequest();
  }

  public FeedRequest feed(String feedName) {
    return new FeedRequest(feedName);
  }

  public FeedRequest all() {
    return new FeedRequest("_all");
  }

  @Override
  public void close() {
    executors.forEach(ExecutorService::shutdown);
  }

  public class FeedRequest {

    private Integer limit;
    private String feedName;
    private Duration pollDelay = Duration.ofSeconds(1);
    private boolean eagerFetching = true;
    private UUID tenantId;

    private FeedRequest() {
    }

    private FeedRequest(String feedName) {
      this.feedName = feedName;
    }

    public FeedRequest withFeed(String feedName) {
      this.feedName = feedName;
      return this;
    }

    public FeedRequest withTenantId(UUID tenantId) {
      this.tenantId = tenantId;
      return this;
    }

    /**
     * @param limit Maximum number of returned feed entries per server response.
     */
    public FeedRequest limit(int limit) {
      this.limit = limit;
      return this;
    }

    /**
     * @param eagerFetching True if the client should continue to fetch event within the same poll as long as there
     *                      are more available. Default is true.
     */
    public FeedRequest eagerFetching(boolean eagerFetching) {
      this.eagerFetching = eagerFetching;
      return this;
    }

    /**
     * @param pollDelay Desired delay between feed polls. Must be between 1s and 60s. Default is 1s.
     */
    public FeedRequest subscriptionPollDelay(Duration pollDelay) {
      if (SUBSCRIPTION_POLL_DELAY_VALUE_RANGE.isValidValue(pollDelay.get(ChronoUnit.SECONDS))) {
        this.pollDelay = pollDelay;
        return this;
      } else {
        throw new IllegalArgumentException(format("Poll delay must be within %d and %d seconds",
            SUBSCRIPTION_POLL_DELAY_VALUE_RANGE.getMinimum(), SUBSCRIPTION_POLL_DELAY_VALUE_RANGE.getMaximum()));
      }
    }

    /**
     * Executes a poll starting at given sequence number.
     *
     * @param since Sequence number to start feeding from. Zero (0) starts from the beginning.
     */
    public FeedResponse execute(long since) {
      HttpUrl.Builder urlBuilder = url();
      Optional.ofNullable(limit).ifPresent(limit -> urlBuilder.addQueryParameter("limit", String.valueOf(limit)));
      HttpUrl url = urlBuilder.addQueryParameter("since", String.valueOf(since)).build();

      if (tenantId != null) {
        return client.get(url, FeedResponse.class, tenantId);
      } else {
        return client.get(url, FeedResponse.class);
      }
    }

    /**
     * Executes a poll starting at given sequence number.
     *
     * @param since            Sequence number to start feeding from. Zero (0) starts from the beginning.
     * @param feedEntryHandler Handler invoked for each received entry
     */
    public void execute(long since, FeedEntryHandler feedEntryHandler) {
      FeedResponse response;
      long offset = since;

      do {
        response = execute(offset);
        for (FeedEntry feedEntry : response.entries()) {
          feedEntryHandler.handle(feedEntry);
          offset = feedEntry.sequenceNumber();
        }
      } while (eagerFetching && response.hasMore());
    }

    /**
     * Starts subscribing to the feed starting at the beginning.
     *
     * @param feedEntryHandler Handler invoked for each received entry
     */
    public void subscribe(FeedEntryHandler feedEntryHandler) {
      subscribe(0, feedEntryHandler);
    }

    /**
     * Starts subscribing to the feed starting at given sequence number.
     *
     * @param feedEntryHandler Handler invoked for each received entry
     */
    public void subscribe(long since, FeedEntryHandler feedEntryHandler) {
      ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();

      final AtomicLong offset = new AtomicLong(since);
      executor.scheduleWithFixedDelay(() -> {
        FeedResponse response;

        do {
          response = execute(offset.get());
          for (FeedEntry feedEntry : response.entries()) {
            try {
              feedEntryHandler.handle(feedEntry);
              offset.set(feedEntry.sequenceNumber());
            } catch (RetryException e) {
              // Retry requested
            }
          }
        } while (eagerFetching && response.hasMore());

      }, pollDelay.getSeconds(), pollDelay.getSeconds(), TimeUnit.SECONDS);
      executors.add(executor);
    }

    /**
     * @return Feed names and details.
     */
    public List listFeeds() {
      HttpUrl url = apiRoot.newBuilder().addPathSegment("feeds").build();

      if (tenantId != null) {
        return client.get(url, FeedsResponse.class, tenantId).feeds();
      } else {
        return client.get(url, FeedsResponse.class).feeds();
      }
    }

    /**
     * Gets the current sequence number for current feed.
     * 

* Note that the 'all' feed has it's own global sequence. * * @return The current sequence number, i.e the sequence number of the most recently stored event batch. */ public long getCurrentSequenceNumber() { HttpUrl url = url().build(); Function func = response -> Long.parseLong(requireNonNull(response.header(SEQUENCE_NUMBER_HEADER))); if (tenantId != null) { return client.head(url, func, tenantId); } else { return client.head(url, func); } } private HttpUrl.Builder url() { Validate.notBlank(feedName, "No feed specified"); return apiRoot.newBuilder().addPathSegment("feeds").addPathSegment(feedName); } } public static class Builder { private final OkHttpClient httpClient; private final ObjectMapper objectMapper; private final HttpUrl apiRoot; Builder(SerializedClientConfig config) { this.httpClient = config.httpClient(); this.objectMapper = config.objectMapper(); this.apiRoot = config.apiRoot(); } public FeedClient build() { return new FeedClient(this); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy