| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  | package controllers;
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  | import static auth.AuthUtils.ACTOR;
 | 
					
						
							|  |  |  | import static auth.AuthUtils.SESSION_COOKIE_GMS_TOKEN_NAME;
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-20 10:58:07 -07:00
										 |  |  | import akka.util.ByteString;
 | 
					
						
							|  |  |  | import auth.Authenticator;
 | 
					
						
							| 
									
										
										
										
											2021-11-22 16:33:14 -08:00
										 |  |  | import com.datahub.authentication.AuthenticationConstants;
 | 
					
						
							| 
									
										
										
										
											2024-09-10 13:01:35 -04:00
										 |  |  | import com.fasterxml.jackson.databind.JsonNode;
 | 
					
						
							|  |  |  | import com.fasterxml.jackson.databind.ObjectMapper;
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  | import com.fasterxml.jackson.databind.node.ObjectNode;
 | 
					
						
							| 
									
										
										
										
											2025-10-01 11:08:38 -05:00
										 |  |  | import com.linkedin.metadata.utils.BasePathUtils;
 | 
					
						
							| 
									
										
										
										
											2021-08-20 10:58:07 -07:00
										 |  |  | import com.linkedin.util.Pair;
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  | import com.typesafe.config.Config;
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  | import java.io.InputStream;
 | 
					
						
							| 
									
										
										
										
											2024-01-22 11:46:04 -06:00
										 |  |  | import java.net.URI;
 | 
					
						
							| 
									
										
										
										
											2025-07-09 19:25:42 +05:30
										 |  |  | import java.net.http.HttpClient;
 | 
					
						
							|  |  |  | import java.net.http.HttpRequest;
 | 
					
						
							|  |  |  | import java.net.http.HttpResponse;
 | 
					
						
							| 
									
										
										
										
											2025-10-01 11:08:38 -05:00
										 |  |  | import java.nio.charset.StandardCharsets;
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  | import java.time.Duration;
 | 
					
						
							| 
									
										
										
										
											2024-09-10 13:01:35 -04:00
										 |  |  | import java.time.Instant;
 | 
					
						
							| 
									
										
										
										
											2025-07-09 19:25:42 +05:30
										 |  |  | import java.util.*;
 | 
					
						
							| 
									
										
										
										
											2021-08-20 10:58:07 -07:00
										 |  |  | import java.util.concurrent.CompletableFuture;
 | 
					
						
							|  |  |  | import java.util.stream.Collectors;
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  | import javax.annotation.Nonnull;
 | 
					
						
							|  |  |  | import javax.annotation.Nullable;
 | 
					
						
							|  |  |  | import javax.inject.Inject;
 | 
					
						
							| 
									
										
										
										
											2022-12-26 10:09:08 -06:00
										 |  |  | import org.slf4j.Logger;
 | 
					
						
							|  |  |  | import org.slf4j.LoggerFactory;
 | 
					
						
							| 
									
										
										
										
											2022-12-08 20:27:51 -06:00
										 |  |  | import play.Environment;
 | 
					
						
							| 
									
										
										
										
											2021-08-20 10:58:07 -07:00
										 |  |  | import play.http.HttpEntity;
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  | import play.libs.Json;
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  | import play.mvc.Controller;
 | 
					
						
							| 
									
										
										
										
											2021-08-20 10:58:07 -07:00
										 |  |  | import play.mvc.Http;
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  | import play.mvc.Http.Cookie;
 | 
					
						
							| 
									
										
										
										
											2021-08-20 10:58:07 -07:00
										 |  |  | import play.mvc.ResponseHeader;
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  | import play.mvc.Result;
 | 
					
						
							| 
									
										
										
										
											2021-08-20 10:58:07 -07:00
										 |  |  | import play.mvc.Security;
 | 
					
						
							| 
									
										
										
										
											2021-11-22 16:33:14 -08:00
										 |  |  | import utils.ConfigUtil;
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | public class Application extends Controller {
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |   private static final Logger logger = LoggerFactory.getLogger(Application.class.getName());
 | 
					
						
							| 
									
										
										
										
											2025-07-09 19:25:42 +05:30
										 |  |  |   private static final Set<String> RESTRICTED_HEADERS =
 | 
					
						
							|  |  |  |       Set.of("connection", "host", "content-length", "expect", "upgrade", "transfer-encoding");
 | 
					
						
							| 
									
										
										
										
											2025-10-01 11:08:38 -05:00
										 |  |  |   private static final Set<String> SWAGGER_PATHS =
 | 
					
						
							|  |  |  |       Set.of("/openapi/swagger-ui", "/openapi/v3/api-docs");
 | 
					
						
							| 
									
										
										
										
											2025-07-25 03:39:04 +05:30
										 |  |  |   private final HttpClient httpClient;
 | 
					
						
							| 
									
										
										
										
											2025-07-09 19:25:42 +05:30
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |   private final Config config;
 | 
					
						
							|  |  |  |   private final Environment environment;
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-10-01 11:08:38 -05:00
										 |  |  |   private final String basePath;
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  |   @Inject
 | 
					
						
							| 
									
										
										
										
											2025-07-25 03:39:04 +05:30
										 |  |  |   public Application(HttpClient httpClient, Environment environment, @Nonnull Config config) {
 | 
					
						
							|  |  |  |     this.httpClient = httpClient;
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |     this.config = config;
 | 
					
						
							|  |  |  |     this.environment = environment;
 | 
					
						
							| 
									
										
										
										
											2025-10-01 11:08:38 -05:00
										 |  |  |     this.basePath = config.getString("datahub.basePath");
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /**
 | 
					
						
							|  |  |  |    * Serves the build output index.html for any given path
 | 
					
						
							|  |  |  |    *
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |    * @param path takes a path string, which essentially is ignored routing is managed client side
 | 
					
						
							| 
									
										
										
										
											2025-10-01 11:08:38 -05:00
										 |  |  |    * @return {Result} rendered index template with dynamic base path
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  |    */
 | 
					
						
							|  |  |  |   @Nonnull
 | 
					
						
							|  |  |  |   private Result serveAsset(@Nullable String path) {
 | 
					
						
							| 
									
										
										
										
											2022-12-26 10:09:08 -06:00
										 |  |  |     try {
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |       InputStream indexHtml = environment.resourceAsStream("public/index.html");
 | 
					
						
							| 
									
										
										
										
											2025-10-01 11:08:38 -05:00
										 |  |  |       if (indexHtml == null) {
 | 
					
						
							|  |  |  |         throw new IllegalStateException("index.html not found");
 | 
					
						
							|  |  |  |       }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       String html = new String(indexHtml.readAllBytes(), StandardCharsets.UTF_8);
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       String basePath = this.basePath;
 | 
					
						
							|  |  |  |       // Ensure base path ends with / for HTML base tag
 | 
					
						
							|  |  |  |       if (!basePath.endsWith("/")) {
 | 
					
						
							|  |  |  |         basePath += "/";
 | 
					
						
							|  |  |  |       }
 | 
					
						
							|  |  |  |       // Inject <base href="..."/> right after <head> for use in the frontend.
 | 
					
						
							|  |  |  |       String modifiedHtml = html.replace("@basePath", basePath);
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return ok(modifiedHtml).withHeader("Cache-Control", "no-cache").as("text/html");
 | 
					
						
							| 
									
										
										
										
											2022-12-26 10:09:08 -06:00
										 |  |  |     } catch (Exception e) {
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |       logger.warn("Cannot load public/index.html resource. Static assets or assets jar missing?");
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |       return notFound().withHeader("Cache-Control", "no-cache").as("text/html");
 | 
					
						
							| 
									
										
										
										
											2022-12-26 10:09:08 -06:00
										 |  |  |     }
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @Nonnull
 | 
					
						
							|  |  |  |   public Result healthcheck() {
 | 
					
						
							|  |  |  |     return ok("GOOD");
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /**
 | 
					
						
							|  |  |  |    * index Action proxies to serveAsset
 | 
					
						
							|  |  |  |    *
 | 
					
						
							|  |  |  |    * @return {Result} response from serveAsset method
 | 
					
						
							|  |  |  |    */
 | 
					
						
							|  |  |  |   @Nonnull
 | 
					
						
							|  |  |  |   public Result index(@Nullable String path) {
 | 
					
						
							| 
									
										
										
										
											2025-10-01 11:08:38 -05:00
										 |  |  |     return serveAsset(path);
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /**
 | 
					
						
							|  |  |  |    * Moves permanently the get into version without trailing slash
 | 
					
						
							|  |  |  |    *
 | 
					
						
							|  |  |  |    * @param path String
 | 
					
						
							|  |  |  |    * @return Result
 | 
					
						
							|  |  |  |    */
 | 
					
						
							|  |  |  |   @Nonnull
 | 
					
						
							|  |  |  |   public Result redirectTrailingSlash(@Nullable String path) {
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return movedPermanently("/" + path);
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-05 10:53:26 -07:00
										 |  |  |   /**
 | 
					
						
							| 
									
										
										
										
											2021-08-20 10:58:07 -07:00
										 |  |  |    * Proxies requests to the Metadata Service
 | 
					
						
							|  |  |  |    *
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |    * <p>TODO: Investigate using mutual SSL authentication to call Metadata Service.
 | 
					
						
							| 
									
										
										
										
											2019-09-05 10:53:26 -07:00
										 |  |  |    */
 | 
					
						
							| 
									
										
										
										
											2021-08-20 10:58:07 -07:00
										 |  |  |   @Security.Authenticated(Authenticator.class)
 | 
					
						
							| 
									
										
										
										
											2025-07-09 19:25:42 +05:30
										 |  |  |   public CompletableFuture<Result> proxy(String path, Http.Request request) {
 | 
					
						
							| 
									
										
										
										
											2022-12-08 20:27:51 -06:00
										 |  |  |     final String authorizationHeaderValue = getAuthorizationHeaderValueToProxy(request);
 | 
					
						
							|  |  |  |     final String resolvedUri = mapPath(request.uri());
 | 
					
						
							| 
									
										
										
										
											2021-11-22 16:33:14 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |     final String metadataServiceHost =
 | 
					
						
							|  |  |  |         ConfigUtil.getString(
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |             config,
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |             ConfigUtil.METADATA_SERVICE_HOST_CONFIG_PATH,
 | 
					
						
							|  |  |  |             ConfigUtil.DEFAULT_METADATA_SERVICE_HOST);
 | 
					
						
							|  |  |  |     final int metadataServicePort =
 | 
					
						
							|  |  |  |         ConfigUtil.getInt(
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |             config,
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |             ConfigUtil.METADATA_SERVICE_PORT_CONFIG_PATH,
 | 
					
						
							|  |  |  |             ConfigUtil.DEFAULT_METADATA_SERVICE_PORT);
 | 
					
						
							| 
									
										
										
										
											2025-10-01 11:08:38 -05:00
										 |  |  |     final String metadataServiceBasePath =
 | 
					
						
							|  |  |  |         ConfigUtil.getString(
 | 
					
						
							|  |  |  |             config,
 | 
					
						
							|  |  |  |             ConfigUtil.METADATA_SERVICE_BASE_PATH_CONFIG_PATH,
 | 
					
						
							|  |  |  |             ConfigUtil.DEFAULT_METADATA_SERVICE_BASE_PATH);
 | 
					
						
							|  |  |  |     final boolean metadataServiceBasePathEnabled =
 | 
					
						
							|  |  |  |         ConfigUtil.getBoolean(
 | 
					
						
							|  |  |  |             config,
 | 
					
						
							|  |  |  |             ConfigUtil.METADATA_SERVICE_BASE_PATH_ENABLED_CONFIG_PATH,
 | 
					
						
							|  |  |  |             ConfigUtil.DEFAULT_METADATA_SERVICE_BASE_PATH_ENABLED);
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |     final boolean metadataServiceUseSsl =
 | 
					
						
							|  |  |  |         ConfigUtil.getBoolean(
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |             config,
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |             ConfigUtil.METADATA_SERVICE_USE_SSL_CONFIG_PATH,
 | 
					
						
							|  |  |  |             ConfigUtil.DEFAULT_METADATA_SERVICE_USE_SSL);
 | 
					
						
							| 
									
										
										
										
											2025-10-01 11:08:38 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // Use the same logic as GMSConfiguration.getResolvedBasePath()
 | 
					
						
							|  |  |  |     String resolvedBasePath =
 | 
					
						
							|  |  |  |         BasePathUtils.resolveBasePath(metadataServiceBasePathEnabled, metadataServiceBasePath);
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-22 16:33:14 -08:00
										 |  |  |     final String protocol = metadataServiceUseSsl ? "https" : "http";
 | 
					
						
							| 
									
										
										
										
											2025-07-09 19:25:42 +05:30
										 |  |  |     final String targetUrl =
 | 
					
						
							|  |  |  |         String.format(
 | 
					
						
							| 
									
										
										
										
											2025-10-01 11:08:38 -05:00
										 |  |  |             "%s://%s:%s%s%s",
 | 
					
						
							|  |  |  |             protocol, metadataServiceHost, metadataServicePort, resolvedBasePath, resolvedUri);
 | 
					
						
							| 
									
										
										
										
											2025-07-09 19:25:42 +05:30
										 |  |  |     HttpRequest.Builder httpRequestBuilder =
 | 
					
						
							|  |  |  |         HttpRequest.newBuilder().uri(URI.create(targetUrl)).timeout(Duration.ofSeconds(120));
 | 
					
						
							|  |  |  |     httpRequestBuilder.method(request.method(), buildBodyPublisher(request));
 | 
					
						
							|  |  |  |     Map<String, List<String>> headers = request.getHeaders().toMap();
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |     if (headers.containsKey(Http.HeaderNames.HOST)
 | 
					
						
							|  |  |  |         && !headers.containsKey(Http.HeaderNames.X_FORWARDED_HOST)) {
 | 
					
						
							|  |  |  |       headers.put(Http.HeaderNames.X_FORWARDED_HOST, headers.get(Http.HeaderNames.HOST));
 | 
					
						
							| 
									
										
										
										
											2022-09-09 23:13:30 +02:00
										 |  |  |     }
 | 
					
						
							| 
									
										
										
										
											2024-01-22 11:46:04 -06:00
										 |  |  |     if (!headers.containsKey(Http.HeaderNames.X_FORWARDED_PROTO)) {
 | 
					
						
							|  |  |  |       final String schema =
 | 
					
						
							|  |  |  |           Optional.ofNullable(URI.create(request.uri()).getScheme()).orElse("http");
 | 
					
						
							|  |  |  |       headers.put(Http.HeaderNames.X_FORWARDED_PROTO, List.of(schema));
 | 
					
						
							|  |  |  |     }
 | 
					
						
							| 
									
										
										
										
											2025-07-09 19:25:42 +05:30
										 |  |  |     headers.entrySet().stream()
 | 
					
						
							|  |  |  |         .filter(
 | 
					
						
							|  |  |  |             entry ->
 | 
					
						
							|  |  |  |                 !RESTRICTED_HEADERS.contains(entry.getKey().toLowerCase())
 | 
					
						
							|  |  |  |                     && !AuthenticationConstants.LEGACY_X_DATAHUB_ACTOR_HEADER.equalsIgnoreCase(
 | 
					
						
							|  |  |  |                         entry.getKey())
 | 
					
						
							|  |  |  |                     && !Http.HeaderNames.CONTENT_TYPE.equalsIgnoreCase(entry.getKey())
 | 
					
						
							|  |  |  |                     && !Http.HeaderNames.AUTHORIZATION.equalsIgnoreCase(entry.getKey()))
 | 
					
						
							|  |  |  |         .forEach(
 | 
					
						
							|  |  |  |             entry -> entry.getValue().forEach(v -> httpRequestBuilder.header(entry.getKey(), v)));
 | 
					
						
							|  |  |  |     if (!authorizationHeaderValue.isEmpty()) {
 | 
					
						
							|  |  |  |       httpRequestBuilder.header(Http.HeaderNames.AUTHORIZATION, authorizationHeaderValue);
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  |     httpRequestBuilder.header(
 | 
					
						
							|  |  |  |         AuthenticationConstants.LEGACY_X_DATAHUB_ACTOR_HEADER, getDataHubActorHeader(request));
 | 
					
						
							|  |  |  |     request
 | 
					
						
							|  |  |  |         .contentType()
 | 
					
						
							|  |  |  |         .ifPresent(ct -> httpRequestBuilder.header(Http.HeaderNames.CONTENT_TYPE, ct));
 | 
					
						
							| 
									
										
										
										
											2024-09-10 13:01:35 -04:00
										 |  |  |     Instant start = Instant.now();
 | 
					
						
							| 
									
										
										
										
											2025-07-09 19:25:42 +05:30
										 |  |  |     return httpClient
 | 
					
						
							|  |  |  |         .sendAsync(httpRequestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |         .thenApply(
 | 
					
						
							|  |  |  |             apiResponse -> {
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |               boolean verboseGraphQLLogging = config.getBoolean("graphql.verbose.logging");
 | 
					
						
							|  |  |  |               int verboseGraphQLLongQueryMillis = config.getInt("graphql.verbose.slowQueryMillis");
 | 
					
						
							| 
									
										
										
										
											2024-09-10 13:01:35 -04:00
										 |  |  |               Instant finish = Instant.now();
 | 
					
						
							|  |  |  |               long timeElapsed = Duration.between(start, finish).toMillis();
 | 
					
						
							|  |  |  |               if (verboseGraphQLLogging && timeElapsed >= verboseGraphQLLongQueryMillis) {
 | 
					
						
							|  |  |  |                 logSlowQuery(request, resolvedUri, timeElapsed);
 | 
					
						
							|  |  |  |               }
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |               final ResponseHeader header =
 | 
					
						
							|  |  |  |                   new ResponseHeader(
 | 
					
						
							| 
									
										
										
										
											2025-07-09 19:25:42 +05:30
										 |  |  |                       apiResponse.statusCode(),
 | 
					
						
							|  |  |  |                       apiResponse.headers().map().entrySet().stream()
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |                           .filter(
 | 
					
						
							|  |  |  |                               entry ->
 | 
					
						
							|  |  |  |                                   !Http.HeaderNames.CONTENT_LENGTH.equalsIgnoreCase(entry.getKey()))
 | 
					
						
							|  |  |  |                           .filter(
 | 
					
						
							|  |  |  |                               entry ->
 | 
					
						
							|  |  |  |                                   !Http.HeaderNames.CONTENT_TYPE.equalsIgnoreCase(entry.getKey()))
 | 
					
						
							|  |  |  |                           .map(entry -> Pair.of(entry.getKey(), String.join(";", entry.getValue())))
 | 
					
						
							|  |  |  |                           .collect(Collectors.toMap(Pair::getFirst, Pair::getSecond)));
 | 
					
						
							|  |  |  |               final HttpEntity body =
 | 
					
						
							|  |  |  |                   new HttpEntity.Strict(
 | 
					
						
							| 
									
										
										
										
											2025-07-09 19:25:42 +05:30
										 |  |  |                       ByteString.fromArray(apiResponse.body()),
 | 
					
						
							|  |  |  |                       apiResponse.headers().firstValue(Http.HeaderNames.CONTENT_TYPE));
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |               return new Result(header, body);
 | 
					
						
							|  |  |  |             })
 | 
					
						
							| 
									
										
										
										
											2025-07-09 19:25:42 +05:30
										 |  |  |         .exceptionally(
 | 
					
						
							|  |  |  |             ex -> {
 | 
					
						
							|  |  |  |               Throwable cause = ex.getCause() != null ? ex.getCause() : ex;
 | 
					
						
							|  |  |  |               if (cause instanceof java.net.http.HttpTimeoutException) {
 | 
					
						
							|  |  |  |                 return status(GATEWAY_TIMEOUT, "Proxy request timed out.");
 | 
					
						
							|  |  |  |               } else if (cause instanceof java.net.ConnectException) {
 | 
					
						
							|  |  |  |                 return status(BAD_GATEWAY, "Proxy connection failed: " + cause.getMessage());
 | 
					
						
							|  |  |  |               } else {
 | 
					
						
							|  |  |  |                 return internalServerError("Proxy error: " + cause.getMessage());
 | 
					
						
							|  |  |  |               }
 | 
					
						
							|  |  |  |             });
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private HttpRequest.BodyPublisher buildBodyPublisher(Http.Request request) {
 | 
					
						
							|  |  |  |     if (request.body().asBytes() != null) {
 | 
					
						
							|  |  |  |       return HttpRequest.BodyPublishers.ofByteArray(request.body().asBytes().toArray());
 | 
					
						
							|  |  |  |     } else if (request.body().asText() != null) {
 | 
					
						
							|  |  |  |       return HttpRequest.BodyPublishers.ofString(request.body().asText());
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  |     return HttpRequest.BodyPublishers.noBody();
 | 
					
						
							| 
									
										
										
										
											2019-09-05 10:53:26 -07:00
										 |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  |   /**
 | 
					
						
							|  |  |  |    * Creates a wrapping ObjectNode containing config information
 | 
					
						
							|  |  |  |    *
 | 
					
						
							|  |  |  |    * @return Http Result instance with app configuration attributes
 | 
					
						
							|  |  |  |    */
 | 
					
						
							|  |  |  |   @Nonnull
 | 
					
						
							|  |  |  |   public Result appConfig() {
 | 
					
						
							|  |  |  |     final ObjectNode config = Json.newObject();
 | 
					
						
							| 
									
										
										
										
											2022-01-20 08:21:47 +05:30
										 |  |  |     config.put("application", "datahub-frontend");
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |     config.put("appVersion", this.config.getString("app.version"));
 | 
					
						
							|  |  |  |     config.put("isInternal", this.config.getBoolean("linkedin.internal"));
 | 
					
						
							|  |  |  |     config.put("shouldShowDatasetLineage", this.config.getBoolean("linkedin.show.dataset.lineage"));
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |     config.put(
 | 
					
						
							|  |  |  |         "suggestionConfidenceThreshold",
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |         Integer.valueOf(this.config.getString("linkedin.suggestion.confidence.threshold")));
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  |     config.set("wikiLinks", wikiLinks());
 | 
					
						
							|  |  |  |     config.set("tracking", trackingInfo());
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |     // In a staging environment, we can trigger this flag to be true so that the UI can handle based
 | 
					
						
							|  |  |  |     // on
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  |     // such config and alert users that their changes will not affect production data
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |     config.put("isStagingBanner", this.config.getBoolean("ui.show.staging.banner"));
 | 
					
						
							|  |  |  |     config.put("isLiveDataWarning", this.config.getBoolean("ui.show.live.data.banner"));
 | 
					
						
							|  |  |  |     config.put("showChangeManagement", this.config.getBoolean("ui.show.CM.banner"));
 | 
					
						
							| 
									
										
										
										
											2019-09-05 10:53:26 -07:00
										 |  |  |     // Flag to enable people entity elements
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |     config.put("showPeople", this.config.getBoolean("ui.show.people"));
 | 
					
						
							|  |  |  |     config.put("changeManagementLink", this.config.getString("ui.show.CM.link"));
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  |     // Flag set in order to warn users that search is experiencing issues
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |     config.put("isStaleSearch", this.config.getBoolean("ui.show.stale.search"));
 | 
					
						
							|  |  |  |     config.put("showAdvancedSearch", this.config.getBoolean("ui.show.advanced.search"));
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  |     // Flag to use the new api for browsing datasets
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |     config.put("useNewBrowseDataset", this.config.getBoolean("ui.new.browse.dataset"));
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  |     // show lineage graph in relationships tabs
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |     config.put("showLineageGraph", this.config.getBoolean("ui.show.lineage.graph"));
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  |     // show institutional memory for available entities
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |     config.put("showInstitutionalMemory", this.config.getBoolean("ui.show.institutional.memory"));
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // Insert properties for user profile operations
 | 
					
						
							|  |  |  |     config.set("userEntityProps", userEntityProps());
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-10-01 11:08:38 -05:00
										 |  |  |     // Add base path configuration for frontend
 | 
					
						
							|  |  |  |     config.put("basePath", this.basePath);
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  |     final ObjectNode response = Json.newObject();
 | 
					
						
							|  |  |  |     response.put("status", "ok");
 | 
					
						
							|  |  |  |     response.set("config", config);
 | 
					
						
							|  |  |  |     return ok(response);
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /**
 | 
					
						
							|  |  |  |    * Creates a JSON object of profile / avatar properties
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |    *
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  |    * @return Json avatar / profile image properties
 | 
					
						
							|  |  |  |    */
 | 
					
						
							|  |  |  |   @Nonnull
 | 
					
						
							|  |  |  |   private ObjectNode userEntityProps() {
 | 
					
						
							|  |  |  |     final ObjectNode props = Json.newObject();
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |     props.put("aviUrlPrimary", config.getString("linkedin.links.avi.urlPrimary"));
 | 
					
						
							|  |  |  |     props.put("aviUrlFallback", config.getString("linkedin.links.avi.urlFallback"));
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  |     return props;
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /**
 | 
					
						
							|  |  |  |    * @return Json object with internal wiki links
 | 
					
						
							|  |  |  |    */
 | 
					
						
							|  |  |  |   @Nonnull
 | 
					
						
							|  |  |  |   private ObjectNode wikiLinks() {
 | 
					
						
							|  |  |  |     final ObjectNode wikiLinks = Json.newObject();
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |     wikiLinks.put("appHelp", config.getString("links.wiki.appHelp"));
 | 
					
						
							|  |  |  |     wikiLinks.put("gdprPii", config.getString("links.wiki.gdprPii"));
 | 
					
						
							|  |  |  |     wikiLinks.put("tmsSchema", config.getString("links.wiki.tmsSchema"));
 | 
					
						
							|  |  |  |     wikiLinks.put("gdprTaxonomy", config.getString("links.wiki.gdprTaxonomy"));
 | 
					
						
							|  |  |  |     wikiLinks.put("staleSearchIndex", config.getString("links.wiki.staleSearchIndex"));
 | 
					
						
							|  |  |  |     wikiLinks.put("dht", config.getString("links.wiki.dht"));
 | 
					
						
							|  |  |  |     wikiLinks.put("purgePolicies", config.getString("links.wiki.purgePolicies"));
 | 
					
						
							|  |  |  |     wikiLinks.put("jitAcl", config.getString("links.wiki.jitAcl"));
 | 
					
						
							|  |  |  |     wikiLinks.put("metadataCustomRegex", config.getString("links.wiki.metadataCustomRegex"));
 | 
					
						
							|  |  |  |     wikiLinks.put("exportPolicy", config.getString("links.wiki.exportPolicy"));
 | 
					
						
							|  |  |  |     wikiLinks.put("metadataHealth", config.getString("links.wiki.metadataHealth"));
 | 
					
						
							|  |  |  |     wikiLinks.put("purgeKey", config.getString("links.wiki.purgeKey"));
 | 
					
						
							|  |  |  |     wikiLinks.put("datasetDecommission", config.getString("links.wiki.datasetDecommission"));
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  |     return wikiLinks;
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /**
 | 
					
						
							|  |  |  |    * @return Json object containing the tracking configuration details
 | 
					
						
							|  |  |  |    */
 | 
					
						
							|  |  |  |   @Nonnull
 | 
					
						
							|  |  |  |   private ObjectNode trackingInfo() {
 | 
					
						
							|  |  |  |     final ObjectNode piwik = Json.newObject();
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |     piwik.put("piwikSiteId", Integer.valueOf(config.getString("tracking.piwik.siteid")));
 | 
					
						
							|  |  |  |     piwik.put("piwikUrl", config.getString("tracking.piwik.url"));
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     final ObjectNode trackers = Json.newObject();
 | 
					
						
							|  |  |  |     trackers.set("piwik", piwik);
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     final ObjectNode trackingConfig = Json.newObject();
 | 
					
						
							|  |  |  |     trackingConfig.set("trackers", trackers);
 | 
					
						
							|  |  |  |     trackingConfig.put("isEnabled", true);
 | 
					
						
							|  |  |  |     return trackingConfig;
 | 
					
						
							|  |  |  |   }
 | 
					
						
							| 
									
										
										
										
											2021-08-20 10:58:07 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-22 16:33:14 -08:00
										 |  |  |   /**
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |    * Returns the value of the Authorization Header to be provided when proxying requests to the
 | 
					
						
							|  |  |  |    * downstream Metadata Service.
 | 
					
						
							| 
									
										
										
										
											2021-11-22 16:33:14 -08:00
										 |  |  |    *
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |    * <p>Currently, the Authorization header value may be derived from
 | 
					
						
							| 
									
										
										
										
											2021-11-22 16:33:14 -08:00
										 |  |  |    *
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |    * <p>a) The value of the "token" attribute of the Session Cookie provided by the client. This
 | 
					
						
							|  |  |  |    * value is set when creating the session token initially from a token granted by the Metadata
 | 
					
						
							|  |  |  |    * Service.
 | 
					
						
							| 
									
										
										
										
											2021-11-22 16:33:14 -08:00
										 |  |  |    *
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |    * <p>Or if the "token" attribute cannot be found in a session cookie, then we fallback to
 | 
					
						
							| 
									
										
										
										
											2021-11-22 16:33:14 -08:00
										 |  |  |    *
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |    * <p>b) The value of the Authorization header provided in the original request. This will be used
 | 
					
						
							|  |  |  |    * in cases where clients are making programmatic requests to Metadata Service APIs directly,
 | 
					
						
							|  |  |  |    * without providing a session cookie (ui only).
 | 
					
						
							| 
									
										
										
										
											2021-11-22 16:33:14 -08:00
										 |  |  |    *
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |    * <p>If neither are found, an empty string is returned.
 | 
					
						
							| 
									
										
										
										
											2021-11-22 16:33:14 -08:00
										 |  |  |    */
 | 
					
						
							| 
									
										
										
										
											2022-12-08 20:27:51 -06:00
										 |  |  |   private String getAuthorizationHeaderValueToProxy(Http.Request request) {
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |     // If the session cookie has an authorization token, use that. If there's an authorization
 | 
					
						
							|  |  |  |     // header provided, simply
 | 
					
						
							| 
									
										
										
										
											2021-11-22 16:33:14 -08:00
										 |  |  |     // use that.
 | 
					
						
							|  |  |  |     String value = "";
 | 
					
						
							| 
									
										
										
										
											2022-12-08 20:27:51 -06:00
										 |  |  |     if (request.session().data().containsKey(SESSION_COOKIE_GMS_TOKEN_NAME)) {
 | 
					
						
							|  |  |  |       value = "Bearer " + request.session().data().get(SESSION_COOKIE_GMS_TOKEN_NAME);
 | 
					
						
							|  |  |  |     } else if (request.getHeaders().contains(Http.HeaderNames.AUTHORIZATION)) {
 | 
					
						
							|  |  |  |       value = request.getHeaders().get(Http.HeaderNames.AUTHORIZATION).get();
 | 
					
						
							| 
									
										
										
										
											2021-11-22 16:33:14 -08:00
										 |  |  |     }
 | 
					
						
							|  |  |  |     return value;
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /**
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |    * Returns the value of the legacy X-DataHub-Actor header to forward to the Metadata Service. This
 | 
					
						
							|  |  |  |    * is sent along with any requests that have a valid frontend session cookie to identify the
 | 
					
						
							|  |  |  |    * calling actor, for backwards compatibility.
 | 
					
						
							| 
									
										
										
										
											2021-11-22 16:33:14 -08:00
										 |  |  |    *
 | 
					
						
							| 
									
										
										
										
											2023-12-06 11:02:42 +05:30
										 |  |  |    * <p>If Metadata Service authentication is enabled, this value is not required because Actor
 | 
					
						
							|  |  |  |    * context will most often come from the authentication credentials provided in the Authorization
 | 
					
						
							|  |  |  |    * header.
 | 
					
						
							| 
									
										
										
										
											2021-11-22 16:33:14 -08:00
										 |  |  |    */
 | 
					
						
							| 
									
										
										
										
											2022-12-08 20:27:51 -06:00
										 |  |  |   private String getDataHubActorHeader(Http.Request request) {
 | 
					
						
							|  |  |  |     String actor = request.session().data().get(ACTOR);
 | 
					
						
							| 
									
										
										
										
											2021-11-22 16:33:14 -08:00
										 |  |  |     return actor == null ? "" : actor;
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-08 13:37:31 -08:00
										 |  |  |   private String mapPath(@Nonnull final String path) {
 | 
					
						
							| 
									
										
										
										
											2025-10-01 11:08:38 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     final String strippedPath;
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Cannot strip base path from swagger urls
 | 
					
						
							|  |  |  |     if (SWAGGER_PATHS.stream().noneMatch(path::contains)) {
 | 
					
						
							|  |  |  |       // First, strip the base path if present
 | 
					
						
							|  |  |  |       strippedPath = BasePathUtils.stripBasePath(path, this.basePath);
 | 
					
						
							|  |  |  |     } else {
 | 
					
						
							|  |  |  |       strippedPath = path;
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-08 13:37:31 -08:00
										 |  |  |     // Case 1: Map legacy GraphQL path to GMS GraphQL API (for compatibility)
 | 
					
						
							| 
									
										
										
										
											2025-10-01 11:08:38 -05:00
										 |  |  |     if (strippedPath.equals("/api/v2/graphql")) {
 | 
					
						
							| 
									
										
										
										
											2021-11-08 13:37:31 -08:00
										 |  |  |       return "/api/graphql";
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Case 2: Map requests to /gms to / (Rest.li API)
 | 
					
						
							|  |  |  |     final String gmsApiPath = "/api/gms";
 | 
					
						
							| 
									
										
										
										
											2025-10-01 11:08:38 -05:00
										 |  |  |     if (strippedPath.startsWith(gmsApiPath)) {
 | 
					
						
							|  |  |  |       String newPath = strippedPath.substring(gmsApiPath.length());
 | 
					
						
							| 
									
										
										
										
											2022-09-21 14:21:55 -07:00
										 |  |  |       if (!newPath.startsWith("/")) {
 | 
					
						
							|  |  |  |         newPath = "/" + newPath;
 | 
					
						
							|  |  |  |       }
 | 
					
						
							|  |  |  |       return newPath;
 | 
					
						
							| 
									
										
										
										
											2021-11-08 13:37:31 -08:00
										 |  |  |     }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-10-01 11:08:38 -05:00
										 |  |  |     // Otherwise, return the stripped path
 | 
					
						
							|  |  |  |     return strippedPath;
 | 
					
						
							| 
									
										
										
										
											2021-11-08 13:37:31 -08:00
										 |  |  |   }
 | 
					
						
							| 
									
										
										
										
											2024-09-10 13:01:35 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |   /**
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |    * Called if verbose logging is enabled and request takes longer that the slow query milliseconds
 | 
					
						
							|  |  |  |    * defined in the config
 | 
					
						
							|  |  |  |    *
 | 
					
						
							| 
									
										
										
										
											2024-09-10 13:01:35 -04:00
										 |  |  |    * @param request GraphQL request that was made
 | 
					
						
							|  |  |  |    * @param resolvedUri URI that was requested
 | 
					
						
							|  |  |  |    * @param duration How long the query took to complete
 | 
					
						
							|  |  |  |    */
 | 
					
						
							|  |  |  |   private void logSlowQuery(Http.Request request, String resolvedUri, float duration) {
 | 
					
						
							|  |  |  |     StringBuilder jsonBody = new StringBuilder();
 | 
					
						
							|  |  |  |     Optional<Cookie> actorCookie = request.getCookie("actor");
 | 
					
						
							|  |  |  |     String actorValue = actorCookie.isPresent() ? actorCookie.get().value() : "N/A";
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-02 16:10:46 -04:00
										 |  |  |     // Get the JSON body
 | 
					
						
							| 
									
										
										
										
											2024-09-10 13:01:35 -04:00
										 |  |  |     try {
 | 
					
						
							|  |  |  |       ObjectMapper mapper = new ObjectMapper();
 | 
					
						
							|  |  |  |       JsonNode jsonNode = request.body().asJson();
 | 
					
						
							|  |  |  |       ((ObjectNode) jsonNode).remove("query");
 | 
					
						
							| 
									
										
										
										
											2025-04-02 16:10:46 -04:00
										 |  |  |       jsonBody.append(mapper.writeValueAsString(jsonNode));
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |     } catch (Exception e) {
 | 
					
						
							|  |  |  |       logger.info("GraphQL Request Received: {}, Unable to parse JSON body", resolvedUri);
 | 
					
						
							| 
									
										
										
										
											2024-09-10 13:01:35 -04:00
										 |  |  |     }
 | 
					
						
							|  |  |  |     String jsonBodyStr = jsonBody.toString();
 | 
					
						
							| 
									
										
										
										
											2025-04-02 16:10:46 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // Get the query string
 | 
					
						
							|  |  |  |     StringBuilder query = new StringBuilder();
 | 
					
						
							|  |  |  |     try {
 | 
					
						
							|  |  |  |       ObjectMapper mapper = new ObjectMapper();
 | 
					
						
							|  |  |  |       query.append(mapper.writeValueAsString(request.queryString()));
 | 
					
						
							|  |  |  |     } catch (Exception e) {
 | 
					
						
							|  |  |  |       logger.info("GraphQL Request Received: {}, Unable to parse query string", resolvedUri);
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  |     String queryString = query.toString();
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |     logger.info(
 | 
					
						
							|  |  |  |         "Slow GraphQL Request Received: {}, Request query string: {}, Request actor: {}, Request JSON: {}, Request completed in {} ms",
 | 
					
						
							|  |  |  |         resolvedUri,
 | 
					
						
							| 
									
										
										
										
											2025-04-02 16:10:46 -04:00
										 |  |  |         queryString,
 | 
					
						
							| 
									
										
										
										
											2024-10-28 09:05:16 -05:00
										 |  |  |         actorValue,
 | 
					
						
							|  |  |  |         jsonBodyStr,
 | 
					
						
							|  |  |  |         duration);
 | 
					
						
							| 
									
										
										
										
											2024-09-10 13:01:35 -04:00
										 |  |  |   }
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  | }
 |