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

io.hyperfoil.http.steps.HttpResponseHandlersImpl Maven / Gradle / Ivy

There is a newer version: 0.27.1
Show newest version
package io.hyperfoil.http.steps;

import static io.hyperfoil.core.session.SessionFactory.access;
import static io.hyperfoil.core.session.SessionFactory.sequenceScopedAccess;

import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntFunction;

import io.hyperfoil.api.config.BuilderBase;
import io.hyperfoil.api.config.Locator;
import io.hyperfoil.api.config.Rewritable;
import io.hyperfoil.api.config.SequenceBuilder;
import io.hyperfoil.api.config.StepBuilder;
import io.hyperfoil.http.api.HttpCache;
import io.hyperfoil.http.api.HttpRequest;
import io.hyperfoil.http.api.FollowRedirect;
import io.hyperfoil.http.api.HeaderHandler;
import io.hyperfoil.http.api.HttpResponseHandlers;
import io.hyperfoil.api.processor.RawBytesHandler;
import io.hyperfoil.api.processor.HttpRequestProcessorBuilder;
import io.hyperfoil.api.processor.Processor;
import io.hyperfoil.api.session.Access;
import io.hyperfoil.api.session.Action;
import io.hyperfoil.api.session.ResourceUtilizer;
import io.hyperfoil.api.session.Session;
import io.hyperfoil.api.session.SessionStopException;
import io.hyperfoil.core.builders.ServiceLoadedBuilderProvider;
import io.hyperfoil.core.data.LimitedPoolResource;
import io.hyperfoil.core.data.Queue;
import io.hyperfoil.core.handlers.ConditionalAction;
import io.hyperfoil.core.handlers.ConditionalProcessor;
import io.hyperfoil.http.api.StatusHandler;
import io.hyperfoil.http.config.HttpErgonomics;
import io.hyperfoil.http.config.HttpPluginBuilder;
import io.hyperfoil.http.html.HtmlHandler;
import io.hyperfoil.http.html.MetaRefreshHandler;
import io.hyperfoil.http.html.RefreshHandler;
import io.hyperfoil.http.handlers.ConditionalHeaderHandler;
import io.hyperfoil.http.handlers.Location;
import io.hyperfoil.http.handlers.RangeStatusValidator;
import io.hyperfoil.http.handlers.Redirect;
import io.hyperfoil.core.steps.AwaitDelayStep;
import io.hyperfoil.core.steps.PushQueueAction;
import io.hyperfoil.core.steps.ScheduleDelayStep;
import io.hyperfoil.core.util.Unique;
import io.hyperfoil.function.SerializableToLongFunction;
import io.hyperfoil.http.cookie.CookieRecorder;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.util.AsciiString;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;

public class HttpResponseHandlersImpl implements HttpResponseHandlers, ResourceUtilizer, Serializable {
   private static final Logger log = LoggerFactory.getLogger(HttpResponseHandlersImpl.class);
   private static final boolean trace = log.isTraceEnabled();

   final StatusHandler[] statusHandlers;
   final HeaderHandler[] headerHandlers;
   final Processor[] bodyHandlers;
   final Action[] completionHandlers;
   final RawBytesHandler[] rawBytesHandlers;

   private HttpResponseHandlersImpl(StatusHandler[] statusHandlers,
                                    HeaderHandler[] headerHandlers,
                                    Processor[] bodyHandlers,
                                    Action[] completionHandlers,
                                    RawBytesHandler[] rawBytesHandlers) {
      this.statusHandlers = statusHandlers;
      this.headerHandlers = headerHandlers;
      this.bodyHandlers = bodyHandlers;
      this.completionHandlers = completionHandlers;
      this.rawBytesHandlers = rawBytesHandlers;
   }

   @Override
   public void handleStatus(HttpRequest request, int status, String reason) {
      Session session = request.session;
      if (request.isCompleted()) {
         if (trace) {
            log.trace("#{} Ignoring status {} as the request has been marked completed (failed).", session.uniqueId(), status);
         }
         return;
      }

      if (trace) {
         log.trace("#{} Received status {}: {}", session.uniqueId(), status, reason);
      }

      try {
         switch (request.method) {
            case GET:
            case HEAD:
               // TODO: should we store 203, 300, 301 and 410?
               // TODO: partial response 206
               if (status != 200) {
                  request.cacheControl.noStore = true;
               }
               break;
            case POST:
            case PUT:
            case DELETE:
            case PATCH:
               if (status >= 200 && status <= 399) {
                  HttpCache.get(request.session).invalidate(request.authority, request.path);
                  request.cacheControl.invalidate = true;
               }
               request.cacheControl.noStore = true;
               break;
         }

         request.statistics().addStatus(request.startTimestampMillis(), status);
         if (statusHandlers != null) {
            for (StatusHandler handler : statusHandlers) {
               handler.handleStatus(request, status);
            }
         }

         if (headerHandlers != null) {
            for (HeaderHandler handler : headerHandlers) {
               handler.beforeHeaders(request);
            }
         }
         if (bodyHandlers != null) {
            for (Processor handler : bodyHandlers) {
               handler.before(request.session);
            }
         }
      } catch (SessionStopException e) {
         throw e;
      } catch (Throwable t) {
         log.error("#{} Response status processing failed on {}", t, session.uniqueId(), this);
         request.statistics().incrementInternalErrors(request.startTimestampMillis());
         request.markInvalid();
         session.stop();
      }
   }

   @Override
   public void handleHeader(HttpRequest request, CharSequence header, CharSequence value) {
      Session session = request.session;
      if (request.isCompleted()) {
         if (trace) {
            log.trace("#{} Ignoring header on a failed request: {}: {}", session.uniqueId(), header, value);
         }
         return;
      }
      if (trace) {
         log.trace("#{} Received header {}: {}", session.uniqueId(), header, value);
      }
      try {
         HttpCache httpCache = HttpCache.get(session);
         if (request.cacheControl.invalidate) {
            if (AsciiString.contentEqualsIgnoreCase(header, HttpHeaderNames.LOCATION)
                  || AsciiString.contentEqualsIgnoreCase(header, HttpHeaderNames.CONTENT_LOCATION)) {
               httpCache.invalidate(request.authority, value);
            }
         }
         if (headerHandlers != null) {
            for (HeaderHandler handler : headerHandlers) {
               handler.handleHeader(request, header, value);
            }
         }
         httpCache.responseHeader(request, header, value);
      } catch (SessionStopException e) {
         throw e;
      } catch (Throwable t) {
         log.error("#{} Response header processing failed on {}", t, session.uniqueId(), this);
         request.statistics().incrementInternalErrors(request.startTimestampMillis());
         request.markInvalid();
         session.stop();
      }
   }

   @Override
   public void handleThrowable(HttpRequest request, Throwable throwable) {
      Session session = request.session;
      if (trace) {
         log.trace("#{} {} Received exception", throwable, session.uniqueId(), request);
      }
      if (request.isCompleted()) {
         if (trace) {
            log.trace("#{} Request has been already completed", session.uniqueId());
         }
         return;
      }
      if (request.isValid()) {
         // Do not mark as invalid when timed out
         request.markInvalid();
      }

      try {
         if (request.isRunning()) {
            request.statistics().incrementResets(request.startTimestampMillis());
            request.setCompleting();
            if (completionHandlers != null) {
               for (Action handler : completionHandlers) {
                  handler.run(session);
               }
            }
         }
      } catch (SessionStopException e) {
         throw e;
      } catch (Throwable t) {
         t.addSuppressed(throwable);
         log.error("#{} Exception {} thrown while handling another exception: ", t, session.uniqueId(), throwable.toString());
         request.statistics().incrementInternalErrors(request.startTimestampMillis());
         session.stop();
      } finally {
         request.setCompleted();
      }
   }

   @Override
   public void handleBodyPart(HttpRequest request, ByteBuf data, int offset, int length, boolean isLastPart) {
      Session session = request.session;
      if (request.isCompleted()) {
         if (trace) {
            log.trace("#{} Ignoring body part ({} bytes) on a failed request.", session.uniqueId(), data.readableBytes());
         }
         return;
      }

      if (trace) {
         log.trace("#{} Received part ({} bytes):\n{}", session.uniqueId(), length,
               data.toString(offset, length, StandardCharsets.UTF_8));
      }

      try {
         int dataStartIndex = data.readerIndex();
         if (bodyHandlers != null) {
            for (Processor handler : bodyHandlers) {
               handler.process(request.session, data, offset, length, isLastPart);
               data.readerIndex(dataStartIndex);
            }
         }
      } catch (SessionStopException e) {
         throw e;
      } catch (Throwable t) {
         log.error("#{} Response body processing failed on {}", t, session.uniqueId(), this);
         request.statistics().incrementInternalErrors(request.startTimestampMillis());
         request.markInvalid();
         session.stop();
      }
   }

   @Override
   public void handleEnd(HttpRequest request, boolean executed) {
      Session session = request.session;
      if (request.isCompleted()) {
         if (trace) {
            log.trace("#{} Request has been already completed.", session.uniqueId());
         }
         return;
      }
      if (trace) {
         log.trace("#{} Completed request on {}", session.uniqueId(), request.connection());
      }

      try {
         if (executed) {
            request.recordResponse(System.nanoTime());

            if (headerHandlers != null) {
               for (HeaderHandler handler : headerHandlers) {
                  handler.afterHeaders(request);
               }
            }
            if (bodyHandlers != null) {
               for (Processor handler : bodyHandlers) {
                  handler.after(request.session);
               }
            }
            HttpCache.get(request.session).tryStore(request);
         }

         if (request.isRunning()) {
            request.setCompleting();
            if (completionHandlers != null) {
               for (Action handler : completionHandlers) {
                  handler.run(session);
               }
            }
         }
      } catch (SessionStopException e) {
         throw e;
      } catch (Throwable t) {
         log.error("#{} Response completion failed on {}, stopping the session.", t, request.session.uniqueId(), this);
         request.statistics().incrementInternalErrors(request.startTimestampMillis());
         request.markInvalid();
         session.stop();
      } finally {
         // When one of the handlers calls Session.stop() it may terminate the phase completely,
         // sending stats before this finally-block may run. In that case this request gets completed
         // when cancelling all the request (including the current request) and we record the invalid
         // request directly there.
         if (executed && !request.isValid() && !request.isCompleted()) {
            request.statistics().addInvalid(request.startTimestampMillis());
         }
         request.setCompleted();
      }
   }

   @Override
   public void handleRawRequest(HttpRequest request, ByteBuf data, int offset, int length) {
      if (rawBytesHandlers == null) {
         return;
      }
      try {
         for (RawBytesHandler rawBytesHandler : rawBytesHandlers) {
            rawBytesHandler.onRequest(request, data, offset, length);
         }
      } catch (SessionStopException e) {
         throw e;
      } catch (Throwable t) {
         log.error("#{} Raw request processing failed on {}", t, request.session.uniqueId(), this);
         request.markInvalid();
         request.session.stop();
      }
   }

   @Override
   public void handleRawResponse(HttpRequest request, ByteBuf data, int offset, int length, boolean isLastPart) {
      if (rawBytesHandlers == null) {
         return;
      }
      try {
         for (RawBytesHandler rawBytesHandler : rawBytesHandlers) {
            rawBytesHandler.onResponse(request, data, offset, length, isLastPart);
         }
      } catch (SessionStopException e) {
         throw e;
      } catch (Throwable t) {
         log.error("#{} Raw response processing failed on {}", t, request.session.uniqueId(), this);
         request.markInvalid();
         request.session.stop();
      }
   }

   @Override
   public void reserve(Session session) {
      ResourceUtilizer.reserve(session, (Object[]) statusHandlers);
      ResourceUtilizer.reserve(session, (Object[]) headerHandlers);
      ResourceUtilizer.reserve(session, (Object[]) bodyHandlers);
      ResourceUtilizer.reserve(session, (Object[]) completionHandlers);
      ResourceUtilizer.reserve(session, (Object[]) rawBytesHandlers);
   }

   /**
    * Manages processing of HTTP responses.
    */
   public static class Builder implements Rewritable {
      private final HttpRequestStep.Builder parent;
      private Boolean autoRangeCheck;
      private Boolean stopOnInvalid;
      private FollowRedirect followRedirect;
      private List statusHandlers = new ArrayList<>();
      private List headerHandlers = new ArrayList<>();
      private List bodyHandlers = new ArrayList<>();
      private List completionHandlers = new ArrayList<>();
      private List rawBytesHandlers = new ArrayList<>();

      public static Builder forTesting() {
         return new Builder(null);
      }

      Builder(HttpRequestStep.Builder parent) {
         this.parent = parent;
      }

      public Builder status(StatusHandler.Builder builder) {
         statusHandlers.add(builder);
         return this;
      }

      public Builder status(StatusHandler handler) {
         statusHandlers.add(() -> handler);
         return this;
      }

      /**
       * Handle HTTP response status.
       *
       * @return Builder.
       */
      public ServiceLoadedBuilderProvider status() {
         return new ServiceLoadedBuilderProvider<>(StatusHandler.Builder.class, statusHandlers::add);
      }

      public Builder header(HeaderHandler handler) {
         return header(() -> handler);
      }

      public Builder header(HeaderHandler.Builder builder) {
         headerHandlers.add(builder);
         return this;
      }

      /**
       * Handle HTTP response headers.
       *
       * @return Builder.
       */
      public ServiceLoadedBuilderProvider header() {
         return new ServiceLoadedBuilderProvider<>(HeaderHandler.Builder.class, headerHandlers::add);
      }

      public Builder body(HttpRequestProcessorBuilder builder) {
         bodyHandlers.add(builder);
         return this;
      }

      /**
       * Handle HTTP response body.
       *
       * @return Builder.
       */
      public ServiceLoadedBuilderProvider body() {
         return new ServiceLoadedBuilderProvider<>(HttpRequestProcessorBuilder.class, bodyHandlers::add);
      }

      public Builder onCompletion(Action handler) {
         return onCompletion(() -> handler);
      }

      public Builder onCompletion(Action.Builder builder) {
         completionHandlers.add(builder);
         return this;
      }

      /**
       * Action executed when the HTTP response is fully received.
       *
       * @return Builder.
       */
      public ServiceLoadedBuilderProvider onCompletion() {
         return new ServiceLoadedBuilderProvider<>(Action.Builder.class, completionHandlers::add);
      }

      public Builder rawBytes(RawBytesHandler handler) {
         rawBytesHandlers.add(() -> handler);
         return this;
      }

      public Builder rawBytes(RawBytesHandler.Builder builder) {
         rawBytesHandlers.add(builder);
         return this;
      }

      /**
       * Handler processing HTTP response before parsing.
       *
       * @return Builder.
       */
      public ServiceLoadedBuilderProvider rawBytes() {
         return new ServiceLoadedBuilderProvider<>(RawBytesHandler.Builder.class, this::rawBytes);
      }

      /**
       * Inject status handler that marks the request as invalid on status 4xx or 5xx.
       * Default value depends on ergonomics.autoRangeCheck
       * (see User Guide).
       *
       * @param autoRangeCheck True for inserting the handler, false otherwise.
       * @return Self.
       */
      public Builder autoRangeCheck(boolean autoRangeCheck) {
         this.autoRangeCheck = autoRangeCheck;
         return this;
      }

      /**
       * Inject completion handler that will stop the session if the request has been marked as invalid.
       * Default value depends on ergonomics.stopOnInvalid
       * (see User Guide).
       *
       * @param stopOnInvalid
       * @return
       */
      public Builder stopOnInvalid(boolean stopOnInvalid) {
         this.stopOnInvalid = stopOnInvalid;
         return this;
      }

      /**
       * Automatically fire requests when the server responds with redirection.
       * Default value depends on ergonomics.followRedirect
       * (see User Guide).
       *
       * @param followRedirect Types of server response that will trigger the request.
       * @return Self.
       */
      public Builder followRedirect(FollowRedirect followRedirect) {
         this.followRedirect = followRedirect;
         return this;
      }

      public HttpRequestStep.Builder endHandler() {
         return parent;
      }

      public Builder wrapBodyHandlers(Function, HttpRequestProcessorBuilder> func) {
         HttpRequestProcessorBuilder wrapped = func.apply(bodyHandlers);
         this.bodyHandlers = new ArrayList<>();
         this.bodyHandlers.add(wrapped);
         return this;
      }

      public void prepareBuild() {
         HttpErgonomics ergonomics = Locator.current().benchmark().plugin(HttpPluginBuilder.class).ergonomics();
         if (ergonomics.repeatCookies()) {
            header(new CookieRecorder());
         }
         // TODO: we might need defensive copies here
         statusHandlers.forEach(StatusHandler.Builder::prepareBuild);
         headerHandlers.forEach(HeaderHandler.Builder::prepareBuild);
         bodyHandlers.forEach(HttpRequestProcessorBuilder::prepareBuild);
         completionHandlers.forEach(Action.Builder::prepareBuild);
         rawBytesHandlers.forEach(RawBytesHandler.Builder::prepareBuild);
         if (autoRangeCheck != null ? autoRangeCheck : ergonomics.autoRangeCheck()) {
            statusHandlers.add(new RangeStatusValidator.Builder().min(200).max(399));
         }
         // We must add this as the very last action since after calling session.stop() there other handlers won't be called
         if (stopOnInvalid != null ? stopOnInvalid : ergonomics.stopOnInvalid()) {
            completionHandlers.add(() -> StopOnInvalidAction.INSTANCE);
         }
         FollowRedirect followRedirect = this.followRedirect != null ? this.followRedirect : ergonomics.followRedirect();
         switch (followRedirect) {
            case LOCATION_ONLY:
               applyRedirect(true, false);
               break;
            case HTML_ONLY:
               applyRedirect(false, true);
               break;
            case ALWAYS:
               applyRedirect(true, true);
               break;
         }
      }

      private void applyRedirect(boolean location, boolean html) {
         Locator locator = Locator.current();
         // Not sequence-scoped as queue output var, requesting sequence-scoped access explicitly where needed
         Unique coordsVar = new Unique();

         String redirectSequenceName = String.format("%s_redirect_%08x",
               locator.sequence().name(), ThreadLocalRandom.current().nextInt());
         String delaySequenceName = String.format("%s_delay_%08x",
               locator.sequence().name(), ThreadLocalRandom.current().nextInt());

         Queue.Key queueKey = new Queue.Key();
         Queue.Key delayedQueueKey = new Queue.Key();
         Unique delayedCoordVar = new Unique();

         int concurrency = Math.max(1, locator.sequence().rootSequence().concurrency());

         if (html) {
            Unique delay = new Unique();
            SequenceBuilder delaySequence = locator.scenario().sequence(delaySequenceName);
            delaySequence.concurrency(Math.max(1, concurrency))
                  .step(() -> {
                     Access inputVar = sequenceScopedAccess(delayedCoordVar);
                     Access delayVar = sequenceScopedAccess(delay);
                     SerializableToLongFunction delayFunc = session -> TimeUnit.SECONDS.toMillis(((Redirect.Coords) inputVar.getObject(session)).delay);
                     return new ScheduleDelayStep(delayVar, ScheduleDelayStep.Type.FROM_NOW, delayFunc);
                  })
                  .step(() -> new AwaitDelayStep(sequenceScopedAccess(delay)))
                  .stepBuilder(new StepBuilder.ActionAdapter(() -> new PushQueueAction(
                        sequenceScopedAccess(delayedCoordVar), queueKey)))
                  .step(session -> {
                     session.getResource(delayedQueueKey).consumed(session);
                     return true;
                  });
            Locator.push(null, delaySequence);
            delaySequence.prepareBuild();
            Locator.pop();
         }

         // The redirect sequence (and queue) needs to have twice the concurrency of the original sequence
         // because while executing one redirect it might activate a second redirect
         int redirectConcurrency = 2 * concurrency;
         LimitedPoolResource.Key poolKey = new LimitedPoolResource.Key<>();

         {
            // Note: there's a lot of copy-paste between current handler because we need to use
            // different method variable for current sequence and new sequence since these have incompatible
            // indices - had we used the same var one sequence would overwrite other's var.
            Unique newTempCoordsVar = new Unique(true);
            HttpRequestStep.BodyGeneratorBuilder bodyBuilder = parent.bodyBuilder();
            HttpRequestStep.Builder httpRequest = new HttpRequestStep.Builder()
                  .method(() -> new Redirect.GetMethod(sequenceScopedAccess(coordsVar)))
                  .path(() -> new Location.GetPath(sequenceScopedAccess(coordsVar)))
                  .authority(() -> new Location.GetAuthority(sequenceScopedAccess(coordsVar)))
                  .headerAppenders(parent.headerAppenders())
                  .body(bodyBuilder == null ? null : bodyBuilder.copy())
                  .sync(false)
                  // we want to reuse the same sequence for subsequent requests
                  .handler().followRedirect(FollowRedirect.NEVER).endHandler();
            if (location) {
               httpRequest.handler()
                     .status(new Redirect.StatusHandler.Builder()
                           .poolKey(poolKey).concurrency(redirectConcurrency).coordsVar(newTempCoordsVar).handlers(statusHandlers))
                     .header(new Redirect.LocationRecorder.Builder()
                           .originalSequenceSupplier(() -> new Redirect.GetOriginalSequence(sequenceScopedAccess(coordsVar)))
                           .concurrency(redirectConcurrency).inputVar(newTempCoordsVar).outputVar(coordsVar)
                           .queueKey(queueKey).sequence(redirectSequenceName));
            }
            if (!headerHandlers.isEmpty()) {
               Redirect.WrappingHeaderHandler.Builder wrappingHandler = new Redirect.WrappingHeaderHandler.Builder().coordVar(coordsVar).handlers(headerHandlers);
               if (location) {
                  httpRequest.handler().header(new ConditionalHeaderHandler.Builder()
                        .condition().stringCondition().fromVar(newTempCoordsVar).isSet(false).end()
                        .handler(wrappingHandler));
               } else {
                  httpRequest.handler().header(wrappingHandler);
               }
            }
            if (!bodyHandlers.isEmpty() || html) {
               Consumer handlerConsumer;
               ConditionalProcessor.Builder conditionalBodyHandler;
               Redirect.WrappingProcessor.Builder wrappingProcessor = new Redirect.WrappingProcessor.Builder().coordVar(coordsVar).processors(bodyHandlers);
               if (location) {
                  if (!bodyHandlers.isEmpty() || html) {
                     conditionalBodyHandler = new ConditionalProcessor.Builder()
                           .condition().stringCondition().fromVar(newTempCoordsVar).isSet(false).end()
                           .processor(wrappingProcessor);
                     handlerConsumer = conditionalBodyHandler::processor;
                     httpRequest.handler().body(HttpRequestProcessorBuilder.adapt(conditionalBodyHandler));
                  } else {
                     handlerConsumer = null;
                  }
               } else {
                  assert html;
                  httpRequest.handler().body(HttpRequestProcessorBuilder.adapt(wrappingProcessor));
                  handlerConsumer = httpRequest.handler()::body;
               }
               if (html) {
                  handlerConsumer.accept(new HtmlHandler.Builder().handler(new MetaRefreshHandler.Builder()
                        .processor(fragmented -> new RefreshHandler(
                              queueKey, delayedQueueKey, poolKey, redirectConcurrency, access(coordsVar), access(delayedCoordVar),
                              redirectSequenceName, delaySequenceName, access(newTempCoordsVar), new Redirect.GetOriginalSequence(sequenceScopedAccess(coordsVar))))));
               }
            }
            if (!completionHandlers.isEmpty()) {
               httpRequest.handler().onCompletion(new ConditionalAction.Builder()
                     .condition().stringCondition().fromVar(newTempCoordsVar).isSet(false).end()
                     .action(new Redirect.WrappingAction.Builder().coordVar(coordsVar).actions(completionHandlers)));
            }
            httpRequest.handler().onCompletion(() -> new Location.Complete<>(poolKey, queueKey, sequenceScopedAccess(coordsVar)));
            SequenceBuilder redirectSequence = locator.scenario().sequence(redirectSequenceName)
                  .concurrency(redirectConcurrency)
                  .stepBuilder(httpRequest)
                  .rootSequence();
            Locator.push(null, redirectSequence);
            redirectSequence.prepareBuild();
            Locator.pop();
         }

         Unique tempCoordsVar = new Unique(locator.sequence().rootSequence().concurrency() > 0);

         if (location) {
            statusHandlers = Collections.singletonList(
                  new Redirect.StatusHandler.Builder().poolKey(poolKey).concurrency(redirectConcurrency).coordsVar(tempCoordsVar).handlers(statusHandlers));
            List headerHandlers = new ArrayList<>();
            headerHandlers.add(new Redirect.LocationRecorder.Builder()
                  .originalSequenceSupplier(() -> Session::currentSequence)
                  .concurrency(redirectConcurrency).inputVar(tempCoordsVar).outputVar(coordsVar).queueKey(queueKey).sequence(redirectSequenceName));
            if (!this.headerHandlers.isEmpty()) {
               // Note: we are using stringCondition.isSet despite the variable is not a string - it doesn't matter for the purpose of this check
               headerHandlers.add(new ConditionalHeaderHandler.Builder()
                     .condition().stringCondition().fromVar(tempCoordsVar).isSet(false).end()
                     .handlers(this.headerHandlers));
            }
            this.headerHandlers = headerHandlers;
         }

         if (!bodyHandlers.isEmpty() || html) {
            Consumer handlerConsumer;
            ConditionalProcessor.Builder conditionalBodyHandler = null;
            if (location) {
               conditionalBodyHandler = new ConditionalProcessor.Builder()
                     .condition().stringCondition().fromVar(tempCoordsVar).isSet(false).end()
                     .processors(bodyHandlers);
               handlerConsumer = conditionalBodyHandler::processor;
            } else {
               handlerConsumer = bodyHandlers::add;
            }
            if (html) {
               handlerConsumer.accept(new HtmlHandler.Builder().handler(new MetaRefreshHandler.Builder()
                     .processor(fragmented -> new RefreshHandler(
                           queueKey, delayedQueueKey, poolKey, redirectConcurrency, access(coordsVar), access(delayedCoordVar),
                           redirectSequenceName, delaySequenceName, access(tempCoordsVar), Session::currentSequence))));
            }
            if (location) {
               bodyHandlers = Collections.singletonList(HttpRequestProcessorBuilder.adapt(conditionalBodyHandler));
            }
         }
         if (!completionHandlers.isEmpty()) {
            completionHandlers = Collections.singletonList(
                  new ConditionalAction.Builder()
                        .condition().stringCondition().fromVar(tempCoordsVar).isSet(false).end()
                        .actions(completionHandlers));
         }
      }

      public HttpResponseHandlersImpl build() {
         return new HttpResponseHandlersImpl(
               toArray(statusHandlers, StatusHandler.Builder::build, StatusHandler[]::new),
               toArray(headerHandlers, HeaderHandler.Builder::build, HeaderHandler[]::new),
               toArray(bodyHandlers, b -> b.build(true), Processor[]::new),
               toArray(completionHandlers, Action.Builder::build, Action[]::new),
               toArray(rawBytesHandlers, RawBytesHandler.Builder::build, RawBytesHandler[]::new));
      }

      private static  T[] toArray(List list, Function build, IntFunction generator) {
         if (list.isEmpty()) {
            return null;
         } else {
            return list.stream().map(build).toArray(generator);
         }
      }

      @Override
      public void readFrom(Builder other) {
         statusHandlers.addAll(BuilderBase.copy(other.statusHandlers));
         headerHandlers.addAll(BuilderBase.copy(other.headerHandlers));
         bodyHandlers.addAll(BuilderBase.copy(other.bodyHandlers));
         completionHandlers.addAll(BuilderBase.copy(other.completionHandlers));
         rawBytesHandlers.addAll(BuilderBase.copy(other.rawBytesHandlers));
         autoRangeCheck = other.autoRangeCheck;
         stopOnInvalid = other.stopOnInvalid;
         followRedirect = other.followRedirect;
      }
   }

   private static class StopOnInvalidAction implements Action {
      // prevents some weird serialization incompatibility
      private static final Action INSTANCE = new StopOnInvalidAction();

      @Override
      public void run(Session session) {
         if (!session.currentRequest().isValid()) {
            session.stop();
         }
      }
   }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy