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

org.opentripplanner.apis.transmodel.TransmodelAPI Maven / Gradle / Ivy

The newest version!
package org.opentripplanner.apis.transmodel;

import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.SchemaPrinter;
import io.micrometer.core.instrument.Tag;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.opentripplanner.apis.support.graphql.injectdoc.ApiDocumentationProfile;
import org.opentripplanner.apis.transmodel.mapping.TransitIdMapper;
import org.opentripplanner.routing.api.request.RouteRequest;
import org.opentripplanner.standalone.api.OtpServerRequestContext;
import org.opentripplanner.standalone.config.routerconfig.TransitRoutingConfig;
import org.opentripplanner.transit.service.TimetableRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Path("/transmodel/v3")
@Produces(MediaType.APPLICATION_JSON)
public class TransmodelAPI {

  // Note, the blank line at the end is intended
  private static final String SCHEMA_DOC_HEADER =
    """
    # THIS IS NOT INTENDED FOR PRODUCTION USE. We recommend using the GraphQL introspection instead.
    # This is intended for the OTP Debug UI and can also be used by humans to get the schema with the
    # OTP configured default-values injected.

    """;

  private static final Logger LOG = LoggerFactory.getLogger(TransmodelAPI.class);

  private static GraphQLSchema schema;
  private static Collection tracingHeaderTags;
  private static int maxNumberOfResultFields;

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

  public TransmodelAPI(@Context OtpServerRequestContext serverContext) {
    this.serverContext = serverContext;
    this.index = new TransmodelGraph(schema);
  }

  /**
   * This class is only here for backwards-compatibility. It will be removed in the future.
   */
  @Path("/routers/{ignoreRouterId}/transmodel/index/graphql")
  public static class TransmodelAPIOldPath extends TransmodelAPI {

    public TransmodelAPIOldPath(
      @Context OtpServerRequestContext serverContext,
      @PathParam("ignoreRouterId") String ignore
    ) {
      super(serverContext);
    }
  }

  /**
   * 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,
    TimetableRepository timetableRepository,
    RouteRequest defaultRouteRequest,
    ApiDocumentationProfile documentationProfile,
    TransitRoutingConfig transitRoutingConfig
  ) {
    if (config.hideFeedId()) {
      TransitIdMapper.setupFixedFeedId(timetableRepository.getAgencies());
    }
    tracingHeaderTags = config.tracingHeaderTags();
    maxNumberOfResultFields = config.maxNumberOfResultFields();
    schema = TransmodelGraphQLSchema.create(
      defaultRouteRequest,
      timetableRepository.getTimeZone(),
      documentationProfile,
      transitRoutingConfig
    );
  }

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

    if (!(queryParameters.get("query") instanceof String query)) {
      throw new BadRequestException("Invalid format for query");
    }

    Object queryVariables = queryParameters.getOrDefault("variables", null);
    Map variables;
    if (queryVariables instanceof Map queryVariablesAsMap) {
      variables = queryVariablesAsMap;
    } else if (
      queryVariables instanceof String queryVariablesAsString && !queryVariablesAsString.isEmpty()
    ) {
      try {
        variables = deserializer.readValue(queryVariablesAsString, Map.class);
      } catch (IOException e) {
        throw new BadRequestException("Variables must be a valid json object");
      }
    } else {
      variables = Collections.emptyMap();
    }
    String operationName = (String) queryParameters.getOrDefault("operationName", null);
    return index.executeGraphQL(
      query,
      serverContext,
      variables,
      operationName,
      maxNumberOfResultFields,
      getTagsFromHeaders(headers)
    );
  }

  @POST
  @Consumes("application/graphql")
  public Response getGraphQL(String query, @Context HttpHeaders headers) {
    return index.executeGraphQL(
      query,
      serverContext,
      null,
      null,
      maxNumberOfResultFields,
      getTagsFromHeaders(headers)
    );
  }

  @GET
  @Path("schema.graphql")
  public Response getGraphQLSchema() {
    var text = SCHEMA_DOC_HEADER + new SchemaPrinter().print(schema);
    return Response.ok().encoding("UTF-8").entity(text).build();
  }

  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