content: /**
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package org.apache.aurora.scheduler.http;
  
  import java.util.List;
  import java.util.Map;
  import java.util.Map.Entry;
  import java.util.Optional;
  import java.util.Set;
  
  import javax.inject.Inject;
  import javax.ws.rs.DELETE;
  import javax.ws.rs.GET;
  import javax.ws.rs.POST;
  import javax.ws.rs.PUT;
  import javax.ws.rs.Path;
  import javax.ws.rs.PathParam;
  import javax.ws.rs.core.Context;
  import javax.ws.rs.core.Response;
  import javax.ws.rs.core.Response.Status;
  import javax.ws.rs.core.UriBuilder;
  import javax.ws.rs.core.UriInfo;
  
  import com.google.common.annotations.VisibleForTesting;
  import com.google.common.collect.ImmutableSet;
  import com.google.common.collect.Iterables;
  
  import org.apache.aurora.scheduler.base.JobKeys;
  import org.apache.aurora.scheduler.base.Query;
  import org.apache.aurora.scheduler.storage.Storage;
  import org.apache.aurora.scheduler.storage.entities.IAssignedTask;
  import org.apache.aurora.scheduler.storage.entities.IScheduledTask;
  
  import static java.util.Objects.requireNonNull;
  
  import static javax.ws.rs.core.Response.Status.NOT_FOUND;
  
  import static org.apache.aurora.gen.ScheduleStatus.RUNNING;
  
  /**
   * Servlet implementing HTTP redirects to task endpoints.
   */
  @Path("/mname")
  public class Mname {
  
    private static final String USAGE =
        "<html>HTTP redirector from the canonical name of a task to its current HTTP endpoint. "
        + "<p>Forwards GET, PUT, POST and DELETE requests using HTTP status code 307. "
        + "This allows compliant clients to seamlessly perform re-directed mutations.</p>"
        + "Usage: /mname/{role}/{env}/{job}/{instance}</html>";
  
    private static final Set<String> HTTP_PORT_NAMES = ImmutableSet.of(
        "health", "http", "HTTP", "web", "admin");
  
    private final Storage storage;
  
    @Inject
    public Mname(Storage storage) {
      this.storage = requireNonNull(storage);
    }
  
    @GET
    public Response getUsage() {
      return Response
          .status(Status.BAD_REQUEST)
          .entity(USAGE)
          .build();
    }
  
    @GET
    @Path("/{role}/{env}/{job}/{instance}/{forward:.+}")
    public Response getWithForwardRequest(
        @PathParam("role") String role,
        @PathParam("env") String env,
        @PathParam("job") String job,
        @PathParam("instance") int instanceId,
        @PathParam("forward") String forward,
        @Context UriInfo uriInfo) {
  
      return get(role, env, job, instanceId, uriInfo, Optional.of(forward));
    }
  
    @PUT
    @Path("/{role}/{env}/{job}/{instance}/{forward:.+}")
    public Response putWithForwardRequest(
        @PathParam("role") String role,
        @PathParam("env") String env,
        @PathParam("job") String job,
        @PathParam("instance") int instanceId,
        @PathParam("forward") String forward,
        @Context UriInfo uriInfo) {
  
      return get(role, env, job, instanceId, uriInfo, Optional.of(forward));
    }
  
    @POST
    @Path("/{role}/{env}/{job}/{instance}/{forward:.+}")
    public Response postWithForwardRequest(
        @PathParam("role") String role,
        @PathParam("env") String env,
        @PathParam("job") String job,
        @PathParam("instance") int instanceId,
        @PathParam("forward") String forward,
        @Context UriInfo uriInfo) {
  
      return get(role, env, job, instanceId, uriInfo, Optional.of(forward));
    }
  
    @DELETE
    @Path("/{role}/{env}/{job}/{instance}/{forward:.+}")
    public Response deleteWithForwardRequest(
        @PathParam("role") String role,
        @PathParam("env") String env,
        @PathParam("job") String job,
        @PathParam("instance") int instanceId,
        @PathParam("forward") String forward,
        @Context UriInfo uriInfo) {
  
      return get(role, env, job, instanceId, uriInfo, Optional.of(forward));
    }
  
    @GET
    @Path("/{role}/{env}/{job}/{instance}")
    public Response get(
        @PathParam("role") String role,
        @PathParam("env") String env,
        @PathParam("job") String job,
        @PathParam("instance") int instanceId,
        @Context UriInfo uriInfo) {
  
      return get(role, env, job, instanceId, uriInfo, Optional.empty());
    }
  
    @PUT
    @Path("/{role}/{env}/{job}/{instance}")
    public Response put(
        @PathParam("role") String role,
        @PathParam("env") String env,
        @PathParam("job") String job,
        @PathParam("instance") int instanceId,
        @Context UriInfo uriInfo) {
  
      return get(role, env, job, instanceId, uriInfo, Optional.empty());
    }
  
    @POST
    @Path("/{role}/{env}/{job}/{instance}")
    public Response post(
        @PathParam("role") String role,
        @PathParam("env") String env,
        @PathParam("job") String job,
        @PathParam("instance") int instanceId,
        @Context UriInfo uriInfo) {
  
      return get(role, env, job, instanceId, uriInfo, Optional.empty());
    }
  
    @DELETE
    @Path("/{role}/{env}/{job}/{instance}")
    public Response delete(
        @PathParam("role") String role,
        @PathParam("env") String env,
        @PathParam("job") String job,
        @PathParam("instance") int instanceId,
        @Context UriInfo uriInfo) {
  
      return get(role, env, job, instanceId, uriInfo, Optional.empty());
    }
  
    private Response get(
        String role,
        String env,
        String job,
        int instanceId,
        UriInfo uriInfo,
        Optional<String> forwardRequest) {
  
      IScheduledTask task = Iterables.getOnlyElement(
          Storage.Util.fetchTasks(storage,
              Query.instanceScoped(JobKeys.from(role, env, job), instanceId).active()),
          null);
      if (task == null) {
        return respond(NOT_FOUND, "No such live instance found.");
      }
  
      if (task.getStatus() != RUNNING) {
        return respond(NOT_FOUND, "The selected instance is currently in state " + task.getStatus());
      }
  
      IAssignedTask assignedTask = task.getAssignedTask();
      Optional<Integer> port = getRedirectPort(assignedTask);
      if (!port.isPresent()) {
        return respond(NOT_FOUND, "The task does not have a registered http port.");
      }
  
      UriBuilder redirect = UriBuilder
          .fromPath(forwardRequest.orElse("/"))
          .scheme("http")
          .host(assignedTask.getSlaveHost())
          .port(port.get());
      for (Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
        for (String value : entry.getValue()) {
          redirect.queryParam(entry.getKey(), value);
        }
      }
  
      return Response.temporaryRedirect(redirect.build()).build();
    }
  
    @VisibleForTesting
    static Optional<Integer> getRedirectPort(IAssignedTask task) {
      Map<String, Integer> ports = task.getAssignedPorts();
      for (String httpPortName : HTTP_PORT_NAMES) {
        Integer port = ports.get(httpPortName);
        if (port != null) {
          return Optional.of(port);
        }
      }
      return Optional.empty();
    }
  
    private Response respond(Status status, String message) {
      return Response.status(status).entity(message).build();
    }
  }
  