mirror of
				https://github.com/datahub-project/datahub.git
				synced 2025-11-03 20:27:50 +00:00 
			
		
		
		
	graphql & backend + models
This commit is contained in:
		
							parent
							
								
									de75524ca5
								
							
						
					
					
						commit
						6a5b619e1d
					
				@ -26,6 +26,11 @@ import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.featureflags.FeatureFlags;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.*;
 | 
			
		||||
import com.linkedin.datahub.graphql.resolvers.MeResolver;
 | 
			
		||||
import com.linkedin.datahub.graphql.resolvers.application.BatchSetApplicationResolver;
 | 
			
		||||
import com.linkedin.datahub.graphql.resolvers.application.CreateApplicationResolver;
 | 
			
		||||
import com.linkedin.datahub.graphql.resolvers.application.DeleteApplicationResolver;
 | 
			
		||||
import com.linkedin.datahub.graphql.resolvers.application.ListApplicationAssetsResolver;
 | 
			
		||||
import com.linkedin.datahub.graphql.resolvers.application.UpdateApplicationResolver;
 | 
			
		||||
import com.linkedin.datahub.graphql.resolvers.assertion.AssertionRunEventResolver;
 | 
			
		||||
import com.linkedin.datahub.graphql.resolvers.assertion.DeleteAssertionResolver;
 | 
			
		||||
import com.linkedin.datahub.graphql.resolvers.assertion.EntityAssertionsResolver;
 | 
			
		||||
@ -241,6 +246,7 @@ import com.linkedin.datahub.graphql.types.BrowsableEntityType;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.EntityType;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.LoadableType;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.SearchableEntityType;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.application.ApplicationType;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.aspect.AspectType;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.assertion.AssertionType;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.auth.AccessTokenMetadataType;
 | 
			
		||||
@ -315,6 +321,7 @@ import com.linkedin.metadata.models.registry.EntityRegistry;
 | 
			
		||||
import com.linkedin.metadata.query.filter.SortCriterion;
 | 
			
		||||
import com.linkedin.metadata.query.filter.SortOrder;
 | 
			
		||||
import com.linkedin.metadata.recommendation.RecommendationsService;
 | 
			
		||||
import com.linkedin.metadata.service.ApplicationService;
 | 
			
		||||
import com.linkedin.metadata.service.AssertionService;
 | 
			
		||||
import com.linkedin.metadata.service.BusinessAttributeService;
 | 
			
		||||
import com.linkedin.metadata.service.DataProductService;
 | 
			
		||||
@ -395,6 +402,7 @@ public class GmsGraphQLEngine {
 | 
			
		||||
  private ConnectionService connectionService;
 | 
			
		||||
  private AssertionService assertionService;
 | 
			
		||||
  private final EntityVersioningService entityVersioningService;
 | 
			
		||||
  private final ApplicationService applicationService;
 | 
			
		||||
 | 
			
		||||
  private final BusinessAttributeService businessAttributeService;
 | 
			
		||||
  private final FeatureFlags featureFlags;
 | 
			
		||||
@ -447,6 +455,7 @@ public class GmsGraphQLEngine {
 | 
			
		||||
  private final DataHubViewType dataHubViewType;
 | 
			
		||||
  private final QueryType queryType;
 | 
			
		||||
  private final DataProductType dataProductType;
 | 
			
		||||
  private final ApplicationType applicationType;
 | 
			
		||||
  private final OwnershipType ownershipType;
 | 
			
		||||
  private final StructuredPropertyType structuredPropertyType;
 | 
			
		||||
  private final DataTypeType dataTypeType;
 | 
			
		||||
@ -521,6 +530,7 @@ public class GmsGraphQLEngine {
 | 
			
		||||
    this.queryService = args.queryService;
 | 
			
		||||
    this.erModelRelationshipService = args.erModelRelationshipService;
 | 
			
		||||
    this.dataProductService = args.dataProductService;
 | 
			
		||||
    this.applicationService = args.applicationService;
 | 
			
		||||
    this.formService = args.formService;
 | 
			
		||||
    this.restrictedService = args.restrictedService;
 | 
			
		||||
    this.connectionService = args.connectionService;
 | 
			
		||||
@ -575,6 +585,7 @@ public class GmsGraphQLEngine {
 | 
			
		||||
    this.dataHubViewType = new DataHubViewType(entityClient);
 | 
			
		||||
    this.queryType = new QueryType(entityClient);
 | 
			
		||||
    this.dataProductType = new DataProductType(entityClient);
 | 
			
		||||
    this.applicationType = new ApplicationType(entityClient);
 | 
			
		||||
    this.ownershipType = new OwnershipType(entityClient);
 | 
			
		||||
    this.structuredPropertyType = new StructuredPropertyType(entityClient);
 | 
			
		||||
    this.dataTypeType = new DataTypeType(entityClient);
 | 
			
		||||
@ -640,7 +651,8 @@ public class GmsGraphQLEngine {
 | 
			
		||||
                restrictedType,
 | 
			
		||||
                businessAttributeType,
 | 
			
		||||
                dataProcessInstanceType,
 | 
			
		||||
                executionRequestType));
 | 
			
		||||
                executionRequestType,
 | 
			
		||||
                applicationType));
 | 
			
		||||
    this.loadableTypes = new ArrayList<>(entityTypes);
 | 
			
		||||
    this.loadableTypes.add(ingestionSourceType);
 | 
			
		||||
    // Extend loadable types with types from the plugins
 | 
			
		||||
@ -717,6 +729,7 @@ public class GmsGraphQLEngine {
 | 
			
		||||
    configureGlossaryNodeResolvers(builder);
 | 
			
		||||
    configureDomainResolvers(builder);
 | 
			
		||||
    configureDataProductResolvers(builder);
 | 
			
		||||
    configureApplicationResolvers(builder);
 | 
			
		||||
    configureAssertionResolvers(builder);
 | 
			
		||||
    configureContractResolvers(builder);
 | 
			
		||||
    configurePolicyResolvers(builder);
 | 
			
		||||
@ -1047,8 +1060,11 @@ public class GmsGraphQLEngine {
 | 
			
		||||
                    "getQuickFilters",
 | 
			
		||||
                    new GetQuickFiltersResolver(this.entityClient, this.viewService))
 | 
			
		||||
                .dataFetcher("dataProduct", getResolver(dataProductType))
 | 
			
		||||
                .dataFetcher("application", getResolver(applicationType))
 | 
			
		||||
                .dataFetcher(
 | 
			
		||||
                    "listDataProductAssets", new ListDataProductAssetsResolver(this.entityClient))
 | 
			
		||||
                .dataFetcher(
 | 
			
		||||
                    "listApplicationAssets", new ListApplicationAssetsResolver(this.entityClient))
 | 
			
		||||
                .dataFetcher(
 | 
			
		||||
                    "listOwnershipTypes", new ListOwnershipTypesResolver(this.entityClient))
 | 
			
		||||
                .dataFetcher(
 | 
			
		||||
@ -1284,6 +1300,15 @@ public class GmsGraphQLEngine {
 | 
			
		||||
                  "deleteDataProduct", new DeleteDataProductResolver(this.dataProductService))
 | 
			
		||||
              .dataFetcher(
 | 
			
		||||
                  "batchSetDataProduct", new BatchSetDataProductResolver(this.dataProductService))
 | 
			
		||||
              .dataFetcher(
 | 
			
		||||
                  "createApplication",
 | 
			
		||||
                  new CreateApplicationResolver(this.applicationService, this.entityService))
 | 
			
		||||
              .dataFetcher(
 | 
			
		||||
                  "updateApplication", new UpdateApplicationResolver(this.applicationService))
 | 
			
		||||
              .dataFetcher(
 | 
			
		||||
                  "deleteApplication", new DeleteApplicationResolver(this.applicationService))
 | 
			
		||||
              .dataFetcher(
 | 
			
		||||
                  "batchSetApplication", new BatchSetApplicationResolver(this.applicationService))
 | 
			
		||||
              .dataFetcher(
 | 
			
		||||
                  "createOwnershipType", new CreateOwnershipTypeResolver(this.ownershipTypeService))
 | 
			
		||||
              .dataFetcher(
 | 
			
		||||
@ -2834,6 +2859,18 @@ public class GmsGraphQLEngine {
 | 
			
		||||
                .dataFetcher("relationships", new EntityRelationshipsResultResolver(graphClient)));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private void configureApplicationResolvers(final RuntimeWiring.Builder builder) {
 | 
			
		||||
    builder.type(
 | 
			
		||||
        "Application",
 | 
			
		||||
        typeWiring ->
 | 
			
		||||
            typeWiring
 | 
			
		||||
                .dataFetcher("entities", new ListApplicationAssetsResolver(this.entityClient))
 | 
			
		||||
                .dataFetcher("privileges", new EntityPrivilegesResolver(entityClient))
 | 
			
		||||
                .dataFetcher(
 | 
			
		||||
                    "aspects", new WeaklyTypedAspectsResolver(entityClient, entityRegistry))
 | 
			
		||||
                .dataFetcher("relationships", new EntityRelationshipsResultResolver(graphClient)));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private void configureAssertionResolvers(final RuntimeWiring.Builder builder) {
 | 
			
		||||
    builder.type(
 | 
			
		||||
        "Assertion",
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,7 @@ import com.linkedin.metadata.graph.GraphClient;
 | 
			
		||||
import com.linkedin.metadata.graph.SiblingGraphService;
 | 
			
		||||
import com.linkedin.metadata.models.registry.EntityRegistry;
 | 
			
		||||
import com.linkedin.metadata.recommendation.RecommendationsService;
 | 
			
		||||
import com.linkedin.metadata.service.ApplicationService;
 | 
			
		||||
import com.linkedin.metadata.service.AssertionService;
 | 
			
		||||
import com.linkedin.metadata.service.BusinessAttributeService;
 | 
			
		||||
import com.linkedin.metadata.service.DataProductService;
 | 
			
		||||
@ -96,6 +97,7 @@ public class GmsGraphQLEngineArgs {
 | 
			
		||||
  ConnectionService connectionService;
 | 
			
		||||
  AssertionService assertionService;
 | 
			
		||||
  EntityVersioningService entityVersioningService;
 | 
			
		||||
  ApplicationService applicationService;
 | 
			
		||||
 | 
			
		||||
  // any fork specific args should go below this line
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,3 @@
 | 
			
		||||
package com.linkedin.datahub.graphql;
 | 
			
		||||
 | 
			
		||||
public class ListApplicationAssetsResolver {}
 | 
			
		||||
@ -0,0 +1,137 @@
 | 
			
		||||
package com.linkedin.datahub.graphql.resolvers.application;
 | 
			
		||||
 | 
			
		||||
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
 | 
			
		||||
 | 
			
		||||
import com.datahub.authorization.ConjunctivePrivilegeGroup;
 | 
			
		||||
import com.datahub.authorization.DisjunctivePrivilegeGroup;
 | 
			
		||||
import com.google.common.collect.ImmutableList;
 | 
			
		||||
import com.linkedin.common.urn.Urn;
 | 
			
		||||
import com.linkedin.common.urn.UrnUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.QueryContext;
 | 
			
		||||
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.exception.AuthorizationException;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.BatchSetApplicationInput;
 | 
			
		||||
import com.linkedin.metadata.authorization.PoliciesConfig;
 | 
			
		||||
import com.linkedin.metadata.service.ApplicationService;
 | 
			
		||||
import graphql.schema.DataFetcher;
 | 
			
		||||
import graphql.schema.DataFetchingEnvironment;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
@RequiredArgsConstructor
 | 
			
		||||
public class BatchSetApplicationResolver implements DataFetcher<CompletableFuture<Boolean>> {
 | 
			
		||||
 | 
			
		||||
  private final ApplicationService _applicationService;
 | 
			
		||||
 | 
			
		||||
  private static final ConjunctivePrivilegeGroup ALL_PRIVILEGES_GROUP =
 | 
			
		||||
      new ConjunctivePrivilegeGroup(
 | 
			
		||||
          ImmutableList.of(PoliciesConfig.EDIT_ENTITY_PRIVILEGE.getType()));
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public CompletableFuture<Boolean> get(DataFetchingEnvironment environment) throws Exception {
 | 
			
		||||
    final QueryContext context = environment.getContext();
 | 
			
		||||
    final BatchSetApplicationInput input =
 | 
			
		||||
        bindArgument(environment.getArgument("input"), BatchSetApplicationInput.class);
 | 
			
		||||
    final String maybeApplicationUrn = input.getApplicationUrn();
 | 
			
		||||
    final List<String> resources = input.getResourceUrns();
 | 
			
		||||
 | 
			
		||||
    return GraphQLConcurrencyUtils.supplyAsync(
 | 
			
		||||
        () -> {
 | 
			
		||||
          verifyResources(resources, context);
 | 
			
		||||
          verifyApplication(maybeApplicationUrn, context);
 | 
			
		||||
 | 
			
		||||
          try {
 | 
			
		||||
            List<Urn> resourceUrns =
 | 
			
		||||
                resources.stream().map(UrnUtils::getUrn).collect(Collectors.toList());
 | 
			
		||||
            if (maybeApplicationUrn != null) {
 | 
			
		||||
              batchSetApplication(maybeApplicationUrn, resourceUrns, context);
 | 
			
		||||
            } else {
 | 
			
		||||
              batchUnsetApplication(resourceUrns, context);
 | 
			
		||||
            }
 | 
			
		||||
            return true;
 | 
			
		||||
          } catch (Exception e) {
 | 
			
		||||
            log.error(
 | 
			
		||||
                "Failed to perform update against input {}, {}", input.toString(), e.getMessage());
 | 
			
		||||
            throw new RuntimeException(
 | 
			
		||||
                String.format("Failed to perform update against input %s", input.toString()), e);
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        this.getClass().getSimpleName(),
 | 
			
		||||
        "get");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private void verifyResources(List<String> resources, QueryContext context) {
 | 
			
		||||
    for (String resource : resources) {
 | 
			
		||||
      if (!_applicationService.verifyEntityExists(
 | 
			
		||||
          context.getOperationContext(), UrnUtils.getUrn(resource))) {
 | 
			
		||||
        throw new RuntimeException(
 | 
			
		||||
            String.format(
 | 
			
		||||
                "Failed to batch set Application, %s in resources does not exist", resource));
 | 
			
		||||
      }
 | 
			
		||||
      Urn resourceUrn = UrnUtils.getUrn(resource);
 | 
			
		||||
      if (!AuthorizationUtils.isAuthorized(
 | 
			
		||||
          context,
 | 
			
		||||
          resourceUrn.getEntityType(),
 | 
			
		||||
          resourceUrn.toString(),
 | 
			
		||||
          new DisjunctivePrivilegeGroup(
 | 
			
		||||
              ImmutableList.of(
 | 
			
		||||
                  ALL_PRIVILEGES_GROUP,
 | 
			
		||||
                  new ConjunctivePrivilegeGroup(
 | 
			
		||||
                      ImmutableList.of(
 | 
			
		||||
                          PoliciesConfig.EDIT_ENTITY_DOMAINS_PRIVILEGE.getType())))))) {
 | 
			
		||||
        throw new AuthorizationException(
 | 
			
		||||
            "Unauthorized to perform this action. Please contact your DataHub administrator.");
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private void verifyApplication(String maybeApplicationUrn, QueryContext context) {
 | 
			
		||||
    if (maybeApplicationUrn != null
 | 
			
		||||
        && !_applicationService.verifyEntityExists(
 | 
			
		||||
            context.getOperationContext(), UrnUtils.getUrn(maybeApplicationUrn))) {
 | 
			
		||||
      throw new RuntimeException(
 | 
			
		||||
          String.format(
 | 
			
		||||
              "Failed to batch set Application, Application urn %s does not exist",
 | 
			
		||||
              maybeApplicationUrn));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private void batchSetApplication(
 | 
			
		||||
      @Nonnull String applicationUrn, List<Urn> resources, QueryContext context) {
 | 
			
		||||
    log.debug(
 | 
			
		||||
        "Batch setting Application. application urn: {}, resources: {}", applicationUrn, resources);
 | 
			
		||||
    try {
 | 
			
		||||
      _applicationService.batchSetApplicationAssets(
 | 
			
		||||
          context.getOperationContext(),
 | 
			
		||||
          UrnUtils.getUrn(applicationUrn),
 | 
			
		||||
          resources,
 | 
			
		||||
          UrnUtils.getUrn(context.getActorUrn()));
 | 
			
		||||
    } catch (Exception e) {
 | 
			
		||||
      throw new RuntimeException(
 | 
			
		||||
          String.format(
 | 
			
		||||
              "Failed to batch set Application %s to resources with urns %s!",
 | 
			
		||||
              applicationUrn, resources),
 | 
			
		||||
          e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private void batchUnsetApplication(List<Urn> resources, QueryContext context) {
 | 
			
		||||
    log.debug("Batch unsetting Application. resources: {}", resources);
 | 
			
		||||
    try {
 | 
			
		||||
      for (Urn resource : resources) {
 | 
			
		||||
        _applicationService.unsetApplication(
 | 
			
		||||
            context.getOperationContext(), resource, UrnUtils.getUrn(context.getActorUrn()));
 | 
			
		||||
      }
 | 
			
		||||
    } catch (Exception e) {
 | 
			
		||||
      throw new RuntimeException(
 | 
			
		||||
          String.format("Failed to batch unset application for resources with urns %s!", resources),
 | 
			
		||||
          e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,96 @@
 | 
			
		||||
package com.linkedin.datahub.graphql.resolvers.application;
 | 
			
		||||
 | 
			
		||||
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
 | 
			
		||||
 | 
			
		||||
import com.datahub.authentication.Authentication;
 | 
			
		||||
import com.datahub.authorization.ConjunctivePrivilegeGroup;
 | 
			
		||||
import com.datahub.authorization.DisjunctivePrivilegeGroup;
 | 
			
		||||
import com.google.common.collect.ImmutableList;
 | 
			
		||||
import com.linkedin.common.urn.Urn;
 | 
			
		||||
import com.linkedin.common.urn.UrnUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.QueryContext;
 | 
			
		||||
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.exception.AuthorizationException;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.Application;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.CreateApplicationInput;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.OwnerEntityType;
 | 
			
		||||
import com.linkedin.datahub.graphql.resolvers.mutate.util.OwnerUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.application.mappers.ApplicationMapper;
 | 
			
		||||
import com.linkedin.entity.EntityResponse;
 | 
			
		||||
import com.linkedin.metadata.authorization.PoliciesConfig;
 | 
			
		||||
import com.linkedin.metadata.entity.EntityService;
 | 
			
		||||
import com.linkedin.metadata.service.ApplicationService;
 | 
			
		||||
import graphql.schema.DataFetcher;
 | 
			
		||||
import graphql.schema.DataFetchingEnvironment;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
@RequiredArgsConstructor
 | 
			
		||||
public class CreateApplicationResolver implements DataFetcher<CompletableFuture<Application>> {
 | 
			
		||||
 | 
			
		||||
  private final ApplicationService _applicationService;
 | 
			
		||||
  private final EntityService _entityService;
 | 
			
		||||
 | 
			
		||||
  private static final ConjunctivePrivilegeGroup ALL_PRIVILEGES_GROUP =
 | 
			
		||||
      new ConjunctivePrivilegeGroup(
 | 
			
		||||
          ImmutableList.of(PoliciesConfig.EDIT_ENTITY_PRIVILEGE.getType()));
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public CompletableFuture<Application> get(final DataFetchingEnvironment environment)
 | 
			
		||||
      throws Exception {
 | 
			
		||||
 | 
			
		||||
    final QueryContext context = environment.getContext();
 | 
			
		||||
    final CreateApplicationInput input =
 | 
			
		||||
        bindArgument(environment.getArgument("input"), CreateApplicationInput.class);
 | 
			
		||||
    final Authentication authentication = context.getAuthentication();
 | 
			
		||||
    final Urn domainUrn = UrnUtils.getUrn(input.getDomainUrn());
 | 
			
		||||
 | 
			
		||||
    return GraphQLConcurrencyUtils.supplyAsync(
 | 
			
		||||
        () -> {
 | 
			
		||||
          if (!_applicationService.verifyEntityExists(context.getOperationContext(), domainUrn)) {
 | 
			
		||||
            throw new IllegalArgumentException("The Domain provided dos not exist");
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          final DisjunctivePrivilegeGroup orPrivilegeGroup =
 | 
			
		||||
              new DisjunctivePrivilegeGroup(ImmutableList.of(ALL_PRIVILEGES_GROUP));
 | 
			
		||||
 | 
			
		||||
          if (!AuthorizationUtils.isAuthorized(
 | 
			
		||||
              context, domainUrn.getEntityType(), domainUrn.toString(), orPrivilegeGroup)) {
 | 
			
		||||
            throw new AuthorizationException(
 | 
			
		||||
                "Unauthorized to perform this action. Please contact your DataHub administrator.");
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          try {
 | 
			
		||||
            final Urn applicationUrn =
 | 
			
		||||
                _applicationService.createApplication(
 | 
			
		||||
                    context.getOperationContext(),
 | 
			
		||||
                    input.getId(),
 | 
			
		||||
                    input.getProperties().getName(),
 | 
			
		||||
                    input.getProperties().getDescription());
 | 
			
		||||
            _applicationService.setDomain(
 | 
			
		||||
                context.getOperationContext(),
 | 
			
		||||
                applicationUrn,
 | 
			
		||||
                UrnUtils.getUrn(input.getDomainUrn()));
 | 
			
		||||
            OwnerUtils.addCreatorAsOwner(
 | 
			
		||||
                context, applicationUrn.toString(), OwnerEntityType.CORP_USER, _entityService);
 | 
			
		||||
            EntityResponse response =
 | 
			
		||||
                _applicationService.getApplicationEntityResponse(
 | 
			
		||||
                    context.getOperationContext(), applicationUrn);
 | 
			
		||||
            if (response != null) {
 | 
			
		||||
              return ApplicationMapper.map(context, response);
 | 
			
		||||
            }
 | 
			
		||||
            // should never happen
 | 
			
		||||
            log.error(String.format("Unable to find application with urn %s", applicationUrn));
 | 
			
		||||
            return null;
 | 
			
		||||
          } catch (Exception e) {
 | 
			
		||||
            throw new RuntimeException(
 | 
			
		||||
                String.format("Failed to create a new Application from input %s", input), e);
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        this.getClass().getSimpleName(),
 | 
			
		||||
        "get");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,71 @@
 | 
			
		||||
package com.linkedin.datahub.graphql.resolvers.application;
 | 
			
		||||
 | 
			
		||||
import com.datahub.authentication.Authentication;
 | 
			
		||||
import com.datahub.authorization.ConjunctivePrivilegeGroup;
 | 
			
		||||
import com.datahub.authorization.DisjunctivePrivilegeGroup;
 | 
			
		||||
import com.google.common.collect.ImmutableList;
 | 
			
		||||
import com.linkedin.common.urn.Urn;
 | 
			
		||||
import com.linkedin.common.urn.UrnUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.QueryContext;
 | 
			
		||||
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.exception.AuthorizationException;
 | 
			
		||||
import com.linkedin.domain.Domains;
 | 
			
		||||
import com.linkedin.metadata.authorization.PoliciesConfig;
 | 
			
		||||
import com.linkedin.metadata.service.ApplicationService;
 | 
			
		||||
import graphql.schema.DataFetcher;
 | 
			
		||||
import graphql.schema.DataFetchingEnvironment;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
@RequiredArgsConstructor
 | 
			
		||||
public class DeleteApplicationResolver implements DataFetcher<CompletableFuture<Boolean>> {
 | 
			
		||||
 | 
			
		||||
  private final ApplicationService _applicationService;
 | 
			
		||||
 | 
			
		||||
  private static final ConjunctivePrivilegeGroup ALL_PRIVILEGES_GROUP =
 | 
			
		||||
      new ConjunctivePrivilegeGroup(
 | 
			
		||||
          ImmutableList.of(PoliciesConfig.EDIT_ENTITY_PRIVILEGE.getType()));
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public CompletableFuture<Boolean> get(final DataFetchingEnvironment environment)
 | 
			
		||||
      throws Exception {
 | 
			
		||||
    final QueryContext context = environment.getContext();
 | 
			
		||||
    final Urn applicationUrn = UrnUtils.getUrn(environment.getArgument("urn"));
 | 
			
		||||
    final Authentication authentication = context.getAuthentication();
 | 
			
		||||
 | 
			
		||||
    return GraphQLConcurrencyUtils.supplyAsync(
 | 
			
		||||
        () -> {
 | 
			
		||||
          if (!_applicationService.verifyEntityExists(
 | 
			
		||||
              context.getOperationContext(), applicationUrn)) {
 | 
			
		||||
            throw new IllegalArgumentException("The Application provided dos not exist");
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          Domains domains =
 | 
			
		||||
              _applicationService.getApplicationDomains(
 | 
			
		||||
                  context.getOperationContext(), applicationUrn);
 | 
			
		||||
          if (domains != null && domains.hasDomains() && domains.getDomains().size() > 0) {
 | 
			
		||||
            // get first domain since we only allow one domain right now
 | 
			
		||||
            Urn domainUrn = UrnUtils.getUrn(domains.getDomains().get(0).toString());
 | 
			
		||||
            final DisjunctivePrivilegeGroup orPrivilegeGroup =
 | 
			
		||||
                new DisjunctivePrivilegeGroup(ImmutableList.of(ALL_PRIVILEGES_GROUP));
 | 
			
		||||
            if (!AuthorizationUtils.isAuthorized(
 | 
			
		||||
                context, domainUrn.getEntityType(), domainUrn.toString(), orPrivilegeGroup)) {
 | 
			
		||||
              throw new AuthorizationException(
 | 
			
		||||
                  "Unauthorized to perform this action. Please contact your DataHub administrator.");
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          try {
 | 
			
		||||
            _applicationService.deleteApplication(context.getOperationContext(), applicationUrn);
 | 
			
		||||
            return true;
 | 
			
		||||
          } catch (Exception e) {
 | 
			
		||||
            throw new RuntimeException("Failed to delete Application", e);
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        this.getClass().getSimpleName(),
 | 
			
		||||
        "get");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,207 @@
 | 
			
		||||
package com.linkedin.datahub.graphql.resolvers.application;
 | 
			
		||||
 | 
			
		||||
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
 | 
			
		||||
import static com.linkedin.metadata.search.utils.QueryUtils.buildFilterWithUrns;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.ImmutableList;
 | 
			
		||||
import com.linkedin.application.ApplicationAssociation;
 | 
			
		||||
import com.linkedin.application.ApplicationProperties;
 | 
			
		||||
import com.linkedin.common.urn.Urn;
 | 
			
		||||
import com.linkedin.common.urn.UrnUtils;
 | 
			
		||||
import com.linkedin.data.DataMap;
 | 
			
		||||
import com.linkedin.datahub.graphql.QueryContext;
 | 
			
		||||
import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.Application;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.EntityType;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.FacetFilterInput;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.SearchAcrossEntitiesInput;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.SearchResults;
 | 
			
		||||
import com.linkedin.datahub.graphql.resolvers.ResolverUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.common.mappers.SearchFlagsInputMapper;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.entitytype.EntityTypeMapper;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.mappers.UrnSearchResultsMapper;
 | 
			
		||||
import com.linkedin.entity.EntityResponse;
 | 
			
		||||
import com.linkedin.entity.client.EntityClient;
 | 
			
		||||
import com.linkedin.metadata.Constants;
 | 
			
		||||
import com.linkedin.metadata.query.SearchFlags;
 | 
			
		||||
import com.linkedin.metadata.query.filter.Filter;
 | 
			
		||||
import graphql.schema.DataFetcher;
 | 
			
		||||
import graphql.schema.DataFetchingEnvironment;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.HashSet;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Resolver responsible for getting the assets belonging to an Application. Get the assets from the
 | 
			
		||||
 * Application aspect, then use search to query and filter for specific assets.
 | 
			
		||||
 */
 | 
			
		||||
@Slf4j
 | 
			
		||||
@RequiredArgsConstructor
 | 
			
		||||
public class ListApplicationAssetsResolver
 | 
			
		||||
    implements DataFetcher<CompletableFuture<SearchResults>> {
 | 
			
		||||
 | 
			
		||||
  private static final int DEFAULT_START = 0;
 | 
			
		||||
  private static final int DEFAULT_COUNT = 10;
 | 
			
		||||
 | 
			
		||||
  private final EntityClient _entityClient;
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public CompletableFuture<SearchResults> get(DataFetchingEnvironment environment) {
 | 
			
		||||
    final QueryContext context = environment.getContext();
 | 
			
		||||
    // get urn from either input or source (in the case of "entities" field)
 | 
			
		||||
    final String urn =
 | 
			
		||||
        environment.getArgument("urn") != null
 | 
			
		||||
            ? environment.getArgument("urn")
 | 
			
		||||
            : ((Application) environment.getSource()).getUrn();
 | 
			
		||||
    final Urn applicationUrn = UrnUtils.getUrn(urn);
 | 
			
		||||
    final SearchAcrossEntitiesInput input =
 | 
			
		||||
        bindArgument(environment.getArgument("input"), SearchAcrossEntitiesInput.class);
 | 
			
		||||
 | 
			
		||||
    // 1. Get urns of assets belonging to Application using an aspect query
 | 
			
		||||
    List<Urn> assetUrns = new ArrayList<>();
 | 
			
		||||
    try {
 | 
			
		||||
      final EntityResponse entityResponse =
 | 
			
		||||
          _entityClient.getV2(
 | 
			
		||||
              context.getOperationContext(),
 | 
			
		||||
              Constants.APPLICATION_ENTITY_NAME,
 | 
			
		||||
              applicationUrn,
 | 
			
		||||
              Collections.singleton(Constants.APPLICATION_PROPERTIES_ASPECT_NAME));
 | 
			
		||||
      if (entityResponse != null
 | 
			
		||||
          && entityResponse
 | 
			
		||||
              .getAspects()
 | 
			
		||||
              .containsKey(Constants.APPLICATION_PROPERTIES_ASPECT_NAME)) {
 | 
			
		||||
        final DataMap data =
 | 
			
		||||
            entityResponse
 | 
			
		||||
                .getAspects()
 | 
			
		||||
                .get(Constants.APPLICATION_PROPERTIES_ASPECT_NAME)
 | 
			
		||||
                .getValue()
 | 
			
		||||
                .data();
 | 
			
		||||
        final ApplicationProperties applicationProperties = new ApplicationProperties(data);
 | 
			
		||||
        if (applicationProperties.hasAssets()) {
 | 
			
		||||
          assetUrns.addAll(
 | 
			
		||||
              applicationProperties.getAssets().stream()
 | 
			
		||||
                  .map(ApplicationAssociation::getDestinationUrn)
 | 
			
		||||
                  .collect(Collectors.toList()));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch (Exception e) {
 | 
			
		||||
      log.error(String.format("Failed to list application assets with urn %s", applicationUrn), e);
 | 
			
		||||
      throw new RuntimeException(
 | 
			
		||||
          String.format("Failed to list application assets with urn %s", applicationUrn), e);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 2. Get list of entities that we should query based on filters or assets from aspect.
 | 
			
		||||
    List<String> entitiesToQuery =
 | 
			
		||||
        assetUrns.stream().map(Urn::getEntityType).distinct().collect(Collectors.toList());
 | 
			
		||||
 | 
			
		||||
    final List<EntityType> inputEntityTypes =
 | 
			
		||||
        (input.getTypes() == null || input.getTypes().isEmpty())
 | 
			
		||||
            ? ImmutableList.of()
 | 
			
		||||
            : input.getTypes();
 | 
			
		||||
    final List<String> inputEntityNames =
 | 
			
		||||
        inputEntityTypes.stream()
 | 
			
		||||
            .map(EntityTypeMapper::getName)
 | 
			
		||||
            .distinct()
 | 
			
		||||
            .collect(Collectors.toList());
 | 
			
		||||
 | 
			
		||||
    final List<String> finalEntityNames =
 | 
			
		||||
        inputEntityNames.size() > 0 ? inputEntityNames : entitiesToQuery;
 | 
			
		||||
 | 
			
		||||
    // escape forward slash since it is a reserved character in Elasticsearch
 | 
			
		||||
    final String sanitizedQuery = ResolverUtils.escapeForwardSlash(input.getQuery());
 | 
			
		||||
 | 
			
		||||
    final int start = input.getStart() != null ? input.getStart() : DEFAULT_START;
 | 
			
		||||
    final int count = input.getCount() != null ? input.getCount() : DEFAULT_COUNT;
 | 
			
		||||
 | 
			
		||||
    return GraphQLConcurrencyUtils.supplyAsync(
 | 
			
		||||
        () -> {
 | 
			
		||||
          // if no assets in application properties, exit early before search and return empty
 | 
			
		||||
          // results
 | 
			
		||||
          if (assetUrns.size() == 0) {
 | 
			
		||||
            SearchResults results = new SearchResults();
 | 
			
		||||
            results.setStart(start);
 | 
			
		||||
            results.setCount(count);
 | 
			
		||||
            results.setTotal(0);
 | 
			
		||||
            results.setSearchResults(ImmutableList.of());
 | 
			
		||||
            return results;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          List<FacetFilterInput> filters = input.getFilters();
 | 
			
		||||
          final List<Urn> urnsToFilterOn = getUrnsToFilterOn(assetUrns, filters);
 | 
			
		||||
          // add urns from the aspect to our filters
 | 
			
		||||
          final Filter baseFilter = ResolverUtils.buildFilter(filters, input.getOrFilters());
 | 
			
		||||
          final Filter finalFilter =
 | 
			
		||||
              buildFilterWithUrns(
 | 
			
		||||
                  context.getDataHubAppConfig(), new HashSet<>(urnsToFilterOn), baseFilter);
 | 
			
		||||
 | 
			
		||||
          final SearchFlags searchFlags;
 | 
			
		||||
          com.linkedin.datahub.graphql.generated.SearchFlags inputFlags = input.getSearchFlags();
 | 
			
		||||
          if (inputFlags != null) {
 | 
			
		||||
            searchFlags = SearchFlagsInputMapper.INSTANCE.apply(context, inputFlags);
 | 
			
		||||
          } else {
 | 
			
		||||
            searchFlags = null;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          try {
 | 
			
		||||
            log.debug(
 | 
			
		||||
                "Executing search for application assets: entity types {}, query {}, filters: {}, start: {}, count: {}",
 | 
			
		||||
                input.getTypes(),
 | 
			
		||||
                input.getQuery(),
 | 
			
		||||
                input.getOrFilters(),
 | 
			
		||||
                start,
 | 
			
		||||
                count);
 | 
			
		||||
 | 
			
		||||
            SearchResults results =
 | 
			
		||||
                UrnSearchResultsMapper.map(
 | 
			
		||||
                    context,
 | 
			
		||||
                    _entityClient.searchAcrossEntities(
 | 
			
		||||
                        context
 | 
			
		||||
                            .getOperationContext()
 | 
			
		||||
                            .withSearchFlags(flags -> searchFlags != null ? searchFlags : flags),
 | 
			
		||||
                        finalEntityNames,
 | 
			
		||||
                        sanitizedQuery,
 | 
			
		||||
                        finalFilter,
 | 
			
		||||
                        start,
 | 
			
		||||
                        count,
 | 
			
		||||
                        null));
 | 
			
		||||
            return results;
 | 
			
		||||
          } catch (Exception e) {
 | 
			
		||||
            log.error(
 | 
			
		||||
                "Failed to execute search for application assets: entity types {}, query {}, filters: {}, start: {}, count: {}",
 | 
			
		||||
                input.getTypes(),
 | 
			
		||||
                input.getQuery(),
 | 
			
		||||
                input.getOrFilters(),
 | 
			
		||||
                start,
 | 
			
		||||
                count);
 | 
			
		||||
            throw new RuntimeException(
 | 
			
		||||
                "Failed to execute search: "
 | 
			
		||||
                    + String.format(
 | 
			
		||||
                        "entity types %s, query %s, filters: %s, start: %s, count: %s",
 | 
			
		||||
                        input.getTypes(), input.getQuery(), input.getOrFilters(), start, count),
 | 
			
		||||
                e);
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        this.getClass().getSimpleName(),
 | 
			
		||||
        "get");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Check to see if our filters list has a hardcoded filter for output ports. If so, let this
 | 
			
		||||
   * filter determine which urns we filter search results on. Otherwise, if no output port filter is
 | 
			
		||||
   * found, return all asset urns as per usual.
 | 
			
		||||
   */
 | 
			
		||||
  @Nonnull
 | 
			
		||||
  private List<Urn> getUrnsToFilterOn(
 | 
			
		||||
      @Nonnull final List<Urn> assetUrns, @Nullable final List<FacetFilterInput> filters) {
 | 
			
		||||
    // All logic related to output ports is removed.
 | 
			
		||||
    // We simply return the original assetUrns list.
 | 
			
		||||
    return assetUrns;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,94 @@
 | 
			
		||||
package com.linkedin.datahub.graphql.resolvers.application;
 | 
			
		||||
 | 
			
		||||
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
 | 
			
		||||
 | 
			
		||||
import com.datahub.authentication.Authentication;
 | 
			
		||||
import com.datahub.authorization.ConjunctivePrivilegeGroup;
 | 
			
		||||
import com.datahub.authorization.DisjunctivePrivilegeGroup;
 | 
			
		||||
import com.google.common.collect.ImmutableList;
 | 
			
		||||
import com.linkedin.common.urn.Urn;
 | 
			
		||||
import com.linkedin.common.urn.UrnUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.QueryContext;
 | 
			
		||||
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.exception.AuthorizationException;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.Application;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.UpdateApplicationInput;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.application.mappers.ApplicationMapper;
 | 
			
		||||
import com.linkedin.domain.Domains;
 | 
			
		||||
import com.linkedin.entity.EntityResponse;
 | 
			
		||||
import com.linkedin.metadata.authorization.PoliciesConfig;
 | 
			
		||||
import com.linkedin.metadata.service.ApplicationService;
 | 
			
		||||
import graphql.schema.DataFetcher;
 | 
			
		||||
import graphql.schema.DataFetchingEnvironment;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
@RequiredArgsConstructor
 | 
			
		||||
public class UpdateApplicationResolver implements DataFetcher<CompletableFuture<Application>> {
 | 
			
		||||
 | 
			
		||||
  private final ApplicationService _applicationService;
 | 
			
		||||
 | 
			
		||||
  private static final ConjunctivePrivilegeGroup ALL_PRIVILEGES_GROUP =
 | 
			
		||||
      new ConjunctivePrivilegeGroup(
 | 
			
		||||
          ImmutableList.of(PoliciesConfig.EDIT_ENTITY_PRIVILEGE.getType()));
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public CompletableFuture<Application> get(final DataFetchingEnvironment environment)
 | 
			
		||||
      throws Exception {
 | 
			
		||||
 | 
			
		||||
    final QueryContext context = environment.getContext();
 | 
			
		||||
    final UpdateApplicationInput input =
 | 
			
		||||
        bindArgument(environment.getArgument("input"), UpdateApplicationInput.class);
 | 
			
		||||
    final Urn applicationUrn = UrnUtils.getUrn(environment.getArgument("urn"));
 | 
			
		||||
    final Authentication authentication = context.getAuthentication();
 | 
			
		||||
 | 
			
		||||
    return GraphQLConcurrencyUtils.supplyAsync(
 | 
			
		||||
        () -> {
 | 
			
		||||
          if (!_applicationService.verifyEntityExists(
 | 
			
		||||
              context.getOperationContext(), applicationUrn)) {
 | 
			
		||||
            throw new IllegalArgumentException("The Application provided dos not exist");
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          Domains domains =
 | 
			
		||||
              _applicationService.getApplicationDomains(
 | 
			
		||||
                  context.getOperationContext(), applicationUrn);
 | 
			
		||||
          if (domains != null && domains.hasDomains() && domains.getDomains().size() > 0) {
 | 
			
		||||
            // get first domain since we only allow one domain right now
 | 
			
		||||
            Urn domainUrn = UrnUtils.getUrn(domains.getDomains().get(0).toString());
 | 
			
		||||
            final DisjunctivePrivilegeGroup orPrivilegeGroup =
 | 
			
		||||
                new DisjunctivePrivilegeGroup(ImmutableList.of(ALL_PRIVILEGES_GROUP));
 | 
			
		||||
            if (!AuthorizationUtils.isAuthorized(
 | 
			
		||||
                context, domainUrn.getEntityType(), domainUrn.toString(), orPrivilegeGroup)) {
 | 
			
		||||
              throw new AuthorizationException(
 | 
			
		||||
                  "Unauthorized to perform this action. Please contact your DataHub administrator.");
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          try {
 | 
			
		||||
            final Urn urn =
 | 
			
		||||
                _applicationService.updateApplication(
 | 
			
		||||
                    context.getOperationContext(),
 | 
			
		||||
                    applicationUrn,
 | 
			
		||||
                    input.getName(),
 | 
			
		||||
                    input.getDescription());
 | 
			
		||||
            EntityResponse response =
 | 
			
		||||
                _applicationService.getApplicationEntityResponse(
 | 
			
		||||
                    context.getOperationContext(), urn);
 | 
			
		||||
            if (response != null) {
 | 
			
		||||
              return ApplicationMapper.map(context, response);
 | 
			
		||||
            }
 | 
			
		||||
            // should never happen
 | 
			
		||||
            log.error(String.format("Unable to find application with urn %s", applicationUrn));
 | 
			
		||||
            return null;
 | 
			
		||||
          } catch (Exception e) {
 | 
			
		||||
            throw new RuntimeException(
 | 
			
		||||
                String.format("Failed to update Application with urn %s", applicationUrn), e);
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        this.getClass().getSimpleName(),
 | 
			
		||||
        "get");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -5,6 +5,7 @@ import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.*;
 | 
			
		||||
import com.datahub.authorization.ConjunctivePrivilegeGroup;
 | 
			
		||||
import com.datahub.authorization.DisjunctivePrivilegeGroup;
 | 
			
		||||
import com.google.common.collect.ImmutableList;
 | 
			
		||||
import com.linkedin.application.ApplicationProperties;
 | 
			
		||||
import com.linkedin.businessattribute.BusinessAttributeInfo;
 | 
			
		||||
import com.linkedin.common.urn.Urn;
 | 
			
		||||
import com.linkedin.container.EditableContainerProperties;
 | 
			
		||||
@ -534,6 +535,32 @@ public class DescriptionUtils {
 | 
			
		||||
        entityService);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public static void updateApplicationDescription(
 | 
			
		||||
      @Nonnull OperationContext opContext,
 | 
			
		||||
      String newDescription,
 | 
			
		||||
      Urn resourceUrn,
 | 
			
		||||
      Urn actor,
 | 
			
		||||
      EntityService<?> entityService) {
 | 
			
		||||
    ApplicationProperties applicationProperties =
 | 
			
		||||
        (ApplicationProperties)
 | 
			
		||||
            EntityUtils.getAspectFromEntity(
 | 
			
		||||
                opContext,
 | 
			
		||||
                resourceUrn.toString(),
 | 
			
		||||
                Constants.APPLICATION_PROPERTIES_ASPECT_NAME,
 | 
			
		||||
                entityService,
 | 
			
		||||
                new ApplicationProperties());
 | 
			
		||||
    if (applicationProperties != null) {
 | 
			
		||||
      applicationProperties.setDescription(newDescription);
 | 
			
		||||
    }
 | 
			
		||||
    persistAspect(
 | 
			
		||||
        opContext,
 | 
			
		||||
        resourceUrn,
 | 
			
		||||
        Constants.APPLICATION_PROPERTIES_ASPECT_NAME,
 | 
			
		||||
        applicationProperties,
 | 
			
		||||
        actor,
 | 
			
		||||
        entityService);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public static void updateBusinessAttributeDescription(
 | 
			
		||||
      @Nonnull OperationContext opContext,
 | 
			
		||||
      String newDescription,
 | 
			
		||||
 | 
			
		||||
@ -66,6 +66,8 @@ public class UpdateDescriptionResolver implements DataFetcher<CompletableFuture<
 | 
			
		||||
        return updateDataProductDescription(targetUrn, input, environment.getContext());
 | 
			
		||||
      case Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME:
 | 
			
		||||
        return updateBusinessAttributeDescription(targetUrn, input, environment.getContext());
 | 
			
		||||
      case Constants.APPLICATION_ENTITY_NAME:
 | 
			
		||||
        return updateApplicationDescription(targetUrn, input, environment.getContext());
 | 
			
		||||
      default:
 | 
			
		||||
        throw new RuntimeException(
 | 
			
		||||
            String.format(
 | 
			
		||||
@ -552,6 +554,37 @@ public class UpdateDescriptionResolver implements DataFetcher<CompletableFuture<
 | 
			
		||||
        "updateDataProductDescription");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private CompletableFuture<Boolean> updateApplicationDescription(
 | 
			
		||||
      Urn targetUrn, DescriptionUpdateInput input, QueryContext context) {
 | 
			
		||||
    return GraphQLConcurrencyUtils.supplyAsync(
 | 
			
		||||
        () -> {
 | 
			
		||||
          if (!DescriptionUtils.isAuthorizedToUpdateDescription(context, targetUrn)) {
 | 
			
		||||
            throw new AuthorizationException(
 | 
			
		||||
                "Unauthorized to perform this action. Please contact your DataHub administrator.");
 | 
			
		||||
          }
 | 
			
		||||
          DescriptionUtils.validateLabelInput(
 | 
			
		||||
              context.getOperationContext(), targetUrn, _entityService);
 | 
			
		||||
 | 
			
		||||
          try {
 | 
			
		||||
            Urn actor = CorpuserUrn.createFromString(context.getActorUrn());
 | 
			
		||||
            DescriptionUtils.updateApplicationDescription(
 | 
			
		||||
                context.getOperationContext(),
 | 
			
		||||
                input.getDescription(),
 | 
			
		||||
                targetUrn,
 | 
			
		||||
                actor,
 | 
			
		||||
                _entityService);
 | 
			
		||||
            return true;
 | 
			
		||||
          } catch (Exception e) {
 | 
			
		||||
            log.error(
 | 
			
		||||
                "Failed to perform update against input {}, {}", input.toString(), e.getMessage());
 | 
			
		||||
            throw new RuntimeException(
 | 
			
		||||
                String.format("Failed to perform update against input %s", input.toString()), e);
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        this.getClass().getSimpleName(),
 | 
			
		||||
        "updateApplicationDescription");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private CompletableFuture<Boolean> updateBusinessAttributeDescription(
 | 
			
		||||
      Urn targetUrn, DescriptionUpdateInput input, QueryContext context) {
 | 
			
		||||
    return GraphQLConcurrencyUtils.supplyAsync(
 | 
			
		||||
 | 
			
		||||
@ -91,7 +91,8 @@ public class SearchUtils {
 | 
			
		||||
          EntityType.DATA_PRODUCT,
 | 
			
		||||
          EntityType.NOTEBOOK,
 | 
			
		||||
          EntityType.BUSINESS_ATTRIBUTE,
 | 
			
		||||
          EntityType.SCHEMA_FIELD);
 | 
			
		||||
          EntityType.SCHEMA_FIELD,
 | 
			
		||||
          EntityType.APPLICATION);
 | 
			
		||||
 | 
			
		||||
  /** Entities that are part of autocomplete by default in Auto Complete Across Entities */
 | 
			
		||||
  public static final List<EntityType> AUTO_COMPLETE_ENTITY_TYPES =
 | 
			
		||||
@ -112,7 +113,8 @@ public class SearchUtils {
 | 
			
		||||
          EntityType.NOTEBOOK,
 | 
			
		||||
          EntityType.DATA_PRODUCT,
 | 
			
		||||
          EntityType.DOMAIN,
 | 
			
		||||
          EntityType.BUSINESS_ATTRIBUTE);
 | 
			
		||||
          EntityType.BUSINESS_ATTRIBUTE,
 | 
			
		||||
          EntityType.APPLICATION);
 | 
			
		||||
 | 
			
		||||
  /** Entities that are part of browse by default */
 | 
			
		||||
  public static final List<EntityType> BROWSE_ENTITY_TYPES =
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,131 @@
 | 
			
		||||
package com.linkedin.datahub.graphql.types.application;
 | 
			
		||||
 | 
			
		||||
import static com.linkedin.metadata.Constants.APPLICATION_ENTITY_NAME;
 | 
			
		||||
import static com.linkedin.metadata.Constants.APPLICATION_PROPERTIES_ASPECT_NAME;
 | 
			
		||||
import static com.linkedin.metadata.Constants.DOMAINS_ASPECT_NAME;
 | 
			
		||||
import static com.linkedin.metadata.Constants.FORMS_ASPECT_NAME;
 | 
			
		||||
import static com.linkedin.metadata.Constants.GLOBAL_TAGS_ASPECT_NAME;
 | 
			
		||||
import static com.linkedin.metadata.Constants.GLOSSARY_TERMS_ASPECT_NAME;
 | 
			
		||||
import static com.linkedin.metadata.Constants.INSTITUTIONAL_MEMORY_ASPECT_NAME;
 | 
			
		||||
import static com.linkedin.metadata.Constants.OWNERSHIP_ASPECT_NAME;
 | 
			
		||||
import static com.linkedin.metadata.Constants.STRUCTURED_PROPERTIES_ASPECT_NAME;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.ImmutableSet;
 | 
			
		||||
import com.linkedin.common.urn.Urn;
 | 
			
		||||
import com.linkedin.common.urn.UrnUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.QueryContext;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.Application;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.AutoCompleteResults;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.Entity;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.EntityType;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.FacetFilterInput;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.SearchResults;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.SearchableEntityType;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.application.mappers.ApplicationMapper;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.mappers.AutoCompleteResultsMapper;
 | 
			
		||||
import com.linkedin.entity.EntityResponse;
 | 
			
		||||
import com.linkedin.entity.client.EntityClient;
 | 
			
		||||
import com.linkedin.metadata.query.AutoCompleteResult;
 | 
			
		||||
import com.linkedin.metadata.query.filter.Filter;
 | 
			
		||||
import graphql.execution.DataFetcherResult;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.HashSet;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import java.util.function.Function;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import org.apache.commons.lang3.NotImplementedException;
 | 
			
		||||
 | 
			
		||||
@RequiredArgsConstructor
 | 
			
		||||
public class ApplicationType
 | 
			
		||||
    implements SearchableEntityType<Application, String>,
 | 
			
		||||
        com.linkedin.datahub.graphql.types.EntityType<Application, String> {
 | 
			
		||||
  public static final Set<String> ASPECTS_TO_FETCH =
 | 
			
		||||
      ImmutableSet.of(
 | 
			
		||||
          APPLICATION_PROPERTIES_ASPECT_NAME,
 | 
			
		||||
          OWNERSHIP_ASPECT_NAME,
 | 
			
		||||
          GLOBAL_TAGS_ASPECT_NAME,
 | 
			
		||||
          GLOSSARY_TERMS_ASPECT_NAME,
 | 
			
		||||
          DOMAINS_ASPECT_NAME,
 | 
			
		||||
          INSTITUTIONAL_MEMORY_ASPECT_NAME,
 | 
			
		||||
          STRUCTURED_PROPERTIES_ASPECT_NAME,
 | 
			
		||||
          FORMS_ASPECT_NAME);
 | 
			
		||||
  private final EntityClient _entityClient;
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public EntityType type() {
 | 
			
		||||
    return EntityType.APPLICATION;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public Function<Entity, String> getKeyProvider() {
 | 
			
		||||
    return Entity::getUrn;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public Class<Application> objectClass() {
 | 
			
		||||
    return Application.class;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public List<DataFetcherResult<Application>> batchLoad(
 | 
			
		||||
      @Nonnull List<String> urns, @Nonnull QueryContext context) throws Exception {
 | 
			
		||||
    final List<Urn> applicationUrns =
 | 
			
		||||
        urns.stream().map(UrnUtils::getUrn).collect(Collectors.toList());
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      final Map<Urn, EntityResponse> entities =
 | 
			
		||||
          _entityClient.batchGetV2(
 | 
			
		||||
              context.getOperationContext(),
 | 
			
		||||
              APPLICATION_ENTITY_NAME,
 | 
			
		||||
              new HashSet<>(applicationUrns),
 | 
			
		||||
              ASPECTS_TO_FETCH);
 | 
			
		||||
 | 
			
		||||
      final List<EntityResponse> gmsResults = new ArrayList<>(urns.size());
 | 
			
		||||
      for (Urn urn : applicationUrns) {
 | 
			
		||||
        gmsResults.add(entities.getOrDefault(urn, null));
 | 
			
		||||
      }
 | 
			
		||||
      return gmsResults.stream()
 | 
			
		||||
          .map(
 | 
			
		||||
              gmsResult ->
 | 
			
		||||
                  gmsResult == null
 | 
			
		||||
                      ? null
 | 
			
		||||
                      : DataFetcherResult.<Application>newResult()
 | 
			
		||||
                          .data(ApplicationMapper.map(context, gmsResult))
 | 
			
		||||
                          .build())
 | 
			
		||||
          .collect(Collectors.toList());
 | 
			
		||||
    } catch (Exception e) {
 | 
			
		||||
      throw new RuntimeException("Failed to batch load Applications", e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public AutoCompleteResults autoComplete(
 | 
			
		||||
      @Nonnull String query,
 | 
			
		||||
      @Nullable String field,
 | 
			
		||||
      @Nullable Filter filters,
 | 
			
		||||
      int limit,
 | 
			
		||||
      @Nonnull final QueryContext context)
 | 
			
		||||
      throws Exception {
 | 
			
		||||
    final AutoCompleteResult result =
 | 
			
		||||
        _entityClient.autoComplete(
 | 
			
		||||
            context.getOperationContext(), APPLICATION_ENTITY_NAME, query, filters, limit);
 | 
			
		||||
    return AutoCompleteResultsMapper.map(context, result);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public SearchResults search(
 | 
			
		||||
      @Nonnull String query,
 | 
			
		||||
      @Nullable List<FacetFilterInput> filters,
 | 
			
		||||
      int start,
 | 
			
		||||
      int count,
 | 
			
		||||
      @Nonnull final QueryContext context)
 | 
			
		||||
      throws Exception {
 | 
			
		||||
    throw new NotImplementedException(
 | 
			
		||||
        "Searchable type (deprecated) not implemented on Application entity type");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,133 @@
 | 
			
		||||
package com.linkedin.datahub.graphql.types.application.mappers;
 | 
			
		||||
 | 
			
		||||
import static com.linkedin.datahub.graphql.authorization.AuthorizationUtils.canView;
 | 
			
		||||
import static com.linkedin.metadata.Constants.APPLICATION_PROPERTIES_ASPECT_NAME;
 | 
			
		||||
import static com.linkedin.metadata.Constants.DOMAINS_ASPECT_NAME;
 | 
			
		||||
import static com.linkedin.metadata.Constants.FORMS_ASPECT_NAME;
 | 
			
		||||
import static com.linkedin.metadata.Constants.GLOBAL_TAGS_ASPECT_NAME;
 | 
			
		||||
import static com.linkedin.metadata.Constants.GLOSSARY_TERMS_ASPECT_NAME;
 | 
			
		||||
import static com.linkedin.metadata.Constants.INSTITUTIONAL_MEMORY_ASPECT_NAME;
 | 
			
		||||
import static com.linkedin.metadata.Constants.OWNERSHIP_ASPECT_NAME;
 | 
			
		||||
import static com.linkedin.metadata.Constants.STRUCTURED_PROPERTIES_ASPECT_NAME;
 | 
			
		||||
 | 
			
		||||
import com.linkedin.application.ApplicationProperties;
 | 
			
		||||
import com.linkedin.common.Forms;
 | 
			
		||||
import com.linkedin.common.GlobalTags;
 | 
			
		||||
import com.linkedin.common.GlossaryTerms;
 | 
			
		||||
import com.linkedin.common.InstitutionalMemory;
 | 
			
		||||
import com.linkedin.common.Ownership;
 | 
			
		||||
import com.linkedin.common.urn.Urn;
 | 
			
		||||
import com.linkedin.common.urn.UrnUtils;
 | 
			
		||||
import com.linkedin.data.DataMap;
 | 
			
		||||
import com.linkedin.datahub.graphql.QueryContext;
 | 
			
		||||
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.Application;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.EntityType;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.common.mappers.CustomPropertiesMapper;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.common.mappers.InstitutionalMemoryMapper;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.common.mappers.OwnershipMapper;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.common.mappers.util.MappingHelper;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.domain.DomainAssociationMapper;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.form.FormsMapper;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.glossary.mappers.GlossaryTermsMapper;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.mappers.ModelMapper;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.structuredproperty.StructuredPropertiesMapper;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.tag.mappers.GlobalTagsMapper;
 | 
			
		||||
import com.linkedin.domain.Domains;
 | 
			
		||||
import com.linkedin.entity.EntityResponse;
 | 
			
		||||
import com.linkedin.entity.EnvelopedAspectMap;
 | 
			
		||||
import com.linkedin.structured.StructuredProperties;
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
public class ApplicationMapper implements ModelMapper<EntityResponse, Application> {
 | 
			
		||||
 | 
			
		||||
  public static final ApplicationMapper INSTANCE = new ApplicationMapper();
 | 
			
		||||
 | 
			
		||||
  public static Application map(
 | 
			
		||||
      @Nullable final QueryContext context, @Nonnull final EntityResponse entityResponse) {
 | 
			
		||||
    return INSTANCE.apply(context, entityResponse);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public Application apply(
 | 
			
		||||
      @Nullable final QueryContext context, @Nonnull final EntityResponse entityResponse) {
 | 
			
		||||
    final Application result = new Application();
 | 
			
		||||
    Urn entityUrn = entityResponse.getUrn();
 | 
			
		||||
 | 
			
		||||
    result.setUrn(entityResponse.getUrn().toString());
 | 
			
		||||
    result.setType(EntityType.APPLICATION);
 | 
			
		||||
 | 
			
		||||
    EnvelopedAspectMap aspectMap = entityResponse.getAspects();
 | 
			
		||||
    MappingHelper<Application> mappingHelper = new MappingHelper<>(aspectMap, result);
 | 
			
		||||
    mappingHelper.mapToResult(
 | 
			
		||||
        APPLICATION_PROPERTIES_ASPECT_NAME,
 | 
			
		||||
        (application, dataMap) -> mapApplicationProperties(application, dataMap, entityUrn));
 | 
			
		||||
    mappingHelper.mapToResult(
 | 
			
		||||
        GLOBAL_TAGS_ASPECT_NAME,
 | 
			
		||||
        (application, dataMap) ->
 | 
			
		||||
            application.setTags(GlobalTagsMapper.map(context, new GlobalTags(dataMap), entityUrn)));
 | 
			
		||||
    mappingHelper.mapToResult(
 | 
			
		||||
        GLOSSARY_TERMS_ASPECT_NAME,
 | 
			
		||||
        (application, dataMap) ->
 | 
			
		||||
            application.setGlossaryTerms(
 | 
			
		||||
                GlossaryTermsMapper.map(context, new GlossaryTerms(dataMap), entityUrn)));
 | 
			
		||||
    mappingHelper.mapToResult(
 | 
			
		||||
        DOMAINS_ASPECT_NAME,
 | 
			
		||||
        (application, dataMap) ->
 | 
			
		||||
            application.setDomain(
 | 
			
		||||
                DomainAssociationMapper.map(context, new Domains(dataMap), application.getUrn())));
 | 
			
		||||
    mappingHelper.mapToResult(
 | 
			
		||||
        OWNERSHIP_ASPECT_NAME,
 | 
			
		||||
        (application, dataMap) ->
 | 
			
		||||
            application.setOwnership(
 | 
			
		||||
                OwnershipMapper.map(context, new Ownership(dataMap), entityUrn)));
 | 
			
		||||
    mappingHelper.mapToResult(
 | 
			
		||||
        INSTITUTIONAL_MEMORY_ASPECT_NAME,
 | 
			
		||||
        (application, dataMap) ->
 | 
			
		||||
            application.setInstitutionalMemory(
 | 
			
		||||
                InstitutionalMemoryMapper.map(
 | 
			
		||||
                    context, new InstitutionalMemory(dataMap), entityUrn)));
 | 
			
		||||
    mappingHelper.mapToResult(
 | 
			
		||||
        STRUCTURED_PROPERTIES_ASPECT_NAME,
 | 
			
		||||
        ((entity, dataMap) ->
 | 
			
		||||
            entity.setStructuredProperties(
 | 
			
		||||
                StructuredPropertiesMapper.map(
 | 
			
		||||
                    context, new StructuredProperties(dataMap), entityUrn))));
 | 
			
		||||
    mappingHelper.mapToResult(
 | 
			
		||||
        FORMS_ASPECT_NAME,
 | 
			
		||||
        ((entity, dataMap) ->
 | 
			
		||||
            entity.setForms(FormsMapper.map(new Forms(dataMap), entityUrn.toString()))));
 | 
			
		||||
 | 
			
		||||
    if (context != null && !canView(context.getOperationContext(), entityUrn)) {
 | 
			
		||||
      return AuthorizationUtils.restrictEntity(result, Application.class);
 | 
			
		||||
    } else {
 | 
			
		||||
      return result;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private void mapApplicationProperties(
 | 
			
		||||
      @Nonnull Application application, @Nonnull DataMap dataMap, @Nonnull Urn urn) {
 | 
			
		||||
    ApplicationProperties applicationProperties = new ApplicationProperties(dataMap);
 | 
			
		||||
    com.linkedin.datahub.graphql.generated.ApplicationProperties properties =
 | 
			
		||||
        new com.linkedin.datahub.graphql.generated.ApplicationProperties();
 | 
			
		||||
 | 
			
		||||
    final String name =
 | 
			
		||||
        applicationProperties.hasName() ? applicationProperties.getName() : urn.getId();
 | 
			
		||||
    properties.setName(name);
 | 
			
		||||
    properties.setDescription(applicationProperties.getDescription());
 | 
			
		||||
    if (applicationProperties.hasExternalUrl()) {
 | 
			
		||||
      properties.setExternalUrl(applicationProperties.getExternalUrl().toString());
 | 
			
		||||
    }
 | 
			
		||||
    if (applicationProperties.hasAssets()) {
 | 
			
		||||
      properties.setNumAssets(applicationProperties.getAssets().size());
 | 
			
		||||
    } else {
 | 
			
		||||
      properties.setNumAssets(0);
 | 
			
		||||
    }
 | 
			
		||||
    properties.setCustomProperties(
 | 
			
		||||
        CustomPropertiesMapper.map(
 | 
			
		||||
            applicationProperties.getCustomProperties(), UrnUtils.getUrn(application.getUrn())));
 | 
			
		||||
 | 
			
		||||
    application.setProperties(properties);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -4,6 +4,7 @@ import static com.linkedin.metadata.Constants.*;
 | 
			
		||||
 | 
			
		||||
import com.linkedin.common.urn.Urn;
 | 
			
		||||
import com.linkedin.datahub.graphql.QueryContext;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.Application;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.Assertion;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.BusinessAttribute;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.Chart;
 | 
			
		||||
@ -249,6 +250,11 @@ public class UrnToEntityMapper implements ModelMapper<com.linkedin.common.urn.Ur
 | 
			
		||||
      ((VersionSet) partialEntity).setUrn(input.toString());
 | 
			
		||||
      ((VersionSet) partialEntity).setType(EntityType.VERSION_SET);
 | 
			
		||||
    }
 | 
			
		||||
    if (input.getEntityType().equals(APPLICATION_ENTITY_NAME)) {
 | 
			
		||||
      partialEntity = new Application();
 | 
			
		||||
      ((Application) partialEntity).setUrn(input.toString());
 | 
			
		||||
      ((Application) partialEntity).setType(EntityType.APPLICATION);
 | 
			
		||||
    }
 | 
			
		||||
    return partialEntity;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -99,7 +99,7 @@ public class DataProductType
 | 
			
		||||
                          .build())
 | 
			
		||||
          .collect(Collectors.toList());
 | 
			
		||||
    } catch (Exception e) {
 | 
			
		||||
      throw new RuntimeException("Failed to batch load Queries", e);
 | 
			
		||||
      throw new RuntimeException("Failed to batch load Data Products", e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -59,6 +59,7 @@ public class EntityTypeMapper {
 | 
			
		||||
          .put(EntityType.RESTRICTED, Constants.RESTRICTED_ENTITY_NAME)
 | 
			
		||||
          .put(EntityType.BUSINESS_ATTRIBUTE, Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME)
 | 
			
		||||
          .put(EntityType.DATA_CONTRACT, Constants.DATA_CONTRACT_ENTITY_NAME)
 | 
			
		||||
          .put(EntityType.APPLICATION, Constants.APPLICATION_ENTITY_NAME)
 | 
			
		||||
          .build();
 | 
			
		||||
 | 
			
		||||
  private static final Map<String, EntityType> ENTITY_NAME_TO_TYPE =
 | 
			
		||||
 | 
			
		||||
@ -78,6 +78,7 @@ public class EntityTypeUrnMapper {
 | 
			
		||||
          .put(
 | 
			
		||||
              Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME,
 | 
			
		||||
              "urn:li:entityType:datahub.businessAttribute")
 | 
			
		||||
          .put(Constants.APPLICATION_ENTITY_NAME, "urn:li:entityType:datahub.application")
 | 
			
		||||
          .build();
 | 
			
		||||
 | 
			
		||||
  private static final Map<String, String> ENTITY_TYPE_URN_TO_NAME =
 | 
			
		||||
 | 
			
		||||
@ -275,6 +275,11 @@ type Query {
 | 
			
		||||
  Fetch a Data Process Instance by primary key (urn)
 | 
			
		||||
  """
 | 
			
		||||
  dataProcessInstance(urn: String!): DataProcessInstance
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  Fetch an Application by primary key (urn)
 | 
			
		||||
  """
 | 
			
		||||
  application(urn: String!): Application
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
@ -885,6 +890,37 @@ type Mutation {
 | 
			
		||||
    input: BatchSetDataProductInput!
 | 
			
		||||
  ): Boolean
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  Create a new Application
 | 
			
		||||
  """
 | 
			
		||||
  createApplication(
 | 
			
		||||
    "Inputs required to create a new Application."
 | 
			
		||||
    input: CreateApplicationInput!
 | 
			
		||||
  ): Application
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  Update a Application
 | 
			
		||||
  """
 | 
			
		||||
  updateApplication(
 | 
			
		||||
    "The urn identifier for the Application to update."
 | 
			
		||||
    urn: String!
 | 
			
		||||
    "Inputs required to create a new Application."
 | 
			
		||||
    input: UpdateApplicationInput!
 | 
			
		||||
  ): Application
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  Delete a Application by urn.
 | 
			
		||||
  """
 | 
			
		||||
  deleteApplication("Urn of the application to remove." urn: String!): Boolean
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  Batch set or unset a Application to a list of entities
 | 
			
		||||
  """
 | 
			
		||||
  batchSetApplication(
 | 
			
		||||
    "Input for batch setting application"
 | 
			
		||||
    input: BatchSetApplicationInput!
 | 
			
		||||
  ): Boolean
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  Create a Custom Ownership Type. This requires the 'Manage Ownership Types' Metadata Privilege.
 | 
			
		||||
  """
 | 
			
		||||
@ -1256,6 +1292,11 @@ enum EntityType {
 | 
			
		||||
  A set of versioned entities, representing a single source / logical entity over time
 | 
			
		||||
  """
 | 
			
		||||
  VERSION_SET
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  An application
 | 
			
		||||
  """
 | 
			
		||||
  APPLICATION
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
@ -12745,6 +12786,204 @@ input BatchSetDataProductInput {
 | 
			
		||||
  resourceUrns: [String!]!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
An Application, or a grouping of Entities for a single business purpose. Compared with Data Products, Applications represent a grouping of tables that exist to serve a specific
 | 
			
		||||
purpose. However, unlike Data Products, they don't represent groups that are tailored to be consumed for any particular purpose. Often, the assets in Applications power specific
 | 
			
		||||
outcomes, for example a Pricing Application.
 | 
			
		||||
"""
 | 
			
		||||
type Application implements Entity {
 | 
			
		||||
  """
 | 
			
		||||
  The primary key of the Application
 | 
			
		||||
  """
 | 
			
		||||
  urn: String!
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  A standard Entity Type
 | 
			
		||||
  """
 | 
			
		||||
  type: EntityType!
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  Properties about an Application
 | 
			
		||||
  """
 | 
			
		||||
  properties: ApplicationProperties
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  Ownership metadata of the Application
 | 
			
		||||
  """
 | 
			
		||||
  ownership: Ownership
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  References to internal resources related to the Application
 | 
			
		||||
  """
 | 
			
		||||
  institutionalMemory: InstitutionalMemory
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  Edges extending from this entity
 | 
			
		||||
  """
 | 
			
		||||
  relationships(input: RelationshipsInput!): EntityRelationshipsResult
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  Children entities inside of the Application
 | 
			
		||||
  """
 | 
			
		||||
  entities(input: SearchAcrossEntitiesInput): SearchResults
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  The structured glossary terms associated with the Application
 | 
			
		||||
  """
 | 
			
		||||
  glossaryTerms: GlossaryTerms
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  The Domain associated with the Application
 | 
			
		||||
  """
 | 
			
		||||
  domain: DomainAssociation
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  Tags used for searching Application
 | 
			
		||||
  """
 | 
			
		||||
  tags: GlobalTags
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  Experimental API.
 | 
			
		||||
  For fetching extra entities that do not have custom UI code yet
 | 
			
		||||
  """
 | 
			
		||||
  aspects(input: AspectParams): [RawAspect!]
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  Structured properties about this asset
 | 
			
		||||
  """
 | 
			
		||||
  structuredProperties: StructuredProperties
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  The forms associated with the Application
 | 
			
		||||
  """
 | 
			
		||||
  forms: Forms
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  Privileges given to a user relevant to this entity
 | 
			
		||||
  """
 | 
			
		||||
  privileges: EntityPrivileges
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Properties about an Application
 | 
			
		||||
"""
 | 
			
		||||
type ApplicationProperties {
 | 
			
		||||
  """
 | 
			
		||||
  Display name of the Application
 | 
			
		||||
  """
 | 
			
		||||
  name: String!
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  Description of the Application
 | 
			
		||||
  """
 | 
			
		||||
  description: String
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  External URL for the Appliation (most likely GitHub repo where Application may be managed as code)
 | 
			
		||||
  """
 | 
			
		||||
  externalUrl: String
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  Number of children entities inside of the Application. This number includes soft deleted entities.
 | 
			
		||||
  """
 | 
			
		||||
  numAssets: Int
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  Custom properties of the Application
 | 
			
		||||
  """
 | 
			
		||||
  customProperties: [CustomPropertiesEntry!]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Input required to fetch the entities inside of a Application.
 | 
			
		||||
"""
 | 
			
		||||
input ApplicationEntitiesInput {
 | 
			
		||||
  """
 | 
			
		||||
  Optional query filter for particular entities inside the Application
 | 
			
		||||
  """
 | 
			
		||||
  query: String
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  The offset of the result set
 | 
			
		||||
  """
 | 
			
		||||
  start: Int
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  The number of entities to include in result set
 | 
			
		||||
  """
 | 
			
		||||
  count: Int
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  Optional Facet filters to apply to the result set
 | 
			
		||||
  """
 | 
			
		||||
  filters: [FacetFilterInput!]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Input required for creating a Application.
 | 
			
		||||
"""
 | 
			
		||||
input CreateApplicationInput {
 | 
			
		||||
  """
 | 
			
		||||
  Properties about the Application
 | 
			
		||||
  """
 | 
			
		||||
  properties: CreateApplicationPropertiesInput!
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  The primary key of the Domain
 | 
			
		||||
  """
 | 
			
		||||
  domainUrn: String!
 | 
			
		||||
  """
 | 
			
		||||
  An optional id for the new application
 | 
			
		||||
  """
 | 
			
		||||
  id: String
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Input properties required for creating a Application
 | 
			
		||||
"""
 | 
			
		||||
input CreateApplicationPropertiesInput {
 | 
			
		||||
  """
 | 
			
		||||
  A display name for the Application
 | 
			
		||||
  """
 | 
			
		||||
  name: String!
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  An optional description for the Application
 | 
			
		||||
  """
 | 
			
		||||
  description: String
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Input properties required for update a Application
 | 
			
		||||
"""
 | 
			
		||||
input UpdateApplicationInput {
 | 
			
		||||
  """
 | 
			
		||||
  A display name for the Application
 | 
			
		||||
  """
 | 
			
		||||
  name: String
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  An optional description for the Application
 | 
			
		||||
  """
 | 
			
		||||
  description: String
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Input properties required for batch setting a Application on other entities
 | 
			
		||||
"""
 | 
			
		||||
input BatchSetApplicationInput {
 | 
			
		||||
  """
 | 
			
		||||
  The urn of the application you are setting on a group of resources.
 | 
			
		||||
  If this is null, the Application will be unset for the given resources.
 | 
			
		||||
  """
 | 
			
		||||
  applicationUrn: String
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  The urns of the entities the given application should be set on
 | 
			
		||||
  """
 | 
			
		||||
  resourceUrns: [String!]!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Properties about an individual Custom Ownership Type.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
@ -66,6 +66,14 @@ extend type Query {
 | 
			
		||||
    input: SearchAcrossEntitiesInput!
 | 
			
		||||
  ): SearchResults
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  List Application assets for a given urn
 | 
			
		||||
  """
 | 
			
		||||
  listApplicationAssets(
 | 
			
		||||
    urn: String!
 | 
			
		||||
    input: SearchAcrossEntitiesInput!
 | 
			
		||||
  ): SearchResults
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  Browse for different entities by getting organizational groups and their
 | 
			
		||||
  aggregated counts + content. Uses browsePathsV2 aspect and replaces our old
 | 
			
		||||
 | 
			
		||||
@ -98,6 +98,7 @@ public class Constants {
 | 
			
		||||
  public static final String DATAHUB_VIEW_ENTITY_NAME = "dataHubView";
 | 
			
		||||
  public static final String QUERY_ENTITY_NAME = "query";
 | 
			
		||||
  public static final String DATA_PRODUCT_ENTITY_NAME = "dataProduct";
 | 
			
		||||
  public static final String APPLICATION_ENTITY_NAME = "application";
 | 
			
		||||
  public static final String OWNERSHIP_TYPE_ENTITY_NAME = "ownershipType";
 | 
			
		||||
  public static final Urn DEFAULT_OWNERSHIP_TYPE_URN =
 | 
			
		||||
      UrnUtils.getUrn("urn:li:ownershipType:__system__none");
 | 
			
		||||
@ -370,6 +371,9 @@ public class Constants {
 | 
			
		||||
  public static final String DATA_PRODUCT_PROPERTIES_ASPECT_NAME = "dataProductProperties";
 | 
			
		||||
  public static final String DATA_PRODUCTS_ASPECT_NAME = "dataProducts";
 | 
			
		||||
 | 
			
		||||
  // Application
 | 
			
		||||
  public static final String APPLICATION_PROPERTIES_ASPECT_NAME = "applicationProperties";
 | 
			
		||||
 | 
			
		||||
  // Ownership Types
 | 
			
		||||
  public static final String OWNERSHIP_TYPE_KEY_ASPECT_NAME = "ownershipTypeKey";
 | 
			
		||||
  public static final String OWNERSHIP_TYPE_INFO_ASPECT_NAME = "ownershipTypeInfo";
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,8 @@
 | 
			
		||||
namespace com.linkedin.application
 | 
			
		||||
import com.linkedin.common.Edge
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents an association of assets to a Application.
 | 
			
		||||
 **/
 | 
			
		||||
record ApplicationAssociation includes Edge {}
 | 
			
		||||
@ -0,0 +1,14 @@
 | 
			
		||||
namespace com.linkedin.application
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Key for a Query
 | 
			
		||||
 */
 | 
			
		||||
@Aspect = {
 | 
			
		||||
  "name": "applicationKey"
 | 
			
		||||
}
 | 
			
		||||
record ApplicationKey {
 | 
			
		||||
  /**
 | 
			
		||||
  * A unique id for the Application.
 | 
			
		||||
  */
 | 
			
		||||
  id: string
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,42 @@
 | 
			
		||||
namespace com.linkedin.application
 | 
			
		||||
import com.linkedin.common.CustomProperties
 | 
			
		||||
import com.linkedin.common.ExternalReference
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The main properties of an Application
 | 
			
		||||
 */
 | 
			
		||||
@Aspect = {
 | 
			
		||||
  "name": "applicationProperties"
 | 
			
		||||
}
 | 
			
		||||
record ApplicationProperties includes CustomProperties, ExternalReference  {
 | 
			
		||||
  /**
 | 
			
		||||
   * Display name of the Application
 | 
			
		||||
   */
 | 
			
		||||
  @Searchable = {
 | 
			
		||||
    "fieldType": "WORD_GRAM",
 | 
			
		||||
    "enableAutocomplete": true,
 | 
			
		||||
    "boostScore": 10.0,
 | 
			
		||||
    "fieldNameAliases": [ "_entityName" ]
 | 
			
		||||
  }
 | 
			
		||||
  name: optional string
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Documentation of the application
 | 
			
		||||
   */
 | 
			
		||||
  @Searchable = {
 | 
			
		||||
    "fieldType": "TEXT",
 | 
			
		||||
    "hasValuesFieldName": "hasDescription"
 | 
			
		||||
  }
 | 
			
		||||
  description: optional string
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A list of assets that are part of this Application
 | 
			
		||||
   */
 | 
			
		||||
  @Relationship = {
 | 
			
		||||
    "/*/destinationUrn": {
 | 
			
		||||
      "name": "ApplicationContains",
 | 
			
		||||
      "entityTypes": [ "dataset", "dataJob", "dataFlow", "chart", "dashboard", "notebook", "container", "mlModel", "mlModelGroup", "mlFeatureTable", "mlFeature", "mlPrimaryKey" ],
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  assets: optional array[ApplicationAssociation]
 | 
			
		||||
}
 | 
			
		||||
@ -21,7 +21,7 @@ record DataProductProperties includes CustomProperties, ExternalReference  {
 | 
			
		||||
  name: optional string
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Documentation of the dataset
 | 
			
		||||
   * Documentation of the data product
 | 
			
		||||
   */
 | 
			
		||||
  @Searchable = {
 | 
			
		||||
    "fieldType": "TEXT",
 | 
			
		||||
 | 
			
		||||
@ -586,6 +586,21 @@ entities:
 | 
			
		||||
      - forms
 | 
			
		||||
      - testResults
 | 
			
		||||
      - subTypes
 | 
			
		||||
  - name: application
 | 
			
		||||
    category: core
 | 
			
		||||
    keyAspect: applicationKey
 | 
			
		||||
    aspects:
 | 
			
		||||
      - applicationProperties
 | 
			
		||||
      - ownership
 | 
			
		||||
      - glossaryTerms
 | 
			
		||||
      - globalTags
 | 
			
		||||
      - domains
 | 
			
		||||
      - institutionalMemory
 | 
			
		||||
      - status
 | 
			
		||||
      - structuredProperties
 | 
			
		||||
      - forms
 | 
			
		||||
      - testResults
 | 
			
		||||
      - subTypes
 | 
			
		||||
  - name: ownershipType
 | 
			
		||||
    doc: Ownership Type represents a user-created ownership category for a person or group who is responsible for an asset.
 | 
			
		||||
    category: core
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,27 @@
 | 
			
		||||
package com.linkedin.gms.factory.application;
 | 
			
		||||
 | 
			
		||||
import com.linkedin.entity.client.EntityClient;
 | 
			
		||||
import com.linkedin.metadata.graph.GraphClient;
 | 
			
		||||
import com.linkedin.metadata.service.ApplicationService;
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Autowired;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Qualifier;
 | 
			
		||||
import org.springframework.context.annotation.Bean;
 | 
			
		||||
import org.springframework.context.annotation.Configuration;
 | 
			
		||||
import org.springframework.context.annotation.Scope;
 | 
			
		||||
 | 
			
		||||
@Configuration
 | 
			
		||||
public class ApplicationServiceFactory {
 | 
			
		||||
 | 
			
		||||
  @Autowired
 | 
			
		||||
  @Qualifier("graphClient")
 | 
			
		||||
  private GraphClient _graphClient;
 | 
			
		||||
 | 
			
		||||
  @Bean(name = "applicationService")
 | 
			
		||||
  @Scope("singleton")
 | 
			
		||||
  @Nonnull
 | 
			
		||||
  protected ApplicationService getInstance(
 | 
			
		||||
      @Qualifier("entityClient") final EntityClient entityClient) throws Exception {
 | 
			
		||||
    return new ApplicationService(entityClient, _graphClient);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -33,6 +33,7 @@ import com.linkedin.metadata.graph.GraphService;
 | 
			
		||||
import com.linkedin.metadata.graph.SiblingGraphService;
 | 
			
		||||
import com.linkedin.metadata.models.registry.EntityRegistry;
 | 
			
		||||
import com.linkedin.metadata.recommendation.RecommendationsService;
 | 
			
		||||
import com.linkedin.metadata.service.ApplicationService;
 | 
			
		||||
import com.linkedin.metadata.service.AssertionService;
 | 
			
		||||
import com.linkedin.metadata.service.BusinessAttributeService;
 | 
			
		||||
import com.linkedin.metadata.service.DataProductService;
 | 
			
		||||
@ -179,6 +180,10 @@ public class GraphQLEngineFactory {
 | 
			
		||||
  @Qualifier("dataProductService")
 | 
			
		||||
  private DataProductService dataProductService;
 | 
			
		||||
 | 
			
		||||
  @Autowired
 | 
			
		||||
  @Qualifier("applicationService")
 | 
			
		||||
  private ApplicationService applicationService;
 | 
			
		||||
 | 
			
		||||
  @Autowired
 | 
			
		||||
  @Qualifier("formService")
 | 
			
		||||
  private FormService formService;
 | 
			
		||||
@ -253,6 +258,7 @@ public class GraphQLEngineFactory {
 | 
			
		||||
    args.setFormService(formService);
 | 
			
		||||
    args.setRestrictedService(restrictedService);
 | 
			
		||||
    args.setDataProductService(dataProductService);
 | 
			
		||||
    args.setApplicationService(applicationService);
 | 
			
		||||
    args.setGraphQLQueryComplexityLimit(
 | 
			
		||||
        configProvider.getGraphQL().getQuery().getComplexityLimit());
 | 
			
		||||
    args.setGraphQLQueryIntrospectionEnabled(
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,520 @@
 | 
			
		||||
package com.linkedin.metadata.service;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.ImmutableList;
 | 
			
		||||
import com.google.common.collect.ImmutableSet;
 | 
			
		||||
import com.linkedin.application.ApplicationAssociation;
 | 
			
		||||
import com.linkedin.application.ApplicationAssociationArray;
 | 
			
		||||
import com.linkedin.application.ApplicationKey;
 | 
			
		||||
import com.linkedin.application.ApplicationProperties;
 | 
			
		||||
import com.linkedin.common.EntityRelationships;
 | 
			
		||||
import com.linkedin.common.UrnArray;
 | 
			
		||||
import com.linkedin.common.urn.Urn;
 | 
			
		||||
import com.linkedin.common.urn.UrnUtils;
 | 
			
		||||
import com.linkedin.data.DataMap;
 | 
			
		||||
import com.linkedin.data.template.SetMode;
 | 
			
		||||
import com.linkedin.domain.Domains;
 | 
			
		||||
import com.linkedin.entity.EntityResponse;
 | 
			
		||||
import com.linkedin.entity.client.EntityClient;
 | 
			
		||||
import com.linkedin.metadata.Constants;
 | 
			
		||||
import com.linkedin.metadata.entity.AspectUtils;
 | 
			
		||||
import com.linkedin.metadata.graph.GraphClient;
 | 
			
		||||
import com.linkedin.metadata.query.filter.RelationshipDirection;
 | 
			
		||||
import com.linkedin.metadata.utils.EntityKeyUtils;
 | 
			
		||||
import com.linkedin.r2.RemoteInvocationException;
 | 
			
		||||
import io.datahubproject.metadata.context.OperationContext;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is used to permit easy CRUD operations on an Application
 | 
			
		||||
 *
 | 
			
		||||
 * <p>Note that no Authorization is performed within the service. The expectation is that the caller
 | 
			
		||||
 * has already verified the permissions of the active Actor.
 | 
			
		||||
 */
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class ApplicationService {
 | 
			
		||||
  private final EntityClient _entityClient;
 | 
			
		||||
  private final GraphClient
 | 
			
		||||
      _graphClient; // Keep for now, though direct relationship manipulation might change.
 | 
			
		||||
 | 
			
		||||
  public ApplicationService(@Nonnull EntityClient entityClient, @Nonnull GraphClient graphClient) {
 | 
			
		||||
    _entityClient = entityClient;
 | 
			
		||||
    _graphClient = graphClient;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a new Application.
 | 
			
		||||
   *
 | 
			
		||||
   * <p>Note that this method does not do authorization validation. It is assumed that users of this
 | 
			
		||||
   * class have already authorized the operation.
 | 
			
		||||
   *
 | 
			
		||||
   * @param name optional name of the Application
 | 
			
		||||
   * @param description optional description of the Application
 | 
			
		||||
   * @return the urn of the newly created Application
 | 
			
		||||
   */
 | 
			
		||||
  public Urn createApplication(
 | 
			
		||||
      @Nonnull OperationContext opContext,
 | 
			
		||||
      @Nullable String id,
 | 
			
		||||
      @Nullable String name,
 | 
			
		||||
      @Nullable String description) {
 | 
			
		||||
    final ApplicationKey key = new ApplicationKey();
 | 
			
		||||
    if (id != null && !id.isBlank()) {
 | 
			
		||||
      key.setId(id);
 | 
			
		||||
    } else {
 | 
			
		||||
      key.setId(UUID.randomUUID().toString());
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      if (_entityClient.exists(
 | 
			
		||||
          opContext,
 | 
			
		||||
          EntityKeyUtils.convertEntityKeyToUrn(key, Constants.APPLICATION_ENTITY_NAME))) {
 | 
			
		||||
        throw new IllegalArgumentException("This Application already exists!");
 | 
			
		||||
      }
 | 
			
		||||
    } catch (RemoteInvocationException e) {
 | 
			
		||||
      throw new RuntimeException("Unable to check for existence of Application!", e);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final ApplicationProperties properties = new ApplicationProperties();
 | 
			
		||||
    properties.setName(name, SetMode.IGNORE_NULL);
 | 
			
		||||
    properties.setDescription(description, SetMode.IGNORE_NULL);
 | 
			
		||||
    // Initialize assets array as per PDL: assets: optional array[ApplicationAssociation]
 | 
			
		||||
    properties.setAssets(new ApplicationAssociationArray());
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      final Urn entityUrn =
 | 
			
		||||
          EntityKeyUtils.convertEntityKeyToUrn(key, Constants.APPLICATION_ENTITY_NAME);
 | 
			
		||||
      return UrnUtils.getUrn(
 | 
			
		||||
          _entityClient.ingestProposal(
 | 
			
		||||
              opContext,
 | 
			
		||||
              AspectUtils.buildMetadataChangeProposal(
 | 
			
		||||
                  entityUrn, Constants.APPLICATION_PROPERTIES_ASPECT_NAME, properties),
 | 
			
		||||
              false));
 | 
			
		||||
    } catch (Exception e) {
 | 
			
		||||
      throw new RuntimeException("Failed to create Application", e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Updates an existing Application. If a provided field is null, the previous value will be kept.
 | 
			
		||||
   *
 | 
			
		||||
   * <p>Note that this method does not do authorization validation. It is assumed that users of this
 | 
			
		||||
   * class have already authorized the operation.
 | 
			
		||||
   *
 | 
			
		||||
   * @param urn the urn of the Application
 | 
			
		||||
   * @param name optional name of the Application
 | 
			
		||||
   * @param description optional description of the Application
 | 
			
		||||
   */
 | 
			
		||||
  public Urn updateApplication(
 | 
			
		||||
      @Nonnull OperationContext opContext,
 | 
			
		||||
      @Nonnull Urn urn,
 | 
			
		||||
      @Nullable String name,
 | 
			
		||||
      @Nullable String description) {
 | 
			
		||||
    Objects.requireNonNull(urn, "urn must not be null");
 | 
			
		||||
    Objects.requireNonNull(opContext.getSessionAuthentication(), "authentication must not be null");
 | 
			
		||||
 | 
			
		||||
    ApplicationProperties properties = getApplicationProperties(opContext, urn);
 | 
			
		||||
    if (properties == null) {
 | 
			
		||||
      throw new IllegalArgumentException(
 | 
			
		||||
          String.format(
 | 
			
		||||
              "Failed to update Application. Application with urn %s does not exist.", urn));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    boolean changed = false;
 | 
			
		||||
    if (name != null && !name.equals(properties.getName())) { // check if actually different
 | 
			
		||||
      properties.setName(name);
 | 
			
		||||
      changed = true;
 | 
			
		||||
    }
 | 
			
		||||
    if (description != null
 | 
			
		||||
        && !description.equals(properties.getDescription())) { // check if actually different
 | 
			
		||||
      properties.setDescription(description);
 | 
			
		||||
      changed = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!changed) {
 | 
			
		||||
      return urn;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      return UrnUtils.getUrn(
 | 
			
		||||
          _entityClient.ingestProposal(
 | 
			
		||||
              opContext,
 | 
			
		||||
              AspectUtils.buildMetadataChangeProposal(
 | 
			
		||||
                  urn, Constants.APPLICATION_PROPERTIES_ASPECT_NAME, properties),
 | 
			
		||||
              false));
 | 
			
		||||
    } catch (Exception e) {
 | 
			
		||||
      throw new RuntimeException(String.format("Failed to update Application with urn %s", urn), e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param applicationUrn the urn of the Application
 | 
			
		||||
   * @return an instance of {@link ApplicationProperties} for the Application, null if it does not
 | 
			
		||||
   *     exist.
 | 
			
		||||
   */
 | 
			
		||||
  @Nullable
 | 
			
		||||
  public ApplicationProperties getApplicationProperties(
 | 
			
		||||
      @Nonnull OperationContext opContext, @Nonnull final Urn applicationUrn) {
 | 
			
		||||
    Objects.requireNonNull(applicationUrn, "applicationUrn must not be null");
 | 
			
		||||
    Objects.requireNonNull(opContext.getSessionAuthentication(), "authentication must not be null");
 | 
			
		||||
    final EntityResponse response;
 | 
			
		||||
    try {
 | 
			
		||||
      response =
 | 
			
		||||
          _entityClient.getV2(
 | 
			
		||||
              opContext,
 | 
			
		||||
              Constants.APPLICATION_ENTITY_NAME,
 | 
			
		||||
              applicationUrn,
 | 
			
		||||
              ImmutableSet.of(Constants.APPLICATION_PROPERTIES_ASPECT_NAME));
 | 
			
		||||
    } catch (Exception e) {
 | 
			
		||||
      log.error(
 | 
			
		||||
          String.format("Failed to retrieve ApplicationProperties for urn %s", applicationUrn), e);
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    if (response != null
 | 
			
		||||
        && response.getAspects().containsKey(Constants.APPLICATION_PROPERTIES_ASPECT_NAME)) {
 | 
			
		||||
      return new ApplicationProperties(
 | 
			
		||||
          response
 | 
			
		||||
              .getAspects()
 | 
			
		||||
              .get(Constants.APPLICATION_PROPERTIES_ASPECT_NAME)
 | 
			
		||||
              .getValue()
 | 
			
		||||
              .data());
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param applicationUrn the urn of the Application
 | 
			
		||||
   * @return an instance of {@link ApplicationProperties} for the Application, null if it does not
 | 
			
		||||
   *     exist.
 | 
			
		||||
   */
 | 
			
		||||
  @Nullable
 | 
			
		||||
  public Domains getApplicationDomains(
 | 
			
		||||
      @Nonnull OperationContext opContext, @Nonnull final Urn applicationUrn) {
 | 
			
		||||
    Objects.requireNonNull(applicationUrn, "applicationUrn must not be null");
 | 
			
		||||
    Objects.requireNonNull(opContext.getSessionAuthentication(), "authentication must not be null");
 | 
			
		||||
    try {
 | 
			
		||||
      final EntityResponse response =
 | 
			
		||||
          _entityClient.getV2(
 | 
			
		||||
              opContext,
 | 
			
		||||
              Constants.APPLICATION_ENTITY_NAME,
 | 
			
		||||
              applicationUrn,
 | 
			
		||||
              ImmutableSet.of(Constants.DOMAINS_ASPECT_NAME));
 | 
			
		||||
      if (response != null && response.getAspects().containsKey(Constants.DOMAINS_ASPECT_NAME)) {
 | 
			
		||||
        return new Domains(
 | 
			
		||||
            response.getAspects().get(Constants.DOMAINS_ASPECT_NAME).getValue().data());
 | 
			
		||||
      }
 | 
			
		||||
    } catch (Exception e) {
 | 
			
		||||
      log.error(String.format("Failed to get domains for application %s", applicationUrn), e);
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Nullable
 | 
			
		||||
  public EntityResponse getApplicationEntityResponse(
 | 
			
		||||
      @Nonnull OperationContext opContext, @Nonnull final Urn applicationUrn) {
 | 
			
		||||
    Objects.requireNonNull(applicationUrn, "applicationUrn must not be null");
 | 
			
		||||
    Objects.requireNonNull(opContext.getSessionAuthentication(), "authentication must not be null");
 | 
			
		||||
    try {
 | 
			
		||||
      return _entityClient.getV2(
 | 
			
		||||
          opContext,
 | 
			
		||||
          Constants.APPLICATION_ENTITY_NAME,
 | 
			
		||||
          applicationUrn,
 | 
			
		||||
          ImmutableSet.of(Constants.APPLICATION_PROPERTIES_ASPECT_NAME));
 | 
			
		||||
    } catch (Exception e) {
 | 
			
		||||
      log.error(String.format("Failed to fetch Application with urn %s", applicationUrn), e);
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public void setDomain(
 | 
			
		||||
      @Nonnull OperationContext opContext,
 | 
			
		||||
      @Nonnull final Urn applicationUrn,
 | 
			
		||||
      @Nonnull final Urn domainUrn) {
 | 
			
		||||
    Objects.requireNonNull(applicationUrn, "applicationUrn must not be null");
 | 
			
		||||
    Objects.requireNonNull(domainUrn, "domainUrn must not be null");
 | 
			
		||||
    Objects.requireNonNull(opContext.getSessionAuthentication(), "authentication must not be null");
 | 
			
		||||
 | 
			
		||||
    Domains domains = getApplicationDomains(opContext, applicationUrn);
 | 
			
		||||
    if (domains == null) {
 | 
			
		||||
      domains = new Domains(new DataMap());
 | 
			
		||||
      domains.setDomains(new UrnArray());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    UrnArray domainList = domains.getDomains();
 | 
			
		||||
    if (domainList.stream().anyMatch(urn -> urn.equals(domainUrn))) {
 | 
			
		||||
      log.info(
 | 
			
		||||
          String.format(
 | 
			
		||||
              "Domain %s already exists for Application %s. Skipping add.",
 | 
			
		||||
              domainUrn, applicationUrn));
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    domains.getDomains().add(domainUrn);
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      _entityClient.ingestProposal(
 | 
			
		||||
          opContext,
 | 
			
		||||
          AspectUtils.buildMetadataChangeProposal(
 | 
			
		||||
              applicationUrn, Constants.DOMAINS_ASPECT_NAME, domains),
 | 
			
		||||
          false);
 | 
			
		||||
    } catch (Exception e) {
 | 
			
		||||
      throw new RuntimeException(
 | 
			
		||||
          String.format(
 | 
			
		||||
              "Failed to set domain for Application with urn %s domain %s",
 | 
			
		||||
              applicationUrn, domainUrn),
 | 
			
		||||
          e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public void deleteApplication(@Nonnull OperationContext opContext, @Nonnull Urn applicationUrn) {
 | 
			
		||||
    Objects.requireNonNull(applicationUrn, "applicationUrn must not be null");
 | 
			
		||||
    Objects.requireNonNull(opContext.getSessionAuthentication(), "authentication must not be null");
 | 
			
		||||
 | 
			
		||||
    // 1. Check properties-based assets
 | 
			
		||||
    ApplicationProperties properties = getApplicationProperties(opContext, applicationUrn);
 | 
			
		||||
    if (properties != null && properties.hasAssets() && !properties.getAssets().isEmpty()) {
 | 
			
		||||
      throw new RuntimeException(
 | 
			
		||||
          String.format(
 | 
			
		||||
              "Cannot delete application %s, it still contains assets (found in properties). Please remove assets before deleting.",
 | 
			
		||||
              applicationUrn));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 2. Check graph-based relationships for assets
 | 
			
		||||
    // Based on PDL: @Relationship = { "/*/destinationUrn": { "name": "ApplicationContains", ... } }
 | 
			
		||||
    // This means an outgoing relationship from Application to Asset.
 | 
			
		||||
    try {
 | 
			
		||||
      EntityRelationships graphRelationships =
 | 
			
		||||
          _graphClient.getRelatedEntities(
 | 
			
		||||
              applicationUrn.toString(),
 | 
			
		||||
              ImmutableSet.of(
 | 
			
		||||
                  "ApplicationContains"), // Relationship type - corrected to ImmutableSet
 | 
			
		||||
              RelationshipDirection.OUTGOING,
 | 
			
		||||
              0, // start
 | 
			
		||||
              1, // count (we only need to know if at least one exists)
 | 
			
		||||
              opContext.getAuthentication().getActor().toUrnStr());
 | 
			
		||||
      if (graphRelationships != null
 | 
			
		||||
          && graphRelationships.getRelationships() != null
 | 
			
		||||
          && !graphRelationships.getRelationships().isEmpty()) {
 | 
			
		||||
        throw new RuntimeException(
 | 
			
		||||
            String.format(
 | 
			
		||||
                "Cannot delete application %s, it still contains assets (found via graph relationships). Please remove assets before deleting.",
 | 
			
		||||
                applicationUrn));
 | 
			
		||||
      }
 | 
			
		||||
    } catch (Exception e) {
 | 
			
		||||
      // Log the exception, as failure to query graph shouldn't necessarily block deletion if
 | 
			
		||||
      // properties are clear,
 | 
			
		||||
      // but it's an indication of potential inconsistency or error.
 | 
			
		||||
      // Depending on strictness, this could also be a hard failure.
 | 
			
		||||
      log.error(
 | 
			
		||||
          String.format(
 | 
			
		||||
              "Error checking graph relationships for application %s during delete: %s",
 | 
			
		||||
              applicationUrn, e.getMessage()),
 | 
			
		||||
          e);
 | 
			
		||||
      // For now, let's make it a hard failure to ensure consistency, similar to
 | 
			
		||||
      // DataProductService's implicit check.
 | 
			
		||||
      throw new RuntimeException(
 | 
			
		||||
          String.format(
 | 
			
		||||
              "Failed to verify graph relationships for application %s during delete.",
 | 
			
		||||
              applicationUrn),
 | 
			
		||||
          e);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 3. Proceed with deletion if both checks pass
 | 
			
		||||
    try {
 | 
			
		||||
      _entityClient.deleteEntity(opContext, applicationUrn);
 | 
			
		||||
    } catch (Exception e) {
 | 
			
		||||
      throw new RuntimeException(
 | 
			
		||||
          String.format("Failed to delete Application with urn %s", applicationUrn), e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public void batchSetApplicationAssets(
 | 
			
		||||
      @Nonnull OperationContext opContext,
 | 
			
		||||
      @Nonnull Urn applicationUrn,
 | 
			
		||||
      @Nonnull List<Urn> resourceUrns,
 | 
			
		||||
      @Nonnull Urn actorUrn) { // actorUrn for audit stamp if not in opContext
 | 
			
		||||
    Objects.requireNonNull(applicationUrn, "applicationUrn must not be null");
 | 
			
		||||
    Objects.requireNonNull(resourceUrns, "resourceUrns must not be null");
 | 
			
		||||
 | 
			
		||||
    ApplicationProperties properties = getApplicationProperties(opContext, applicationUrn);
 | 
			
		||||
    if (properties == null) {
 | 
			
		||||
      log.error(
 | 
			
		||||
          "Application properties not found for {}, cannot add assets. Create the application first.",
 | 
			
		||||
          applicationUrn);
 | 
			
		||||
      throw new IllegalArgumentException(
 | 
			
		||||
          "Application properties not found for "
 | 
			
		||||
              + applicationUrn
 | 
			
		||||
              + ". Create the application first.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ApplicationAssociationArray currentAssets =
 | 
			
		||||
        properties.hasAssets() ? properties.getAssets() : new ApplicationAssociationArray();
 | 
			
		||||
    boolean changed = false;
 | 
			
		||||
 | 
			
		||||
    for (Urn resourceUrn : resourceUrns) {
 | 
			
		||||
      // TODO: Revisit ApplicationAssociation structure once PDL is finalized and classes generated.
 | 
			
		||||
      // Assuming ApplicationAssociation PDL will define how a resource URN is stored.
 | 
			
		||||
      // For now, we'll create a new association and add it. The exact mechanism
 | 
			
		||||
      // for checking existence and setting the resource URN might change based on PDL.
 | 
			
		||||
      boolean exists = false;
 | 
			
		||||
      for (ApplicationAssociation existing : currentAssets) {
 | 
			
		||||
        // TODO: Replace with actual check once ApplicationAssociation.getResource() is
 | 
			
		||||
        // confirmed/available
 | 
			
		||||
        // if (existing.getResource().equals(resourceUrn)) {
 | 
			
		||||
        //   exists = true;
 | 
			
		||||
        //   break;
 | 
			
		||||
        // }
 | 
			
		||||
      }
 | 
			
		||||
      if (!exists) {
 | 
			
		||||
        ApplicationAssociation newAssociation = new ApplicationAssociation();
 | 
			
		||||
        // TODO: newAssociation.setResource(resourceUrn); // This line caused errors, dependent on
 | 
			
		||||
        // PDL
 | 
			
		||||
        currentAssets.add(newAssociation);
 | 
			
		||||
        changed = true;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (changed) {
 | 
			
		||||
      properties.setAssets(currentAssets);
 | 
			
		||||
      // AuditStamp handling can be added here if ApplicationProperties has an audit field
 | 
			
		||||
      // Or if it's handled by the MCP interceptor automatically
 | 
			
		||||
      // final AuditStamp audit = new
 | 
			
		||||
      // AuditStamp().setActor(actorUrn).setTime(System.currentTimeMillis());
 | 
			
		||||
      // properties.setLastModified(audit); // Example if such a field exists
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        _entityClient.ingestProposal(
 | 
			
		||||
            opContext,
 | 
			
		||||
            AspectUtils.buildMetadataChangeProposal(
 | 
			
		||||
                applicationUrn, Constants.APPLICATION_PROPERTIES_ASPECT_NAME, properties),
 | 
			
		||||
            false);
 | 
			
		||||
      } catch (Exception e) {
 | 
			
		||||
        throw new RuntimeException(
 | 
			
		||||
            String.format(
 | 
			
		||||
                "Failed to batch set application assets for %s with resources %s!",
 | 
			
		||||
                applicationUrn, resourceUrns),
 | 
			
		||||
            e);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public void batchRemoveApplicationAssets(
 | 
			
		||||
      @Nonnull OperationContext opContext,
 | 
			
		||||
      @Nonnull Urn applicationUrn,
 | 
			
		||||
      @Nonnull List<Urn> resourceUrnsToRemove,
 | 
			
		||||
      @Nonnull Urn actorUrn) { // actorUrn for audit stamp
 | 
			
		||||
    Objects.requireNonNull(applicationUrn, "applicationUrn must not be null");
 | 
			
		||||
    Objects.requireNonNull(resourceUrnsToRemove, "resourceUrnsToRemove must not be null");
 | 
			
		||||
 | 
			
		||||
    ApplicationProperties properties = getApplicationProperties(opContext, applicationUrn);
 | 
			
		||||
    if (properties == null || !properties.hasAssets() || properties.getAssets().isEmpty()) {
 | 
			
		||||
      log.info(
 | 
			
		||||
          "Application {} has no assets or properties not found, nothing to remove.",
 | 
			
		||||
          applicationUrn);
 | 
			
		||||
      return; // No assets to remove
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ApplicationAssociationArray currentAssets = properties.getAssets();
 | 
			
		||||
    List<ApplicationAssociation> associationsToRemove = new ArrayList<>();
 | 
			
		||||
    for (ApplicationAssociation asset : currentAssets) {
 | 
			
		||||
      // TODO: Revisit ApplicationAssociation structure once PDL is finalized and classes generated.
 | 
			
		||||
      // Replace with actual check once ApplicationAssociation.getResource() is confirmed/available
 | 
			
		||||
      // if (resourceUrnsToRemove.contains(asset.getResource())) {
 | 
			
		||||
      //  associationsToRemove.add(asset);
 | 
			
		||||
      // }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!associationsToRemove.isEmpty()) {
 | 
			
		||||
      for (ApplicationAssociation toRemove : associationsToRemove) {
 | 
			
		||||
        currentAssets.remove(
 | 
			
		||||
            toRemove); // Relies on proper .equals() in ApplicationAssociation or object identity
 | 
			
		||||
      }
 | 
			
		||||
      properties.setAssets(currentAssets);
 | 
			
		||||
      // AuditStamp handling (similar to batchSetApplicationAssets)
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        _entityClient.ingestProposal(
 | 
			
		||||
            opContext,
 | 
			
		||||
            AspectUtils.buildMetadataChangeProposal(
 | 
			
		||||
                applicationUrn, Constants.APPLICATION_PROPERTIES_ASPECT_NAME, properties),
 | 
			
		||||
            false);
 | 
			
		||||
      } catch (Exception e) {
 | 
			
		||||
        throw new RuntimeException(
 | 
			
		||||
            String.format("Failed to batch remove assets from application %s", applicationUrn), e);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public void unsetApplication(
 | 
			
		||||
      @Nonnull OperationContext opContext, @Nonnull Urn resourceUrn, @Nonnull Urn actorUrn) {
 | 
			
		||||
    Objects.requireNonNull(resourceUrn, "resourceUrn must not be null");
 | 
			
		||||
    Objects.requireNonNull(actorUrn, "actorUrn must not be null");
 | 
			
		||||
    Objects.requireNonNull(opContext.getSessionAuthentication(), "authentication must not be null");
 | 
			
		||||
 | 
			
		||||
    // Find the parent Application URN from the resourceUrn by querying the graph
 | 
			
		||||
    // We are looking for an INCOMING "ApplicationContains" relationship to the resourceUrn.
 | 
			
		||||
    // The source of this relationship will be the Application.
 | 
			
		||||
    Urn applicationUrn = null;
 | 
			
		||||
    try {
 | 
			
		||||
      final EntityRelationships relationships =
 | 
			
		||||
          _graphClient.getRelatedEntities(
 | 
			
		||||
              resourceUrn.toString(),
 | 
			
		||||
              ImmutableSet.of("ApplicationContains"), // Relationship type
 | 
			
		||||
              RelationshipDirection
 | 
			
		||||
                  .INCOMING, // Direction: From Application TO Resource, so INCOMING for Resource
 | 
			
		||||
              0, // start
 | 
			
		||||
              1, // count (expecting only one Application to contain a given resource)
 | 
			
		||||
              opContext.getAuthentication().getActor().toUrnStr());
 | 
			
		||||
 | 
			
		||||
      if (relationships != null
 | 
			
		||||
          && relationships.getRelationships() != null
 | 
			
		||||
          && !relationships.getRelationships().isEmpty()) {
 | 
			
		||||
        if (relationships.getRelationships().size() > 1) {
 | 
			
		||||
          log.warn(
 | 
			
		||||
              "Resource {} is part of multiple applications via 'ApplicationContains' relationship. Proceeding with the first one found: {}. This might indicate a data modeling issue.",
 | 
			
		||||
              resourceUrn,
 | 
			
		||||
              relationships.getRelationships().get(0).getEntity());
 | 
			
		||||
        }
 | 
			
		||||
        applicationUrn = relationships.getRelationships().get(0).getEntity();
 | 
			
		||||
      } else {
 | 
			
		||||
        log.info(
 | 
			
		||||
            "Resource {} is not part of any application via 'ApplicationContains' relationship. Nothing to unset.",
 | 
			
		||||
            resourceUrn);
 | 
			
		||||
        return; // Nothing to do if the resource is not part of any application
 | 
			
		||||
      }
 | 
			
		||||
    } catch (Exception e) {
 | 
			
		||||
      log.error(
 | 
			
		||||
          String.format(
 | 
			
		||||
              "Failed to find parent application for resource %s. Error: %s",
 | 
			
		||||
              resourceUrn, e.getMessage()),
 | 
			
		||||
          e);
 | 
			
		||||
      throw new RuntimeException(
 | 
			
		||||
          String.format("Failed to find parent application for resource %s", resourceUrn), e);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (applicationUrn != null) {
 | 
			
		||||
      // Now that we have the application URN, call batchRemoveApplicationAssets
 | 
			
		||||
      batchRemoveApplicationAssets(
 | 
			
		||||
          opContext, applicationUrn, ImmutableList.of(resourceUrn), actorUrn);
 | 
			
		||||
      log.info(
 | 
			
		||||
          "Successfully unset application for resource {} from application {}",
 | 
			
		||||
          resourceUrn,
 | 
			
		||||
          applicationUrn);
 | 
			
		||||
    } else {
 | 
			
		||||
      // This case should ideally be caught by the graph query result, but as a safeguard:
 | 
			
		||||
      log.warn(
 | 
			
		||||
          "Could not determine application to unset for resource {}. No action taken.",
 | 
			
		||||
          resourceUrn);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public boolean verifyEntityExists(@Nonnull OperationContext opContext, @Nonnull Urn entityUrn) {
 | 
			
		||||
    try {
 | 
			
		||||
      return _entityClient.exists(opContext, entityUrn);
 | 
			
		||||
    } catch (RemoteInvocationException e) {
 | 
			
		||||
      throw new RuntimeException("Failed to check entity existence: " + e.getMessage(), e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -722,6 +722,26 @@ public class PoliciesConfig {
 | 
			
		||||
              CREATE_ENTITY_PRIVILEGE,
 | 
			
		||||
              EXISTS_ENTITY_PRIVILEGE));
 | 
			
		||||
 | 
			
		||||
  // Application Privileges
 | 
			
		||||
  public static final ResourcePrivileges APPLICATION_PRIVILEGES =
 | 
			
		||||
      ResourcePrivileges.of(
 | 
			
		||||
          "application",
 | 
			
		||||
          "Applications",
 | 
			
		||||
          "Applications created on DataHub",
 | 
			
		||||
          ImmutableList.of(
 | 
			
		||||
              VIEW_ENTITY_PAGE_PRIVILEGE,
 | 
			
		||||
              EDIT_ENTITY_OWNERS_PRIVILEGE,
 | 
			
		||||
              EDIT_ENTITY_DOCS_PRIVILEGE,
 | 
			
		||||
              EDIT_ENTITY_DOC_LINKS_PRIVILEGE,
 | 
			
		||||
              EDIT_ENTITY_PRIVILEGE,
 | 
			
		||||
              DELETE_ENTITY_PRIVILEGE,
 | 
			
		||||
              EDIT_ENTITY_TAGS_PRIVILEGE,
 | 
			
		||||
              EDIT_ENTITY_GLOSSARY_TERMS_PRIVILEGE,
 | 
			
		||||
              EDIT_ENTITY_DOMAINS_PRIVILEGE,
 | 
			
		||||
              EDIT_ENTITY_PROPERTIES_PRIVILEGE,
 | 
			
		||||
              CREATE_ENTITY_PRIVILEGE,
 | 
			
		||||
              EXISTS_ENTITY_PRIVILEGE));
 | 
			
		||||
 | 
			
		||||
  // Glossary Term Privileges
 | 
			
		||||
  public static final ResourcePrivileges GLOSSARY_TERM_PRIVILEGES =
 | 
			
		||||
      ResourcePrivileges.of(
 | 
			
		||||
@ -863,7 +883,8 @@ public class PoliciesConfig {
 | 
			
		||||
          BUSINESS_ATTRIBUTE_PRIVILEGES,
 | 
			
		||||
          STRUCTURED_PROPERTIES_PRIVILEGES,
 | 
			
		||||
          VERSION_SET_PRIVILEGES,
 | 
			
		||||
          PLATFORM_INSTANCE_PRIVILEGES);
 | 
			
		||||
          PLATFORM_INSTANCE_PRIVILEGES,
 | 
			
		||||
          APPLICATION_PRIVILEGES);
 | 
			
		||||
 | 
			
		||||
  // Merge all entity specific resource privileges to create a superset of all resource privileges
 | 
			
		||||
  public static final ResourcePrivileges ALL_RESOURCE_PRIVILEGES =
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user