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

org.opentripplanner.ext.transmodelapi.TransmodelAPI Maven / Gradle / Ivy

There is a newer version: 2.6.0
Show newest version
package org.opentripplanner.ext.transmodelapi;

import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.ExecutionResult;
import graphql.schema.GraphQLSchema;
import io.micrometer.core.instrument.Tag;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.opentripplanner.api.json.GraphQLResponseSerializer;
import org.opentripplanner.ext.transmodelapi.mapping.TransitIdMapper;
import org.opentripplanner.ext.transmodelapi.support.GqlUtil;
import org.opentripplanner.routing.api.request.RouteRequest;
import org.opentripplanner.standalone.api.OtpServerRequestContext;
import org.opentripplanner.transit.service.TransitModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// TODO move to org.opentripplanner.api.resource, this is a Jersey resource class

@Path("/routers/{ignoreRouterId}/transmodel/index")
// It would be nice to get rid of the final /index.
@Produces(MediaType.APPLICATION_JSON) // One @Produces annotation for all endpoints.
public class TransmodelAPI {

  @SuppressWarnings("unused")
  private static final Logger LOG = LoggerFactory.getLogger(TransmodelAPI.class);

  private static GraphQLSchema schema;
  private static Collection tracingHeaderTags;

  private final OtpServerRequestContext serverContext;
  private final TransmodelGraph index;
  private final ObjectMapper deserializer = new ObjectMapper();

  public TransmodelAPI(
    @Context OtpServerRequestContext serverContext,
    /**
     * @deprecated The support for multiple routers are removed from OTP2.
     * See https://github.com/opentripplanner/OpenTripPlanner/issues/2760
     */
    @Deprecated @PathParam("ignoreRouterId") String ignoreRouterId
  ) {
    this.serverContext = serverContext;
    this.index = new TransmodelGraph(schema);
  }

  /**
   * This method should be called BEFORE the Web-Container is started and load new instances of this
   * class. This is a hack, and it would be better if the configuration was done more explicit and
   * enforced, not relaying on a "static" setup method to be called.
   */
  public static void setUp(
    TransmodelAPIParameters config,
    TransitModel transitModel,
    RouteRequest defaultRouteRequest
  ) {
    if (config.hideFeedId()) {
      TransitIdMapper.setupFixedFeedId(transitModel.getAgencies());
    }
    tracingHeaderTags = config.tracingHeaderTags();
    GqlUtil gqlUtil = new GqlUtil(transitModel.getTimeZone());
    schema = TransmodelGraphQLSchema.create(defaultRouteRequest, gqlUtil);
  }

  /**
   * Return 200 when service is loaded.
   */
  @GET
  @Path("/live")
  public Response isAlive() {
    return Response.status(Response.Status.NO_CONTENT).build();
  }

  @POST
  @Path("/graphql")
  @Consumes(MediaType.APPLICATION_JSON)
  public Response getGraphQL(
    HashMap queryParameters,
    @HeaderParam("OTPMaxResolves") @DefaultValue("1000000") int maxResolves,
    @Context HttpHeaders headers
  ) {
    if (queryParameters == null || !queryParameters.containsKey("query")) {
      LOG.debug("No query found in body");
      throw new BadRequestException("No query found in body");
    }

    String query = (String) queryParameters.get("query");
    Object queryVariables = queryParameters.getOrDefault("variables", null);
    String operationName = (String) queryParameters.getOrDefault("operationName", null);
    Map variables;
    if (queryVariables instanceof Map) {
      variables = (Map) queryVariables;
    } else if (queryVariables instanceof String && !((String) queryVariables).isEmpty()) {
      try {
        variables = deserializer.readValue((String) queryVariables, Map.class);
      } catch (IOException e) {
        throw new BadRequestException("Variables must be a valid json object");
      }
    } else {
      variables = new HashMap<>();
    }
    return index.getGraphQLResponse(
      query,
      serverContext,
      variables,
      operationName,
      maxResolves,
      getTagsFromHeaders(headers)
    );
  }

  @POST
  @Path("/graphql")
  @Consumes("application/graphql")
  public Response getGraphQL(
    String query,
    @HeaderParam("OTPMaxResolves") @DefaultValue("1000000") int maxResolves,
    @Context HttpHeaders headers
  ) {
    return index.getGraphQLResponse(
      query,
      serverContext,
      null,
      null,
      maxResolves,
      getTagsFromHeaders(headers)
    );
  }

  @POST
  @Path("/graphql/batch")
  @Consumes(MediaType.APPLICATION_JSON)
  public Response getGraphQLBatch(
    List> queries,
    @HeaderParam("OTPTimeout") @DefaultValue("10000") int timeout,
    @HeaderParam("OTPMaxResolves") @DefaultValue("1000000") int maxResolves,
    @Context HttpHeaders headers
  ) {
    List> futures = new ArrayList<>();

    for (Map query : queries) {
      Map variables;
      if (query.get("variables") instanceof Map) {
        variables = (Map) query.get("variables");
      } else if (
        query.get("variables") instanceof String && ((String) query.get("variables")).length() > 0
      ) {
        try {
          variables = deserializer.readValue((String) query.get("variables"), Map.class);
        } catch (IOException e) {
          throw new BadRequestException("Variables must be a valid json object");
        }
      } else {
        variables = null;
      }
      String operationName = (String) query.getOrDefault("operationName", null);

      futures.add(() ->
        index.getGraphQLExecutionResult(
          (String) query.get("query"),
          serverContext,
          variables,
          operationName,
          maxResolves,
          getTagsFromHeaders(headers)
        )
      );
    }

    try {
      List> results = index.threadPool.invokeAll(futures);
      return Response
        .status(Response.Status.OK)
        .entity(GraphQLResponseSerializer.serializeBatch(queries, results))
        .build();
    } catch (InterruptedException e) {
      LOG.error("Batch query interrupted", e);
      throw new RuntimeException(e);
    }
  }

  private static Iterable getTagsFromHeaders(HttpHeaders headers) {
    return tracingHeaderTags
      .stream()
      .map(header -> {
        String value = headers.getHeaderString(header);
        return Tag.of(header, value == null ? "__UNKNOWN__" : value);
      })
      .collect(Collectors.toList());
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy