Last updated Sep 25, 2023

Rate this page:

PAPI Client (Spring Boot)

The following is a simplified implementation of a PAPI client in Spring Boot intended to be used as a reference

Java version at the time of implementation: Java 17

The folder structure we have defined is as follows (most files omitted for readability):

1
2
├── src
│ ├── main
│ │ ├── java
│ │ │ ├── com.example.papiclientexamplejava
│ │ │ │ ├── client
│ │ │ │ ├── entities
│ │ │ │ │ ├── agg
│ │ │ │ │ │ ├── offering
│ │ │ │ │ │ │ ├── btf
│ │ │ │ │ │ │ ├── cloud
│ │ │ │ │ │ │ ├── filter
│ │ │ │ ├── utils
│ │ │ │ ├── ExampleClass.java
│ │ │ │ ├── ExampleService.java
│ │ │ │ ├── PapiClientExampleJavaApplication.java
│ │ ├── resources
│ │ │ ├── graphql
│ │ │ │ ├── queries
│ │ │ │ ├── schema
└── application.yml

Dependencies

Here are the following dependencies in pom.xml:

1
2
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-graphql</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webflux</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webflux</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.graphql</groupId>
        <artifactId>spring-graphql-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webflux</artifactId>
    </dependency>
</dependencies>

Schemas

Here are the schemas that are needed to make GraphQL calls to Atlassian GraphQL Gateway (AGG). These files go in src/main/resources/graphql/schemas:

queries.graphqls

1
2
type Query {
    partner: Partner
}

offering.graphqls

1
2
directive @oneOf on INPUT_OBJECT

type Partner {
    "Get all cloud and BTF product offerings and pricing information"
    offeringCatalog: PartnerOfferingListResponse

    "Get offering and pricing details for a given product, including related apps"
    offeringDetails(
        where: PartnerOfferingFilter
    ): PartnerOfferingDetailsResponse
}

"Search for available product offerings"
input PartnerOfferingFilter @oneOf {
    "Search cloud offerings by product key"
    cloudProduct: PartnerOfferingCloudInput
    "Search BTF offerings by product key"
    btfProduct: PartnerOfferingBtfInput
}

input PartnerOfferingCloudInput {
    "Unique identifier for a cloud product"
    id: ID!
    "Available currencies for a cloud product or app"
    currency: [PartnerCurrency]
    "Available license types for a cloud offering"
    pricingPlanType: [PartnerCloudLicenseType]
}

input PartnerOfferingBtfInput {
    "Unique identifier for a BTF product"
    productKey: ID!
    "Available currencies for a BTF product or app"
    currency: [PartnerCurrency]
    "Available license types for a BTF offering"
    licenseType: [PartnerBtfLicenseType]
}

interface PartnerCloudProductNode {
    id: ID!
    name: String
}

interface PartnerOfferingNode {
    id: ID!
    name: String
}

interface PartnerPricingPlanNode {
    id: ID!
    description: String
    currency: String
    type: String
}

interface PartnerBtfProductNode {
    productKey: ID!
    productDescription: String
}

interface PartnerOrderableItemNode {
    orderableItemId: ID!
    description: String
    licenseType: String
    currency: String
}

type PartnerOfferingDetailsResponse {
    cloudProducts: [PartnerCloudProduct]
    cloudApps: [PartnerCloudApp]

    btfProducts: [PartnerBtfProduct]
    btfApps: [PartnerBtfProduct]
}

type PartnerOfferingListResponse {
    cloudProducts: [PartnerCloudProductItem]
    btfProducts: [PartnerBtfProductItem]
}

type PartnerCloudProductItem implements PartnerCloudProductNode {
    id: ID!
    name: String
}

type PartnerCloudProduct implements PartnerCloudProductNode {
    id: ID!
    name: String
    chargeElements: [String]
    uncollectibleAction: PartnerUncollectibleAction
    offerings: [PartnerOfferingItem]
}

type PartnerOfferingItem implements PartnerOfferingNode {
    id: ID!
    name: String
    sku: String
    level: Int
    supportedBillingSystems: [String]
    hostingType: String
    pricingType: String
    billingType: String
    parent: String
    pricingPlans: [PartnerPricingPlan]
}

type PartnerCloudApp implements PartnerOfferingNode {
    id: ID!
    name: String
    sku: String
    level: Int
    supportedBillingSystems: [String]
    hostingType: String
    pricingType: String
    billingType: String
    parent: String
}

type PartnerPricingPlan implements PartnerPricingPlanNode {
    id: ID!
    description: String
    currency: String
    type: String
    sku: String
    primaryCycle: PartnerBillingCycle
    items: [PartnerPricingPlanItem]
}

type PartnerPricingPlanItem {
    tiersMode: String
    chargeElement: String
    chargeType: String
    cycle: PartnerBillingCycle
    tiers: [PartnerPricingTier]
}

type PartnerPricingTier {
    ceiling: Float
    amount: Float
    flatAmount: Float
    unitAmount: Float
    floor: Float
    policy: String
}

type PartnerUncollectibleAction {
    type: String
    destination: PartnerUncollectibleDestination
}

type PartnerUncollectibleDestination {
    offeringKey: ID!
}

type PartnerBillingCycle {
    name: String
    count: Int
    interval: String
}

type PartnerBtfProductItem implements PartnerBtfProductNode {
    productKey: ID!
    productDescription: String
}

type PartnerBtfProduct implements PartnerBtfProductNode {
    productKey: ID!
    productDescription: String
    productType: String
    discountOptOut: Boolean
    lastModified: String
    marketplaceAddon: Boolean
    contactSalesForAdditionalPricing: Boolean
    dataCenter: Boolean
    userCountEnforced: Boolean
    parentDescription: String
    parentKey: String
    billingType: String
    orderableItems: [PartnerOrderableItem]
    monthly: [PartnerBillingSpecificTier]
    annual: [PartnerBillingSpecificTier]
}

type PartnerOrderableItem implements PartnerOrderableItemNode {
    orderableItemId: ID!
    newPricingPlanItem: String
    description: String
    publiclyAvailable: Boolean
    saleType: String
    amount: Float
    renewalAmount: Float
    licenseType: String
    unitCount: Int
    monthsValid: Int
    editionDescription: String
    editionId: String
    editionType: String
    editionTypeIsDeprecated: Boolean
    unitLabel: String
    enterprise: Boolean
    starter: Boolean
    sku: String
    edition: String
    renewalFrequency: String
    currency: String
}

type PartnerBillingSpecificTier {
    unitStart: Int
    unitLimit: Int
    unitBlockSize: Int
    price: Float
    editionType: String
    entitionTypeIsDepercated: Boolean
    unitLabel: String
    currency: String
}

common.graphqls

1
2
enum PartnerCloudLicenseType {
    FREE
    COMMERCIAL
    ACADEMIC
    COMMUNITY
    OPEN_SOURCE
    EVALUATION
    STARTER
    DEVELOPER
    DEMONSTRATION
}

enum PartnerBtfLicenseType {
    COMMERCIAL
    ACADEMIC
    EVALUATION
    STARTER
}

enum PartnerCurrency {
    USD
    JPY
}

Queries

Here are the queries that we will use to make GraphQL calls to Atlassian GraphQL Gateway (AGG). These files go in src/main/resources/graphql/queries:

partnerOfferingCatalogQuery.graphql

1
2
query FetchOfferingCatalog {
    partner {
        offeringCatalog {
            btfProducts {
                productDescription
                productKey
            }
            cloudProducts {
                id
                name
            }
        }
    }
}

partnerOfferingDetailsQuery.graphql

1
2
query FetchOfferingDetails($where: PartnerOfferingFilter) {
    partner {
        offeringDetails(where: $where){
            cloudProducts {
                id
                name
                offerings {
                    billingType
                    hostingType
                    id
                    level
                    name
                    pricingType
                    sku
                    pricingPlans {
                        currency
                        description
                        items {
                            chargeElement
                            chargeType
                            cycle {
                                count
                                interval
                                name
                            }
                            tiers {
                                amount
                                ceiling
                                flatAmount
                                floor
                                policy
                                unitAmount
                            }
                            tiersMode
                        }
                        id
                        primaryCycle{
                            count
                            interval
                            name
                        }
                        sku
                        type
                    }
                }
            }
            cloudApps {
                billingType
                hostingType
                id
                level
                name
                parent
                pricingType
                sku
                supportedBillingSystems
            }
            btfProducts {
                billingType
                contactSalesForAdditionalPricing
                dataCenter
                discountOptOut
                lastModified
                marketplaceAddon
                parentKey
                parentDescription
                productDescription
                productKey
                userCountEnforced
                productType
                monthly {
                    currency
                    editionType
                    entitionTypeIsDepercated
                    price
                    unitBlockSize
                    unitLabel
                    unitLimit
                    unitStart
                }
                orderableItems {
                    amount
                    currency
                    description
                    edition
                    editionDescription
                    editionId
                    editionType
                    editionTypeIsDeprecated
                    enterprise
                    licenseType
                    monthsValid
                    newPricingPlanItem
                    orderableItemId
                    publiclyAvailable
                    renewalAmount
                    renewalFrequency
                    saleType
                    sku
                    starter
                    unitCount
                    unitLabel
                }
            }
            btfApps {
                billingType
                contactSalesForAdditionalPricing
                dataCenter
                discountOptOut
                lastModified
                marketplaceAddon
                parentDescription
                parentKey
                productDescription
                productKey
                productType
                userCountEnforced
                orderableItems {
                    amount
                    currency
                    description
                    edition
                    editionDescription
                    editionId
                    editionType
                    editionTypeIsDeprecated
                    enterprise
                    monthsValid
                    licenseType
                    newPricingPlanItem
                    orderableItemId
                    publiclyAvailable
                    renewalAmount
                    saleType
                    renewalFrequency
                    sku
                    starter
                    unitCount
                    unitLabel
                }
                monthly {
                    currency
                    editionType
                    entitionTypeIsDepercated
                    price
                    unitBlockSize
                    unitLabel
                    unitLimit
                    unitStart
                }
                annual {
                    currency
                    editionType
                    entitionTypeIsDepercated
                    price
                    unitBlockSize
                    unitLabel
                    unitLimit
                    unitStart
                }
            }
        }
    }
}

Classes

Here are some classes we will be needing

To make GraphQL calls to AGG we will send POST requests with this class as the body value. This file goes in src/main/java/com/example/papiclientexamplejava/entities/:

1
2
package com.example.papiclientexamplejava.entities;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class GraphQlRequestBody {

    private String query;
    private Object variables;
    private String operationName;

}

To read the queries inside of .graphql files we will need this class. This file goes in src/main/java/com/example/papiclientexamplejava/utils/:

1
2
package com.example.papiclientexamplejava.utils;

import java.io.IOException;

public final class GraphQlSchemaReaderUtil {

    public static String getSchemaFromFileName(final String filename) throws IOException {
        return new String(
                GraphQlSchemaReaderUtil.class.getClassLoader().getResourceAsStream(filename).readAllBytes()
        );
    }
}

POJOs

Here are the POJOs and some ancillary classes that translates the .graphqls files into Java classes

These files go inside of src/main/java/com/example/papiclientexamplejava/entities/:

1
2
package com.example.papiclientexamplejava.entities;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class GraphQlRequestBody {

    private String query;
    private Object variables;
    private String operationName;

}
1
2
package com.example.papiclientexamplejava.entities;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class WhereFilter <T>{

    T where;
}

These files go inside of src/main/java/com/example/papiclientexamplejava/entities/offering/:

1
2
package com.example.papiclientexamplejava.entities.agg.offering;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(value = JsonInclude.Include.NON_DEFAULT)
public class PartnerData {
    Partner partner;
}
1
2
package com.example.papiclientexamplejava.entities.agg.offering;

import com.example.papiclientexamplejava.entities.agg.offering.PartnerOfferingResponse;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(value = JsonInclude.Include.NON_DEFAULT)
public class Partner {
    PartnerOfferingResponse offeringCatalog;
    PartnerOfferingResponse offeringDetails;
}
1
2
package com.example.papiclientexamplejava.entities.agg.offering;

import com.example.papiclientexamplejava.entities.agg.offering.btf.PartnerBtfProduct;
import com.example.papiclientexamplejava.entities.agg.offering.cloud.PartnerCloudProduct;
import com.example.papiclientexamplejava.entities.agg.offering.cloud.PartnerOfferingItem;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

import java.util.List;

@Builder
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class PartnerOfferingResponse {

    List<PartnerCloudProduct> cloudProducts;
    List<PartnerOfferingItem> cloudApps;

    List<PartnerBtfProduct> btfProducts;
    List<PartnerBtfProduct> btfApps;
}

These files go inside of src/main/java/com/example/papiclientexamplejava/entities/offering/btf/:

1
2
package com.example.papiclientexamplejava.entities.agg.offering.btf;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Builder
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class PartnerBillingSpecificTier {

    Integer unitStart;
    Integer unitLimit;
    Integer unitBlockSize;
    Float price;
    String editionType;
    Boolean entitionTypeIsDepercated;
    String unitLabel;
    String currency;
}
1
2
package com.example.papiclientexamplejava.entities.agg.offering.btf;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

import java.util.List;

@Builder
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class PartnerBtfProduct {

    String productKey;
    String productDescription;
    String productType;
    Boolean discountOptOut;
    String lastModified;
    Boolean marketplaceAddon;
    Boolean contactSalesForAdditionalPricing;
    Boolean dataCenter;
    Boolean userCountEnforced;
    String parentDescription;
    String parentKey;
    String billingType;
    List<PartnerOrderableItem> orderableItems;
    List<PartnerBillingSpecificTier> monthly;
    List<PartnerBillingSpecificTier> annual;
}
1
2
package com.example.papiclientexamplejava.entities.agg.offering.btf;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Builder
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class PartnerOrderableItem {

    String orderableItemId;
    String newPricingPlanItem;
    String description;
    Boolean publiclyAvailable;
    String saleType;
    Float amount;
    Float renewalAmount;
    String licenseType;
    Integer unitCount;
    Integer monthsValid;
    String editionDescription;
    String editionId;
    String editionType;
    Boolean editionTypeIsDeprecated;
    String unitLabel;
    Boolean enterprise;
    Boolean starter;
    String sku;
    String edition;
    String renewalFrequency;
    String currency;
}

These files go inside of src/main/java/com/example/papiclientexamplejava/entities/offering/cloud/:

1
2
package com.example.papiclientexamplejava.entities.agg.offering.cloud;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Builder
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class PartnerBillingCycle {
    String name;
    Integer count;
    String interval;
}
1
2
package com.example.papiclientexamplejava.entities.agg.offering.cloud;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

import java.util.List;

@Builder
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class PartnerCloudProduct {

    String id;
    String name;
    List<String> chargeElements;
    PartnerUncollectibleAction uncollectibleAction;
    List<PartnerOfferingItem> offerings;
}
1
2
package com.example.papiclientexamplejava.entities.agg.offering.cloud;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

import java.util.List;

@Builder
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class PartnerOfferingItem {

    String id;
    String name;
    String sku;
    Integer level;
    List<String> supportedBillingSystems;
    String hostingType;
    String pricingType;
    String billingType;
    String parent;
    List<PartnerPricingPlan> pricingPlans; //added separately by searching defaultPricingPlans
}
1
2
package com.example.papiclientexamplejava.entities.agg.offering.cloud;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

import java.util.List;

@Builder
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class PartnerPricingPlan {

    String id;
    String description;
    String sku;
    String type;
    PartnerBillingCycle primaryCycle;
    String currency;
    List<PartnerPricingPlanItem> items;
}
1
2
package com.example.papiclientexamplejava.entities.agg.offering.cloud;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

import java.util.List;

@Builder
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class PartnerPricingPlanItem {

    String tiersMode;
    String chargeElement;
    String chargeType;
    PartnerBillingCycle cycle;
    List<PartnerPricingTier> tiers;
}
1
2
package com.example.papiclientexamplejava.entities.agg.offering.cloud;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Builder
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class PartnerPricingTier {

    Float ceiling;
    Float amount;
    Float flatAmount;
    Float unitAmount;
    Float floor;
    String policy;
}
1
2
package com.example.papiclientexamplejava.entities.agg.offering.cloud;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class PartnerUncollectibleAction {

    String type;
    PartnerUncollectibleDestination destination;
}
1
2
package com.example.papiclientexamplejava.entities.agg.offering.cloud;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class PartnerUncollectibleDestination {

    String offeringKey;
}

These files go inside of src/main/java/com/example/papiclientexamplejava/entities/offering/filter/:

1
2
package com.example.papiclientexamplejava.entities.agg.offering.filter;

import com.example.papiclientexamplejava.entities.agg.SchemaConstants;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.List;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class PartnerOfferingBtfInput {

    String productKey;
    List<SchemaConstants.Currency> currency;
    List<String> orderableItemId;
    List<SchemaConstants.BtfLicenseType> licenseType;
}
1
2
package com.example.papiclientexamplejava.entities.agg.offering.filter;

import com.example.papiclientexamplejava.entities.agg.SchemaConstants;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.List;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class PartnerOfferingCloudInput {

    String id;
    List<String> offeringKey;
    List<String> pricingPlanKey;
    List<SchemaConstants.Currency> currency;
    List<SchemaConstants.CloudLicenseType> pricingPlanType;
}
1
2
package com.example.papiclientexamplejava.entities.agg.offering.filter;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class PartnerOfferingFilter {

    PartnerOfferingCloudInput cloudProduct;
    PartnerOfferingBtfInput btfProduct;
}

Client

First, we need to set a container token value inside of application.yml. Next, we will create a class called WebClientBuilders inside of src/main/java/com.example.papiclientexamplejava/client/ that will help us use Spring WebClient. Finally, we will create a class called PapiClient inside of src/main/java/com.example.papiclientexamplejava/client/ which uses Spring WebClient to make GraphQL calls to AGG.

application.yml

1
2
container-token: "<insert container token here>"

WebClientBuilders.java

1
2
package com.example.papiclientexamplejava.client;

import io.netty.resolver.DefaultAddressResolverGroup;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;

@Slf4j
@Component
public class WebClientBuilders {

	// returns builder with no customization
	public WebClient.Builder getWebClientBuilder() {
		return WebClient
			.builder()
			.clientConnector(
				new ReactorClientHttpConnector(
					HttpClient.create().resolver(DefaultAddressResolverGroup.INSTANCE)
				)
			)
			.exchangeStrategies(
				ExchangeStrategies
					.builder()
					.codecs(configurer ->
						configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024)
					)
					.build()
			);
	}
}

PapiClient.java

1
2
package com.example.papiclientexamplejava.client;

import com.example.papiclientexamplejava.common.PropertyConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

@Component
public class PapiClient {
    private WebClient webClient;
    private String containerToken;

    @Autowired
    public PapiClient(WebClientBuilders webClientBuilders, Environment environment) {
        this.webClient =
                webClientBuilders
                        .getWebClientBuilder()
                        .baseUrl(environment.getProperty(PropertyConstants.SERVICES_AGG_URL))
                        .build();

        this.containerToken = environment.getProperty("container-token");
    }
}

Next, let's add a method in PapiClient that we'll use to make generic POST requests to AGG. This method will set the Authorization header (along with any headers or generic tasks such as logging and error handling). This method can be used by any other classes or functions to make calls to AGG. Note that the generic parameter T will be specified by the caller.

1
2
private <T> ResponseEntity<PapiResponse<T>> getPapiResponse(
        Class<T> clazz,
        GraphQlRequestBody graphQlRequestBody
) {
    ResponseEntity<PapiResponse<T>> papiResponse =
            webClient
                    .post()
                    .header(HttpHeaders.CONTENT_TYPE, "application/json")
                    .header(HttpHeaders.AUTHORIZATION, "Bearer " + containerToken)
                    .bodyValue(graphQlRequestBody)
                    .exchangeToMono(clientResponse -> {
                        if (clientResponse.statusCode().isError()) {
                            return clientResponse.createException().flatMap(Mono::error);
                        } else {
                            ResolvableType resolvableType =
                                    ResolvableType.forClassWithGenerics(
                                            PapiResponse.class,
                                            clazz
                                    );
                            ParameterizedTypeReference<PapiResponse<T>> typeRef =
                                    ParameterizedTypeReference.forType(
                                            resolvableType.getType()
                                    );
                            return clientResponse.toEntity(typeRef);
                        }
                    })
                    .block();

    return papiResponse;
}

Finally, let's add methods that we'll use to make offeringCatalog and offeringDetails queries to AGG:

1
2
public ResponseEntity<PapiResponse<PartnerData>> fetchPartnerOfferingCatalog() throws IOException {
    String query = GraphQlSchemaReaderUtil.getSchemaFromFileName("graphql/queries/partnerOfferingCatalogQuery.graphql");
    GraphQlRequestBody graphQlRequestBody = new GraphQlRequestBody(query, null, null);
    return getPapiResponse(
            PartnerData.class,
            graphQlRequestBody
    );
}

public ResponseEntity<PapiResponse<PartnerData>> fetchPartnerOfferingDetails(PartnerOfferingFilter partnerOfferingFilter) throws IOException {
    String query = GraphQlSchemaReaderUtil.getSchemaFromFileName("graphql/queries/partnerOfferingDetailsQuery.graphql");

    WhereFilter<PartnerOfferingFilter> whereFilter = WhereFilter.<PartnerOfferingFilter>builder()
            .where(partnerOfferingFilter)
            .build();
    GraphQlRequestBody graphQlRequestBody = new GraphQlRequestBody(query, whereFilter, null);
    return getPapiResponse(
            PartnerData.class,
            graphQlRequestBody
    );
}

The full PapiClient class should look like this:

1
2
package com.example.papiclientexamplejava.client;

import com.example.papiclientexamplejava.common.PropertyConstants;
import com.example.papiclientexamplejava.entities.GraphQlRequestBody;
import com.example.papiclientexamplejava.entities.agg.PapiResponse;
import com.example.papiclientexamplejava.entities.agg.offering.PartnerData;
import com.example.papiclientexamplejava.entities.agg.offering.filter.PartnerOfferingFilter;
import com.example.papiclientexamplejava.entities.WhereFilter;
import com.example.papiclientexamplejava.utils.GraphQlSchemaReaderUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.io.IOException;

@Component
public class PapiClient {
    private WebClient webClient;
    private String containerToken;

    @Autowired
    public PapiClient(WebClientBuilders webClientBuilders, Environment environment) {
        this.webClient =
                webClientBuilders
                        .getWebClientBuilder()
                        .baseUrl(environment.getProperty(PropertyConstants.SERVICES_AGG_URL))
                        .build();

        this.containerToken = environment.getProperty("container-token");
    }

    private <T> ResponseEntity<PapiResponse<T>> getPapiResponse(
            Class<T> clazz,
            GraphQlRequestBody graphQlRequestBody
    ) {
        ResponseEntity<PapiResponse<T>> papiResponse =
                webClient
                        .post()
                        .header(HttpHeaders.CONTENT_TYPE, "application/json")
                        .header(HttpHeaders.AUTHORIZATION, "Bearer " + containerToken)
                        .bodyValue(graphQlRequestBody)
                        .exchangeToMono(clientResponse -> {
                            if (clientResponse.statusCode().isError()) {
                                return clientResponse.createException().flatMap(Mono::error);
                            } else {
                                ResolvableType resolvableType =
                                        ResolvableType.forClassWithGenerics(
                                                PapiResponse.class,
                                                clazz
                                        );
                                ParameterizedTypeReference<PapiResponse<T>> typeRef =
                                        ParameterizedTypeReference.forType(
                                                resolvableType.getType()
                                        );
                                return clientResponse.toEntity(typeRef);
                            }
                        })
                        .block();

        return papiResponse;
    }


    public ResponseEntity<PapiResponse<PartnerData>> fetchPartnerOfferingCatalog() throws IOException {
        String query = GraphQlSchemaReaderUtil.getSchemaFromFileName("graphql/queries/partnerOfferingCatalogQuery.graphql");
        GraphQlRequestBody graphQlRequestBody = new GraphQlRequestBody(query, null, null);
        return getPapiResponse(
                PartnerData.class,
                graphQlRequestBody
        );
    }

    public ResponseEntity<PapiResponse<PartnerData>> fetchPartnerOfferingDetails(PartnerOfferingFilter partnerOfferingFilter) throws IOException {
        String query = GraphQlSchemaReaderUtil.getSchemaFromFileName("graphql/queries/partnerOfferingDetailsQuery.graphql");

        WhereFilter<PartnerOfferingFilter> whereFilter = WhereFilter.<PartnerOfferingFilter>builder()
                .where(partnerOfferingFilter)
                .build();
        GraphQlRequestBody graphQlRequestBody = new GraphQlRequestBody(query, whereFilter, null);
        return getPapiResponse(
                PartnerData.class,
                graphQlRequestBody
        );
    }
}

Service

Now we can create a service called CatalogService that uses the methods in PapiClient to call offeringCatalog and offeringDetails from AGG:

1
2
package com.example.papiclientexamplejava;

import com.example.papiclientexamplejava.entities.agg.offering.filter.PartnerOfferingFilter;
import com.example.papiclientexamplejava.client.PapiClient;
import com.example.papiclientexamplejava.entities.agg.PapiResponse;
import com.example.papiclientexamplejava.entities.agg.offering.PartnerData;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import java.io.IOException;

@Service
public class CatalogService {
    private final PapiClient papiClient;

    public CatalogService(PapiClient papiClient) {
        this.papiClient = papiClient;
    }

    public PapiResponse<PartnerData> fetchPartnerOfferingCatalog() throws IOException {
        ResponseEntity<PapiResponse<PartnerData>> responseEntity = papiClient.fetchPartnerOfferingCatalog();
        System.out.println(responseEntity.getBody());
        return responseEntity.getBody();
    }

    public PapiResponse<PartnerData> fetchPartnerOfferingDetails(PartnerOfferingFilter partnerOfferingFilter) throws IOException {
        ResponseEntity<PapiResponse<PartnerData>> responseEntity = papiClient.fetchPartnerOfferingDetails(partnerOfferingFilter);
        System.out.println(responseEntity.getBody());
        return responseEntity.getBody();
    }
}

Offering Catalog

We can inject CatalogService into a class to call the offeringCatalog query from AGG

1
2
package com.example.papiclientexamplejava;

import com.example.papiclientexamplejava.entities.agg.PapiResponse;
import com.example.papiclientexamplejava.entities.agg.offering.PartnerData;
import com.example.papiclientexamplejava.service.CatalogService;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class ExampleClass {

    private final CatalogService catalogService;

    public ExampleClass(CatalogService catalogService) {
        this.catalogService = catalogService;
    }

    public PapiResponse<PartnerData> fetchPartnerOfferingCatalog() throws IOException {
        PapiResponse<PartnerData> response = catalogService.fetchPartnerOfferingCatalog();
        return response;
    }
}

Offering Details + Apps

We can inject CatalogService into a class to call the partnerOfferingDetails query from AGG

The partnerOfferingDetails query has a whereFilter which is required to specify either a cloud or a BTF product identifier, along with additional optional parameters. This is defined as the PartnerOfferingFilter GraphQL type.

We can either fetch cloud or BTF product and app details. Only one product can be fetched at a time.

Cloud

Cloud products can be fetched by passing a cloud product filter

1
2
package com.example.papiclientexamplejava;

import com.example.papiclientexamplejava.entities.agg.PapiResponse;
import com.example.papiclientexamplejava.entities.agg.offering.PartnerData;
import com.example.papiclientexamplejava.entities.agg.offering.filter.PartnerOfferingCloudInput;
import com.example.papiclientexamplejava.entities.agg.offering.filter.PartnerOfferingFilter;
import com.example.papiclientexamplejava.service.CatalogService;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class ExampleClass {

    private final CatalogService catalogService;

    public ExampleClass(CatalogService catalogService) {
        this.catalogService = catalogService;
    }
    
    public PapiResponse<PartnerData> fetchCloudProduct() throws IOException {
        PartnerOfferingCloudInput partnerOfferingCloudInput = PartnerOfferingCloudInput.builder()
                .id("0b8ea1e2-52df-11ea-8d77-2e728ce88125")
                .build();

        PartnerOfferingFilter partnerOfferingFilter = PartnerOfferingFilter.builder()
                .cloudProduct(partnerOfferingCloudInput)
                .build();

        PapiResponse<PartnerData> response = catalogService.fetchPartnerOfferingDetails(partnerOfferingFilter);
        return response;
    }
}
BTF

BTF products can be fetched by passing a BTF product filter

1
2
package com.example.papiclientexamplejava;

import com.example.papiclientexamplejava.entities.agg.PapiResponse;
import com.example.papiclientexamplejava.entities.agg.offering.PartnerData;
import com.example.papiclientexamplejava.entities.agg.offering.filter.PartnerOfferingBtfInput;
import com.example.papiclientexamplejava.entities.agg.offering.filter.PartnerOfferingFilter;
import com.example.papiclientexamplejava.service.CatalogService;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class ExampleClass {

    private final CatalogService catalogService;

    public ExampleClass(CatalogService catalogService) {
        this.catalogService = catalogService;
    }

    public PapiResponse<PartnerData> fetchBtfProduct() throws IOException {
        PartnerOfferingBtfInput partnerOfferingBtfInput = PartnerOfferingBtfInput.builder()
                .productKey("confluence")
                .build();

        PartnerOfferingFilter partnerOfferingFilter = PartnerOfferingFilter.builder()
                .btfProduct(partnerOfferingBtfInput)
                .build();

        PapiResponse<PartnerData> response = catalogService.fetchPartnerOfferingDetails(partnerOfferingFilter);
        return response;
    }
}

Rate this page: