<repo_name>ocadotechnology/newrelic-alerts-configurator<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/exception/NewRelicSyncException.java
package com.ocadotechnology.newrelic.alertsconfigurator.exception;

public class NewRelicSyncException extends RuntimeException {
    public NewRelicSyncException(String message) {
        super(message);
    }
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/model/transactions/KeyTransaction.java
package com.ocadotechnology.newrelic.apiclient.model.transactions;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.ocadotechnology.newrelic.apiclient.model.applications.ApplicationSummary;
import com.ocadotechnology.newrelic.apiclient.model.applications.EndUserSummary;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;

import java.time.OffsetDateTime;

/**
 * See <a href="https://rpm.newrelic.com/api/explore/key_transactions/list">Doc</a>
 */
@Value
@Builder
@AllArgsConstructor
public class KeyTransaction {
    @JsonProperty
    Integer id;
    @JsonProperty
    String name;
    @JsonProperty("transaction_name")
    String transactionName;
    @JsonProperty("health_status")
    String healthStatus;
    @JsonProperty
    Boolean reporting;
    @JsonProperty("last_reported_at")
    OffsetDateTime lastReportedAt;
    @JsonProperty("application_summary")
    ApplicationSummary applicationSummary;
    @JsonProperty("end_user_summary")
    EndUserSummary endUserSummary;
    @JsonProperty
    KeyTransactionLinks links;
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/graphql/apiclient/internal/GraphqlAlertsPoliciesApi.java
package com.ocadotechnology.newrelic.graphql.apiclient.internal;

import com.ocadotechnology.newrelic.apiclient.AlertsPoliciesApi;
import com.ocadotechnology.newrelic.apiclient.model.policies.AlertsPolicy;
import com.ocadotechnology.newrelic.apiclient.model.policies.AlertsPolicyChannels;
import com.ocadotechnology.newrelic.graphql.apiclient.internal.transport.JavaLikeEnum;
import com.ocadotechnology.newrelic.graphql.apiclient.internal.transport.Query;
import com.ocadotechnology.newrelic.graphql.apiclient.internal.transport.Result;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import static com.github.k0kubun.builder.query.graphql.GraphQLQueryBuilder.query;
import static com.ocadotechnology.newrelic.graphql.apiclient.internal.util.Preconditions.checkArgument;
import static java.util.Objects.nonNull;

public class GraphqlAlertsPoliciesApi extends ApiBase implements AlertsPoliciesApi {
    private final long accountId;

    public GraphqlAlertsPoliciesApi(QueryExecutor executor, long accountId) {
        super(executor);
        this.accountId = accountId;
    }

    @Override
    public Optional<AlertsPolicy> getByName(String alertsPolicyName) {
        String query = query()
                .object("actor",
                        object(actor -> actor.object("account", param("id", accountId),
                                object(account -> account.object("alerts",
                                        object(alerts -> alerts.object(
                                                "policiesSearch",
                                                param("searchCriteria", complexParamValue(param("name", alertsPolicyName))),
                                                object(policiesSearch -> policiesSearch.object("policies", object(policies -> policies
                                                        .field("id")
                                                        .field("name")
                                                        .field("incidentPreference"))))))))))).build();

        List<AlertsPolicy> policies = execute(Query.wrapped(query)).getPolicies();
        return policies.isEmpty() ? Optional.empty() : Optional.ofNullable(policies.get(0));
    }

    public Optional<AlertsPolicy> getById(int policyId) {
        return getPolicy(doGetById(policyId));
    }

    private Result doGetById(int policyId) {
        String query = query()
                .object("actor",
                        object(actor -> actor.object("account", param("id", accountId),
                                object(account -> account.object("alerts",
                                        object(alerts -> alerts.object("policy", param("id", policyId),
                                                object(policy -> policy
                                                        .field("id")
                                                        .field("name")
                                                        .field("incidentPreference"))))))))).build();

        return execute(Query.wrapped(query));
    }

    private Optional<AlertsPolicy> getPolicy(Result result) {
        return result.notFound() ? Optional.empty() : throwOnError(result).getAlertsPolicy();
    }

    @Override
    public AlertsPolicy create(AlertsPolicy policy) {
        checkArgument(nonNull(policy), "policy must not be null");
        checkArgument(nonNull(policy.getName()), "name must not be null");
        checkArgument(nonNull(policy.getIncidentPreference()), "incident preference must not be null");

        String query = query()
                .object("mutation",
                        object(mutation -> mutation.object(
                                "alertsPolicyCreate",
                                params(
                                        "accountId", accountId,
                                        "policy", complexParamValue(params(
                                                "name", policy.getName(),
                                                "incidentPreference", new JavaLikeEnum(policy.getIncidentPreference())))),

                                object(alertsPolicyCreate -> alertsPolicyCreate
                                        .field("id")
                                        .field("name")
                                        .field("incidentPreference"))))).build();

        return throwOnError(execute(Query.of(query))).getAlertsPolicyCreate().get();
    }

    @Override
    public AlertsPolicy delete(int policyId) {
        AlertsPolicy policy = throwOnError(doGetById(policyId)).getAlertsPolicy().get();

        String query = query()
                .object("mutation",
                        object(mutation -> mutation.object(
                                "alertsPolicyDelete",
                                params(
                                        "accountId", accountId,
                                        "id", "" + policyId),
                                object(alertsPolicyDelete -> alertsPolicyDelete.field("id")))))
                .build();

        throwOnError(execute(Query.of(query)));
        return policy;
    }

    @Override
    public AlertsPolicyChannels updateChannels(AlertsPolicyChannels channels) {
        checkArgument(nonNull(channels), "channels must not be null");
        checkArgument(nonNull(channels.getPolicyId()), "policy id must not be null");

        String query = query()
                .object("mutation",
                        object(mutation -> mutation.object(
                                "alertsNotificationChannelsAddToPolicy",
                                params(
                                        "accountId", accountId,
                                        "policyId", channels.getPolicyId(),
                                        "notificationChannelIds", Optional.ofNullable(channels.getChannelIds()).orElseGet(ArrayList::new)),
                                object(alertsNotificationChannelsAddToPolicy -> alertsNotificationChannelsAddToPolicy
                                        .object("notificationChannels", object(notificationChannels -> notificationChannels
                                                .field("id")))
                                        .object("errors", object(errors -> errors
                                                .field("description")
                                                .field("errorType")
                                                .field("notificationChannelId")))))))
                .build();

        List<Integer> notificationChannelIds = throwOnError(execute(Query.of(query)), Result::getAlertsNotificationChannelsAddToPolicy)
                .orElseGet(ArrayList::new);
        return AlertsPolicyChannels.builder().policyId(channels.getPolicyId()).channelIds(notificationChannelIds).build();
    }
}
<file_sep>/newrelic-alerts-configurator-dsl/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/dsl/configuration/condition/NrqlConditionDsl.kt
package com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.condition

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.NrqlCondition
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.signal.NrqlSignalConfiguration
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.NrqlTermsConfiguration
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.NewRelicConfigurationMarker
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.condition.terms.NrqlTermConfigurations

@NewRelicConfigurationMarker
class NrqlConditionDsl {
    var conditionName: String? = null
    var enabled: Boolean = true
    var runBookUrl: String? = null
    var valueFunction: NrqlCondition.ValueFunction? = null
    var query: String? = null
    internal val terms: MutableList<NrqlTermsConfiguration> = mutableListOf()
    var nrqlSignalConfiguration: NrqlSignalConfiguration? = null

    fun terms(block: NrqlTermConfigurations.() -> Unit) = terms.addAll(NrqlTermConfigurations().apply(block).terms)

    fun signal(block: NrqlSignalConfigurationDsl.() -> Unit) {
        nrqlSignalConfiguration = nrqlSignalConfiguration(block)
    }
}

fun nrqlCondition(block: NrqlConditionDsl.() -> Unit): NrqlCondition {
    val dsl = NrqlConditionDsl()
    dsl.block()

    return NrqlCondition.builder()
            .conditionName(requireNotNull(dsl.conditionName) { "Nrql condition name cannot be null" })
            .enabled(dsl.enabled)
            .runBookUrl(dsl.runBookUrl)
            .terms(dsl.terms)
            .valueFunction(requireNotNull(dsl.valueFunction) { "Nrql condition value cannot be null" })
            .query(requireNotNull(dsl.query) { "Nrql condition query cannot be null" })
            .signal(dsl.nrqlSignalConfiguration)
            .build()
}<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/model/synthetics/Monitor.java
package com.ocadotechnology.newrelic.apiclient.model.synthetics;

import java.util.Collection;

import com.fasterxml.jackson.annotation.JsonProperty;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Singular;
import lombok.Value;

/**
 * See <a href="https://docs.newrelic.com/docs/apis/synthetics-rest-api/monitor-examples/manage-synthetics-monitors-rest-api">Doc</a>
 */
@Value
@Builder
@AllArgsConstructor
public class Monitor {
    @JsonProperty("id")
    String uuid;
    @JsonProperty
    String name;
    @JsonProperty
    String type;
    @JsonProperty
    Integer frequency;
    @JsonProperty
    @Singular
    Collection<String> locations;
    @JsonProperty
    String uri;
    @JsonProperty
    String status;
    @JsonProperty
    Double slaThreshold;
    @JsonProperty
    Integer userId;
    @JsonProperty
    String apiVersion;
}
<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/condition/violationclosetimer/ViolationCloseTimer.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.violationclosetimer;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor(access = AccessLevel.PACKAGE)
public enum ViolationCloseTimer {
    DURATION_1("1"),
    DURATION_2("2"),
    DURATION_4("4"),
    DURATION_8("8"),
    DURATION_12("12"),
    DURATION_24("24");

    String duration;
}
<file_sep>/settings.gradle
rootProject.name = 'newrelic-alerts-configurator'
include 'newrelic-api-client', 'newrelic-alerts-configurator', 'newrelic-alerts-configurator-examples', 'newrelic-alerts-configurator-dsl'
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/internal/ApiBase.java
package com.ocadotechnology.newrelic.apiclient.internal;

import com.ocadotechnology.newrelic.apiclient.internal.client.NewRelicClient;
import com.ocadotechnology.newrelic.apiclient.internal.model.ObjectList;

import javax.ws.rs.client.Invocation.Builder;
import javax.ws.rs.core.Link;
import javax.ws.rs.core.Response;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;

abstract class ApiBase {

    protected final NewRelicClient client;

    ApiBase(NewRelicClient client) {
        this.client = client;
    }

    <T, LT extends ObjectList<T, LT>> LT getPageable(Builder request, Class<LT> responseType) {
        Response response = request.get();
        LT responseBody = response.readEntity(responseType);
        Link next = response.getLink("next");
        return followPageable(next, responseBody, responseType);
    }

    private <T, LT extends ObjectList<T, LT>> LT followPageable(Link next, LT responseBody, Class<LT> responseType) {
        while (next != null) {
            Response nextResponse = client.target(next).request(APPLICATION_JSON_TYPE).get();
            LT nextResponseBody = nextResponse.readEntity(responseType);
            responseBody = responseBody.merge(nextResponseBody);
            next = nextResponse.getLink("next");
        }
        return responseBody;
    }
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/model/servers/ServerLinks.java
package com.ocadotechnology.newrelic.apiclient.model.servers;


import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;

@Value
public class ServerLinks {
    @JsonProperty("alert_policy")
    Integer alertPolicy;
}
<file_sep>/MIGRATION.md
# Migrating from version 2 to 3

## New Conditions and Channels management policy

In version 3 we changed the way condition and channel synchronization works. In version 3 all conditions and channels are managed
 in the same way.

1. If you don't define any conditions in the policy, synchronization process will not modify already existing conditions.
   Example:
   ```java
   PolicyConfiguration configuration = PolicyConfiguration.builder()
       .policyName("Policy name")
       .incidentPreference(PolicyConfiguration.IncidentPreference.PER_POLICY)
       .build()
   ```
   This configuration will not remove, create or update any existing condition.
   
1. If you define some conditions in the policy, then:
    - conditions which are defined in configuration, but are missing will be created.
    - conditions which are defined in configuration and already exist will be updated.
    - conditions which are not defined in configuration, but already exist will be removed.
   Example:
   ```java
   PolicyConfiguration configuration = PolicyConfiguration.builder()
       .policyName("Policy name")
       .incidentPreference(PolicyConfiguration.IncidentPreference.PER_POLICY)
       .condition(condition1)
       .condition(condition2)
       .build()
   ```
   This configuration will create/update condition1 and condition2, and will remove all other conditions.
   
1. If you define empty conditions list in the policy, then synchronization process will remove all existing conditions.
   Example:
   ```java
   PolicyConfiguration configuration = PolicyConfiguration.builder()
       .policyName("Policy name")
       .incidentPreference(PolicyConfiguration.IncidentPreference.PER_POLICY)
       .conditions(Collections.emptyList())
       .build()
   ```
   This configuration will not create nor update any conditions, but will remove all existing ones.
   
In version 2 there was no way to preserve already existing conditions. Existing conditions were removed in both cases: 
when you 
provided empty conditions list and when you didn't provide anything at all

## Changed default condition state

In version 2 all conditions were by default disabled. In version 3 it has been changed and conditions are by default enabled<file_sep>/newrelic-alerts-configurator/src/test/java/com/ocadotechnology/newrelic/alertsconfigurator/ApmUserDefinedConditionConfiguratorTest.java
package com.ocadotechnology.newrelic.alertsconfigurator;

import com.google.common.collect.ImmutableList;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.PolicyConfiguration;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.ApmAppCondition;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.Condition;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.UserDefinedConfiguration;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.DurationTerm;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.OperatorTerm;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.PriorityTerm;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.TermsConfiguration;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.TimeFunctionTerm;
import com.ocadotechnology.newrelic.alertsconfigurator.internal.entities.EntityResolver;
import com.ocadotechnology.newrelic.apiclient.model.conditions.AlertsCondition;
import com.ocadotechnology.newrelic.apiclient.model.policies.AlertsPolicy;
import org.assertj.core.groups.Tuple;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;

import java.util.Collections;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class ApmUserDefinedConditionConfiguratorTest extends AbstractConfiguratorTest {
    @Rule
    public final ExpectedException expectedException = ExpectedException.none();
    private static final String POLICY_NAME = "policyName";
    private static final String METRIC = "userDefined";
    private static final String CONDITION_NAME = "conditionName";
    private static final boolean ENABLED = true;
    private static final String APPLICATION_NAME = "applicationName";
    private static final int APPLICATION_ENTITY_ID = 1;

    private static final ApmAppCondition.ConditionScope CONDITION_SCOPE = ApmAppCondition.ConditionScope.APPLICATION;
    private static final AlertsPolicy POLICY = AlertsPolicy.builder().id(42).name(POLICY_NAME).build();
    private static final TermsConfiguration TERMS_CONFIGURATION = createTermsConfiguration().build();
    private static final UserDefinedConfiguration USER_DEFINED_CONFIGURATION = createUserDefinedConfiguration();
    private static final Condition APP_CONDITION = createAppCondition(CONDITION_NAME);
    private static final PolicyConfiguration CONFIGURATION = createConfiguration();

    @Captor
    private ArgumentCaptor<AlertsCondition> alertsConditionCaptor;
    @Mock
    private EntityResolver entityResolverMock;

    @InjectMocks
    private ConditionConfigurator testee;

    @Before
    public void setUp() {
        when(alertsPoliciesApiMock.getByName(POLICY_NAME)).thenReturn(Optional.of(POLICY));
        when(entityResolverMock.resolveEntities(apiMock, APP_CONDITION)).thenReturn(Collections.singletonList(APPLICATION_ENTITY_ID));
    }

    @Test
    public void shouldCorrectlyCreateUserDefinedCondition() throws Exception {
        //given
        when(alertsConditionsApiMock.list(POLICY.getId())).thenReturn(ImmutableList.of());
        when(alertsConditionsApiMock.create(eq(POLICY.getId()), any(AlertsCondition.class))).thenReturn(AlertsCondition.builder().build());

        //when
        testee.sync(CONFIGURATION);

        //then
        verify(alertsConditionsApiMock).create(eq(POLICY.getId()), alertsConditionCaptor.capture());
        AlertsCondition result = alertsConditionCaptor.getValue();
        assertThat(result.getConditionScope()).isEqualTo("application");
        assertThat(result.getType()).isEqualTo("apm_app_metric");
        assertThat(result.getName()).isEqualTo(CONDITION_NAME);
        assertThat(result.getEnabled()).isEqualTo(true);
        assertThat(result.getEntities()).containsExactly(APPLICATION_ENTITY_ID);
        assertThat(result.getMetric()).isEqualTo("user_defined");
        assertThat(result.getTerms())
                .extracting("duration", "operator", "priority", "threshold", "timeFunction")
                .containsExactly(new Tuple("5", "above", "critical", "0.5", "all"));
        assertThat(result.getUserDefined().getMetric()).isEqualTo(METRIC);
        assertThat(result.getUserDefined().getValueFunction()).isEqualTo("average");
    }

    private static PolicyConfiguration createConfiguration() {
        return PolicyConfiguration.builder()
            .policyName(POLICY_NAME)
            .incidentPreference(PolicyConfiguration.IncidentPreference.PER_POLICY)
            .condition(APP_CONDITION)
            .build();
    }

    private static TermsConfiguration.TermsConfigurationBuilder createTermsConfiguration() {
        return TermsConfiguration.builder()
            .durationTerm(DurationTerm.DURATION_5)
            .operatorTerm(OperatorTerm.ABOVE)
            .priorityTerm(PriorityTerm.CRITICAL)
            .thresholdTerm(0.5f)
            .timeFunctionTerm(TimeFunctionTerm.ALL);
    }

    private static ApmAppCondition createAppCondition(String conditionName) {
        return ApmAppCondition.builder()
            .conditionName(conditionName)
            .enabled(ENABLED)
            .application(APPLICATION_NAME)
            .metric(ApmAppCondition.Metric.USER_DEFINED)
            .conditionScope(CONDITION_SCOPE)
            .term(TERMS_CONFIGURATION)
            .userDefinedConfiguration(USER_DEFINED_CONFIGURATION)
            .build();
    }

    private static UserDefinedConfiguration createUserDefinedConfiguration() {
        return UserDefinedConfiguration.builder()
                .metric(METRIC)
                .valueFunction(UserDefinedConfiguration.ValueFunction.AVERAGE).build();
    }
}
<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/channel/SlackChannel.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel;

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.internal.SlackChannelTypeSupport;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;

/**
 * Slack channel configuration.
 * Configuration parameters:
 * <ul>
 *     <li>{@link #channelName}</li>
 *     <li>{@link #slackUrl}</li>
 *     <li>{@link #teamChannel} (optional)</li>
 * </ul>
 */
@Builder
@Getter
public class SlackChannel implements Channel {
    private final ChannelType type = ChannelType.SLACK;
    /**
     * Name of your alerts channel
     */
    @NonNull
    private String channelName;
    /**
     * Slack url to your channel
     */
    @NonNull
    private String slackUrl;
    /**
     * Name of your team channel
     */
    private String teamChannel;

    private final ChannelTypeSupport channelTypeSupport = new SlackChannelTypeSupport(this);

    @Override
    public ChannelTypeSupport getChannelTypeSupport() {
        return channelTypeSupport;
    }
}
<file_sep>/CONTRIBUTING.md
## How to contribute to NewRelic Alerts Configurator

#### Did you find a bug?
- Ensure the bug was not already reported by searching on GitHub under [Issues](https://github.com/ocadotechnology/newrelic-alerts-configurator/issues).
- Open new issue. Include a title and description with as much information as possible. 
  Code samples and test cases demonstrating the problem or steps to reproduce would be a great help.
  
#### Did you write a patch that fixes a bug?
- Make sure an issue describing the bug exists.
- Open a new GitHub [pull request](https://help.github.com/categories/collaborating-with-issues-and-pull-requests/) with the 
  patch.

#### Do you intend to add a new feature or change an existing one?
- Open a new GitHub [pull request](https://help.github.com/categories/collaborating-with-issues-and-pull-requests/) with the 
  patch. Describe introduced feature in details. Remember to provide unit tests and documentation.
<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/internal/entities/IdProvider.java
package com.ocadotechnology.newrelic.alertsconfigurator.internal.entities;


import com.ocadotechnology.newrelic.apiclient.NewRelicApi;

public interface IdProvider {
    Integer getId(NewRelicApi api, String name);
}
<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/condition/terms/TermsUtils.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms;

import com.ocadotechnology.newrelic.apiclient.model.conditions.Terms;

import java.util.Collection;
import java.util.stream.Collectors;

public final class TermsUtils {

    private TermsUtils() {
    }

    public static Collection<Terms> createTerms(Collection<TermsConfiguration> termsConfigurations) {
        return termsConfigurations.stream().map(TermsUtils::mapTerms).collect(Collectors.toList());
    }

    private static Terms mapTerms(TermsConfiguration termsConfiguration) {
        return Terms.builder()
            .duration(String.valueOf(termsConfiguration.getDurationTerm().getDuration()))
            .operator(termsConfiguration.getOperatorTerm().name().toLowerCase())
            .priority(termsConfiguration.getPriorityTerm().name().toLowerCase())
            .threshold(String.valueOf(termsConfiguration.getThresholdTerm()))
            .timeFunction(termsConfiguration.getTimeFunctionTerm().name().toLowerCase())
            .build();
    }

    public static Collection<Terms> createNrqlTerms(Collection<NrqlTermsConfiguration> termsConfigurations) {
        return termsConfigurations.stream().map(TermsUtils::mapNrqlTerms).collect(Collectors.toList());
    }

    private static Terms mapNrqlTerms(NrqlTermsConfiguration termsConfiguration) {
        return Terms.builder()
                .duration(String.valueOf(termsConfiguration.getDurationTerm().getDuration()))
                .operator(termsConfiguration.getOperatorTerm().name().toLowerCase())
                .priority(termsConfiguration.getPriorityTerm().name().toLowerCase())
                .threshold(String.valueOf(termsConfiguration.getThresholdTerm()))
                .timeFunction(termsConfiguration.getTimeFunctionTerm().name().toLowerCase())
                .build();
    }
}
<file_sep>/newrelic-alerts-configurator-dsl/README.md
# Table of contents

- [Configurator DSL](#configurator-dsl)
- [Application configuration DSL](#application-configuration-dsl)
- [Policy configuration DSL](#policy-configuration-dsl)
    - Alerts condition
        - [APM application metric condition DSL](#apm-application-metric-condition-dsl)
        - [APM JVM metric condition DSL](#apm-jvm-metric-condition-dsl)
        - [APM key transaction metric condition DSL](#apm-key-transaction-metric-condition-dsl)
        - [Server metric condition DSL](#server-metric-condition-dsl)
        - [New Relic Browser metric condition DSL](#new-relic-browser-metric-condition-dsl)
        - Alerts external service condition
            - [APM external service condition DSL](#apm-external-service-condition-dsl)
        - [Alerts NRQL condition DSL](#alerts-nrql-condition-dsl)
        - [Synthetics Monitor Failure condition DSL](#synthetics-monitor-failure-condition-dsl)
        - [User defined configuration DSL](#user-defined-configuration-dsl)
        - [Term DSL](#term-configurations-dsl)
        - [NRQL Term DSL](#nrql-term-configuration-dsl)
        - Notification channel
            - [Email channel DSL](#email-channel-dsl)
            - [Slack channel DSL](#slack-channel-dsl)
            - [Webhook channel DSL](#webhook-channel-dsl)
            - [PagerDuty channel DSL](#pagerduty-channel-dsl)
            - [NewRelic user channel DSL](#newrelic-user-channel-dsl)

## Configurator DSL
To create and run [Configurator](../newrelic-alerts-configurator#configurator) use `configuration` dsl. 

```kotlin
val appConfig: ApplicationConfiguration
val policyConfig: PolicyConfiguration

configuration(yourApiKey) {
    applications {
        application { /* configuration described in Application configuration DSL section */ }
        application { /* configuration described in Application configuration DSL section */ }
        +appConfig // if you already have ApplicationConfiguration defined, you can add it by using unary plus operator
    }
    policies {
        policy { /* configuration described in Policy configuration DSL section */ }
        policy { /* configuration described in Policy configuration DSL section */ }
        policy { /* configuration described in Policy configuration DSL section */ }
        +policyConfig // if you already have PolicyConfiguration defined, you can add it by using unary plus operator
    }
}
```

This will create `Configurator`, set applications and policies, and *run* synchronization process

# Application configuration DSL
To create [ApplicationConfiguration](../newrelic-alerts-configurator#application-configuration) use `applicationConfiguration` dsl.

```kotlin
val config: ApplicationConfiguration = applicationConfiguration {
    applicationName = "test"
    appApdexThreshold = 0.5f
    endUserApdexThreshold = 0.7f
    enableRealUserMonitoring = true
}
```

## Policy configuration DSL
To create [PolicyConfiguration](../newrelic-alerts-configurator#policy-configuration) use `policyConfiguration` dsl.

```kotlin
val conditionConfig: ApmJvmCondition
val channelConfig: EmailChannel

val config: PolicyConfiguration = policyConfiguration {
    policyName = "Application Policy"
    incidentPreference = PolicyConfiguration.IncidentPreference.PER_POLICY

    conditions {
        nrql { /* configuration described in NRQL condition DSL section */ }
        apmApp { /* configuration described in APM application metric condition DSL section */ }
        apmJvm { /* configuration described in Apm JVM condition DSL section */ }
        browser { /* configuration described in New Relic browser metric condition DSL section */ }
        synthetics { /* configuration described in Synthetics Monitor Failure condition DSL section */ }
        serversMetric { /* configuration described in Servers metric condition DSL section */ }
        apmKeyTransation { /* configuration described in Apm key transaction condition DSL section */ }
        apmExternalService { /* configuration described in APM external service condition DSL section */ }
        +conditionConfig // if you already have condition defined, you can add it by using unary plus operator
    }

    channels {
        user { /* configuration described in User channel DSL section */ }
        email { /* configuration described in Email channel DSL section */ }
        slack { /* configuration described in Slack channel DSL section */ }
        webhook { /* configuration described in Webhook channel DSL section */ }
        pagerDuty { /* configuration described in PagerDuty channel DSL section */ }
        +channelConfig // if you already have channel defined, you can add it by using unary plus operator
    }
}
```

## APM application metric condition DSL
To create [ApmAppCondition](../newrelic-alerts-configurator#apm-application-metric-condition) use `apmAppCondition` dsl.

```kotlin
val termConfig: TermsConfiguration
val userDefinedConfig: UserDefinedConfiguration

val config: ApmAppCondition = apmAppCondition {
    conditionName = "Condition name"
    enabled = true
    applications = listOf("app1", "app2")
    metric = ApmAppCondition.Metric.APDEX
    conditionScope = ApmAppCondition.ConditionScope.APPLICATION
    runBookUrl = "runBookUrl"
    violationCloseTimer = ViolationCloseTimer.DURATION_24
    
    userDefined { /* configuration described in User defined configuration DSL section */ }
    // if you already have user defined configuration, you can add it:
    // userDefinedConfiguration = userDefinedConfig
    
    terms {
        term { /* configuration described in Term configuration DSL section */ }
        term { /* configuration described in Term configuration DSL section */ }
        +termConfig // if you already have term defined, you can add it by using unary plus operator
        // you can also use special fluent syntax to define term
        PriorityTerm.CRITICAL given TimeFunctionTerm.ALL inLast DurationTerm.DURATION_5 minutes OperatorTerm.BELOW value 0.7f
    }
}
```

## APM JVM metric condition DSL
To create [ApmJVMCondition](../newrelic-alerts-configurator#apm-jvm-metric-condition) use `apmJvmCondition` dsl.

```kotlin
val termConfig: TermsConfiguration

val config: ApmJVMCondition = apmJvmCondition {
    conditionName = "Condition name"
    enabled = true
    applications = listOf("app1", "app2")
    metric = ApmJvmCondition.Metric.GC_CPU_TIME
    gcMetric = ApmJvmCondition.GcMetric.GC_MARK_SWEEP
    runBookUrl = "runBookUrl"
    violationCloseTimer = ViolationCloseTimer.DURATION_24
    
    terms {
        term { /* configuration described in Term configuration DSL section */ }
        term { /* configuration described in Term configuration DSL section */ }
        +termConfig // if you already have term defined, you can add it by using unary plus operator
        // you can also use special fluent syntax to define term
        PriorityTerm.CRITICAL given TimeFunctionTerm.ALL inLast DurationTerm.DURATION_5 minutes OperatorTerm.BELOW value 0.7f
    }
}
```

## APM key transaction metric condition DSL
To create [ApmKeyTransactionCondition](../newrelic-alerts-configurator#apm-key-transaction-metric-condition) use `apmKeyTransactionCondition` dsl.

```kotlin
val termConfig: TermsConfiguration

val config: ApmKeyTransactionCondition = apmKeyTransactionCondition {
    conditionName = "Condition name"
    enabled = true
    keyTransaction = "Key Transaction name"
    metric = ApmKeyTransactionCondition.Metric.APDEX
    runBookUrl = "runBookUrl"
    
    terms {
        term { /* configuration described in Term configuration DSL section */ }
        term { /* configuration described in Term configuration DSL section */ }
        +termConfig // if you already have term defined, you can add it by using unary plus operator
        // you can also use special fluent syntax to define term
        PriorityTerm.CRITICAL given TimeFunctionTerm.ALL inLast DurationTerm.DURATION_5 minutes OperatorTerm.BELOW value 0.7f
    }
}
```

## Server metric condition DSL
To create [ServersMetricCondition](../newrelic-alerts-configurator#server-metric-condition) use `serverMetricCondition` dsl.

```kotlin
val termConfig: TermsConfiguration
val userDefinedConfig: UserDefinedConfiguration

val config: ServersMetricCondition = serverMetricCondition {
    conditionName = "Condition name"
    enabled = true
    servers = listOf("some-host", "some-host-2")
    metric = ServersMetricCondition.Metric.CPU_PERCENTAGE
    runBookUrl = "runBookUrl"
    
    userDefined { /* configuration described in User defined configuration DSL section */ }
    // if you already have user defined configuration, you can add it:
    // userDefinedConfiguration = userDefinedConfig
    
    terms {
        term { /* configuration described in Term configuration DSL section */ }
        term { /* configuration described in Term configuration DSL section */ }
        +termConfig // if you already have term defined, you can add it by using unary plus operator
        // you can also use special fluent syntax to define term
        PriorityTerm.CRITICAL given TimeFunctionTerm.ALL inLast DurationTerm.DURATION_5 minutes OperatorTerm.BELOW value 0.7f
    }
}
```

## New Relic browser metric condition DSL
To create [BrowserCondition](../newrelic-alerts-configurator#new-relic-browser-metric-condition) use `browserCondition` dsl.

```kotlin
val termConfig: TermsConfiguration
val userDefinedConfig: UserDefinedConfiguration

val config: BrowserCondition = browserCondition {
    conditionName = "Condition name"
    enabled = true
    applications = listOf("application 1", "application 2")
    metric = BrowserCondition.Metric.PAGE_VIEWS_WITH_JS_ERRORS
    runBookUrl = "runBookUrl"
    
    userDefined { /* configuration described in User defined configuration DSL section */ }
    // if you already have user defined configuration, you can add it:
    // userDefinedConfiguration = userDefinedConfig
    
    terms {
        term { /* configuration described in Term configuration DSL section */ }
        term { /* configuration described in Term configuration DSL section */ }
        +termConfig // if you already have term defined, you can add it by using unary plus operator
        // you can also use special fluent syntax to define term
        PriorityTerm.CRITICAL given TimeFunctionTerm.ALL inLast DurationTerm.DURATION_5 minutes OperatorTerm.BELOW value 0.7f
    }
}
```

## APM external service condition DSL
To create [ApmExternalServiceCondition](../newrelic-alerts-configurator#apm-external-service-condition) use `apmExternalServiceCondition` dsl.

```kotlin
val termConfig: TermsConfiguration

val config: ApmExternalServiceCondition = apmExternalServiceCondition {
    conditionName = "Condition name"
    enabled = true
    applications = listOf("application 1", "application 2")
    externalServiceUrl = "externalServiceUrl"
    metric = ApmExternalServiceCondition.Metric.RESPONSE_TIME_AVERAGE
    runBookUrl = "runBookUrl"
    
    terms {
        term { /* configuration described in Term configuration DSL section */ }
        term { /* configuration described in Term configuration DSL section */ }
         +termConfig // if you already have term defined, you can add it by using unary plus operator
         // you can also use special fluent syntax to define term
         PriorityTerm.CRITICAL given TimeFunctionTerm.ALL inLast DurationTerm.DURATION_5 minutes OperatorTerm.BELOW value 0.7f
    }
}
```

## Alerts NRQL condition DSL
To create [NrqlCondition](../newrelic-alerts-configurator#alerts-nrql-condition) use `nrqlCondition` dsl.

```kotlin
val termConfig: NrqlTermsConfiguration

val config: NrqlCondition = nrqlCondition {
    conditionName = "Condition name"
    enabled = true
    valueFunction = NrqlCondition.ValueFunction.SINGLE_VALUE
    query = "SELECT count(*) FROM `myApp:HealthStatus` WHERE healthy IS false"
    runBookUrl = "runBookUrl"
    
    terms {
        term { /* configuration described in NRQL term configuration DSL section */ }
        term { /* configuration described in NRQL term configuration DSL section */ }
        +termConfig // if you already have term defined, you can add it by using unary plus operator
        // you can also use special fluent syntax to define term
        PriorityTerm.CRITICAL given TimeFunctionTerm.ALL inLast NrqlDurationTerm.DURATION_5 minutes OperatorTerm.BELOW value 0.7f
    }
    
    signal {
        aggregationWindow = 1
        evaluationWindows = 15
        signalFillOption = SignalFillOption.LAST_KNOWN_VALUE
        signalLost {
            signalIsLostAfter = 22
            openNewViolationOnSignalLost = true
            closeCurrentViolationsOnSignalLost = false
        }
    }
}
```

## Synthetics Monitor Failure condition DSL
To create [SyntheticsCondition](../newrelic-alerts-configurator#synthetics-monitor-failure-condition) use `syntheticsCondition` dsl.

```kotlin
val config: SyntheticsCondition = syntheticsCondition {
    conditionName = "MyMonitor failed"
    enabled = true
    monitorName = "MyMonitor"
    runBookUrl = "runBookUrl"
}
```

## User defined configuration DSL
To create [UserDefinedConfiguration](../newrelic-alerts-configurator#user-defined-configuration) use `userDefinedConfiguration` dsl.

```kotlin
val config: UserDefinedConfiguration = userDefinedConfiguration {
    metric = "MY_CUSTOM_METRIC"
    valueFunction = UserDefinedConfiguration.ValueFunction.MAX
}
```

## Term configurations DSL
To create [TermsConfiguration](../newrelic-alerts-configurator#term) use `termsConfiguration` dsl.

```kotlin
val config: TermsConfiguration = termsConfiguration {
    durationTerm = DurationTerm.DURATION_5
    operatorTerm = OperatorTerm.BELOW
    thresholdTerm = 0.8f
    priorityTerm = PriorityTerm.CRITICAL
    timeFunctionTerm = TimeFunctionTerm.ALL
}
```

## NRQL term configuration DSL
To create [NrqlTermsConfiguration](../newrelic-alerts-configurator#nrql-term) use `nrqlTermsConfiguration` dsl.

```kotlin
val config: NrqlTerms = nrqlTermsConfiguration {
    durationTerm = NrqlDurationTerm.DURATION_5
    operatorTerm = OperatorTerm.BELOW
    thresholdTerm = 0.8f
    priorityTerm = PriorityTerm.CRITICAL
    timeFunctionTerm = TimeFunctionTerm.ALL
}
```

## Email channel DSL
To create [EmailChannel](../newrelic-alerts-configurator#email-channel) use `emailChannel` dsl.

```kotlin
val config: EmailChannel = emailChannel {
    channelName = "Channel name"
    emailAddress = "<EMAIL>"
    includeJsonAttachment = true
}
```

## Slack channel DSL
To create [SlackChannel](../newrelic-alerts-configurator#slack-channel) use `slackChannel` dsl.

```kotlin
val config: SlackChannel = slackChannel {
    channelName = "Channel name"
    slackUrl = "https://hooks.slack.com/services/..."
    teamName = "My team slack name"
}
```

## Webhook channel DSL
To create [WebhookChannel](../newrelic-alerts-configurator#webhook-channel) use `webhookChannel` dsl.

```kotlin
val config: WebhookChannel = webhookChannel {
    channelName = "Channel name"
    baseUrl = "https://example.com/webhook/abcd123"
    authUsername = "john.doea"
    authPassword = "<PASSWORD>"
}
```

## PagerDuty channel DSL
To create [PagerDutyChannel](../newrelic-alerts-configurator#pagerduty-channel) use `pagerDutyChannel` dsl.

```kotlin
val config: PagerDutyChannel = pagerDutyChannel {
    channelName = "Channel name"
    serviceKey = "<KEY>"
}
```

## NewRelic user channel DSL
To create [UserChannel](../newrelic-alerts-configurator#newrelic-user-channel) use `userChannel` dsl.

```kotlin
val config: UserChannel = userChannel {
    emailAddress = "<EMAIL>"
}
```<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/internal/entities/ApplicationIdProvider.java
package com.ocadotechnology.newrelic.alertsconfigurator.internal.entities;


import com.ocadotechnology.newrelic.alertsconfigurator.exception.NewRelicSyncException;
import com.ocadotechnology.newrelic.apiclient.NewRelicApi;
import com.ocadotechnology.newrelic.apiclient.model.applications.Application;

import java.util.Optional;

import static java.lang.String.format;

class ApplicationIdProvider implements IdProvider {
    @Override
    public Integer getId(NewRelicApi api, String name) {
        Optional<Application> applicationOptional = api.getApplicationsApi().getByName(name);
        Application application = applicationOptional.orElseThrow(
                () -> new NewRelicSyncException(format("Application %s does not exist", name)));
        return application.getId();
    }
}
<file_sep>/newrelic-alerts-configurator-dsl/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/dsl/configuration/channel/UserChannelDsl.kt
package com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.channel

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.UserChannel
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.NewRelicConfigurationMarker

@NewRelicConfigurationMarker
class UserChannelDsl {
    var userEmail: String? = null
}

fun userChannel(block: UserChannelDsl.() -> Unit): UserChannel {
    val dsl = UserChannelDsl()
    dsl.block()

    return UserChannel.builder()
            .userEmail(requireNotNull(dsl.userEmail) { "User channel name cannot be null" })
            .build()
}<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/condition/ExternalServiceCondition.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition;

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.TermsConfiguration;

import java.util.Collection;

/**
 * Alerts external service condition configuration.
 * Implementations:
 * <ul>
 *     <li>{@link ApmExternalServiceCondition}</li>
 * </ul>
 */
public interface ExternalServiceCondition {
    /**
     * Returns condition type. Each ExternalServiceCondition implementation should have unique type.
     *
     * @return condition type
     */
    ExternalServiceConditionType getType();

    /**
     * Returns condition type in string format.
     *
     * @return condition type
     */
    String getTypeString();

    /**
     * Returns name of the condition.
     *
     * @return condition name
     */
    String getConditionName();

    /**
     * Returns if condition is enabled.
     *
     * @return {@code true} if condition is enabled, {@code false} otherwise
     */
    boolean isEnabled();

    /**
     * Returns collection of entities for which this condition is applied.
     * The type of the entity depends on concrete condition implementation. This can be an application
     * name or host name for example.
     * <p>
     * If entity with given name does not exist an exception will be thrown.
     *
     * @return entity names
     */
    Collection<String> getEntities();

    /**
     * Returns URL of the external service to be monitored. This string must not include the protocol.
     *
     * @return external service URL
     */
    String getExternalServiceUrl();

    /**
     * Returns metric used in given condition.
     *
     * @return metric
     */
    String getMetric();

    /**
     * Returns the runbook URL to display in notifications.
     *
     * @return runbook URL
     */
    String getRunBookUrl();

    /**
     * Returns collection of terms used for alerts condition.
     *
     * @return terms
     */
    Collection<TermsConfiguration> getTerms();
}
<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/condition/signal/ExpirationUtils.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.signal;

import com.ocadotechnology.newrelic.apiclient.model.conditions.Expiration;

public final class ExpirationUtils {

    private ExpirationUtils() {
    }

    public static Expiration createExpiration(SignalLostConfiguration signalLostConfiguration) {
        if (signalLostConfiguration == null) {
            return null;
        }
        return Expiration.builder()
            .expirationDuration(String.valueOf(signalLostConfiguration.getSignalIsLostAfter()))
            .closeViolationsOnExpiration(signalLostConfiguration.isCloseCurrentViolationsOnSignalLost())
            .openViolationOnExpiration(signalLostConfiguration.isOpenNewViolationOnSignalLost())
            .build();
    }
}
<file_sep>/newrelic-api-client/src/test/java/com/ocadotechnology/newrelic/apiclient/NewRelicApiIntegrationTest.java
package com.ocadotechnology.newrelic.apiclient;

import com.github.tomakehurst.wiremock.junit.WireMockClassRule;
import com.ocadotechnology.newrelic.apiclient.model.applications.Application;
import com.ocadotechnology.newrelic.apiclient.model.channels.AlertsChannel;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpHeaders;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.apache.http.HttpStatus.SC_OK;
import static org.assertj.core.api.Assertions.assertThat;

public class NewRelicApiIntegrationTest {
    @ClassRule
    public static final WireMockClassRule WIRE_MOCK = new WireMockClassRule(6766);

    private static final String APPLICATION_JSON = "application/json";
    private static final Charset UTF_8 = Charset.forName("UTF-8");

    private NewRelicApi testee;
    private String applications;
    private String channels1;
    private String channels2;

    @Before
    public void setUp() throws IOException {
        WIRE_MOCK.resetMappings();
        testee = new NewRelicRestApi("http://localhost:6766", "http://localhost:6766", "secret");
        applications = IOUtils.toString(NewRelicApiIntegrationTest.class.getResource("/applications.json"), UTF_8);
        channels1 = IOUtils.toString(NewRelicApiIntegrationTest.class.getResource("/channels1.json"), UTF_8);
        channels2 = IOUtils.toString(NewRelicApiIntegrationTest.class.getResource("/channels2.json"), UTF_8);
    }

    @Test
    public void shouldGetApplicationByNameCorrectly() throws IOException {

        // given
        newRelicReturnsApplications();

        // when
        Optional<Application> app = testee.getApplicationsApi().getByName("application_name");

        // then
        assertThat(app).isNotEmpty();
    }

    @Test
    public void shouldGetPaginatedChannelsCorrectly() {

        // given
        newRelicReturnsPaginatedChannels();

        // when
        List<AlertsChannel> channels = testee.getAlertsChannelsApi().list();

        // then
        Set<Integer> channelsIds = channels.stream().map(AlertsChannel::getId).collect(Collectors.toSet());
        assertThat(channelsIds).containsExactlyInAnyOrder(1, 2);
    }

    private void newRelicReturnsApplications() throws UnsupportedEncodingException {
        String queryParam = URLEncoder.encode("filter[name]", UTF_8.name());

        WIRE_MOCK.addStubMapping(
                get(urlPathEqualTo("/v2/applications.json"))
                        .withQueryParam(queryParam, equalTo("application_name"))
                        .withHeader("X-Api-Key", equalTo("secret"))
                        .willReturn(aResponse()
                                .withStatus(SC_OK)
                                .withHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON)
                                .withBody(applications)
                        ).build());
    }

    private void newRelicReturnsPaginatedChannels() {
        WIRE_MOCK.addStubMapping(
                get(urlPathEqualTo("/v2/alerts_channels.json"))
                        .withHeader("X-Api-Key", equalTo("secret"))
                        .willReturn(aResponse()
                                .withStatus(SC_OK)
                                .withHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON)
                                .withHeader("Link",
                                        "<http://localhost:6766/v2/alerts_channels.json?page=2>; rel=\"next\", " +
                                                "<http://localhost:6766/v2/alerts_channels.json?page=2>; rel=\"last\"")
                                .withBody(channels1)
                        ).build());

        WIRE_MOCK.addStubMapping(
                get(urlPathEqualTo("/v2/alerts_channels.json"))
                        .withQueryParam("page", equalTo(String.valueOf(2)))
                        .withHeader("X-Api-Key", equalTo("secret"))
                        .willReturn(aResponse()
                                .withStatus(SC_OK)
                                .withHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON)
                                .withHeader("Link",
                                        "<http://localhost:6766/v2/alerts_channels.json?page=2>; rel=\"last\"")
                                .withBody(channels2)
                        ).build());
    }
}
<file_sep>/newrelic-api-client/src/test/java/com/ocadotechnology/newrelic/graphql/apiclient/internal/testutil/TestResources.java
package com.ocadotechnology.newrelic.graphql.apiclient.internal.testutil;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.*;

public class TestResources {
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    public static InputStream inputStream(String fileName) {
        return TestResources.class.getResourceAsStream(fileName);
    }

    public static <T> T fromJson(String fileName, Class<T> clazz) {
        try {
            return OBJECT_MAPPER.readValue(new InputStreamReader(inputStream(fileName)), clazz);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static String fromJson(String fileName) {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream(fileName)))) {
            StringBuilder builder = new StringBuilder();
            while (reader.ready()) {
                builder.append(reader.readLine()).append("\n");
            }
            return builder.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
<file_sep>/newrelic-alerts-configurator-dsl/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/dsl/configuration/condition/UserDefinedConfigurationDsl.kt
package com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.condition

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.UserDefinedConfiguration
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.NewRelicConfigurationMarker

@NewRelicConfigurationMarker
class UserDefinedConfigurationDsl {
    var metric: String? = null
    var valueFunction: UserDefinedConfiguration.ValueFunction? = null
}

fun userDefinedConfiguration(block: UserDefinedConfigurationDsl.() -> Unit): UserDefinedConfiguration {
    val dsl = UserDefinedConfigurationDsl()
    dsl.block()

    return UserDefinedConfiguration.builder()
            .metric(requireNotNull(dsl.metric) { "User defined configuration metric cannot be null" })
            .valueFunction(requireNotNull(dsl.valueFunction) { "User defined configuration value cannot be null" })
            .build()
}<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/ApplicationsApi.java
package com.ocadotechnology.newrelic.apiclient;

import com.ocadotechnology.newrelic.apiclient.model.applications.Application;

import java.util.Optional;

public interface ApplicationsApi {
    /**
     * Gets {@link Application} object using its name.
     *
     * @param applicationName name of the application registered in NewRelic
     * @return Optional containing {@link Application} object, or empty if application not found
     */
    Optional<Application> getByName(String applicationName);

    /**
     * Updates {@link Application} object.
     *
     * @param applicationId id of the application to be updated
     * @param application   application definition to be updated
     * @return updated {@link Application}
     */
    Application update(int applicationId, Application application);
}
<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/condition/NrqlCondition.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition;

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.signal.NrqlSignalConfiguration;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.NrqlTermsConfiguration;

import java.util.Collection;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import lombok.Singular;

/**
 * NRQL condition.
 * Configuration parameters:
 * <ul>
 *     <li>{@link #conditionName}</li>
 *     <li>{@link #enabled} (optional)</li>
 *     <li>{@link #runBookUrl} (optional)</li>
 *     <li>{@link #terms}</li>
 *     <li>{@link #valueFunction}</li>
 *     <li>{@link #query}</li>
 *     <li>{@link #signal}</li>
 * </ul>
 */
@Getter
@Builder
public class NrqlCondition {

    public static final int DEFAULT_VIOLATION_TIME_LIMIT_SECONDS = 259_200;

    /**
     * Name of your NRQL condition
     */
    @NonNull
    private String conditionName;
    /**
     * If your NRQL condition is enabled. Default is true
     */
    @Builder.Default
    private boolean enabled = true;

    /**
     * Used to automatically close instance-based incidents after the number of seconds specified.
     * <br>
     * Default is {@link #DEFAULT_VIOLATION_TIME_LIMIT_SECONDS}. Maximum is 30 days.
     * <br>
     * Used for:
     * <li>Location conditions</li>
     * <li>NRQL conditions</li>

     * @see <a href="https://docs.newrelic.com/docs/alerts-applied-intelligence/new-relic-alerts/advanced-alerts/rest-api-alerts/alerts-conditions-api-field-names/#violation_time_limit_seconds">Alerts conditions API field names - violation_time_limit_seconds</a>
     */
    @Builder.Default
    private Integer violationTimeLimitSeconds = DEFAULT_VIOLATION_TIME_LIMIT_SECONDS;
    /**
     * The runbook URL to display in notifications
     */
    private String runBookUrl;
    /**
     * Collection of terms used for alerts condition
     */
    @NonNull
    @Singular
    private Collection<NrqlTermsConfiguration> terms;
    /**
     * Value function
     */
    @NonNull
    private ValueFunction valueFunction;
    /**
     * This is NRQL query that will be executed by the condition.
     */
    @NonNull
    private String query;

    /**
     * NRQL Signal configuration.
     */
    private NrqlSignalConfiguration signal;

    @Getter
    @AllArgsConstructor
    public enum ValueFunction {
        SINGLE_VALUE("single_value"),
        SUM("sum");

        private final String valueString;
    }
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/internal/model/AlertsPolicyWrapper.java
package com.ocadotechnology.newrelic.apiclient.internal.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.ocadotechnology.newrelic.apiclient.model.policies.AlertsPolicy;
import lombok.Value;

@Value
public class AlertsPolicyWrapper {
    @JsonProperty
    AlertsPolicy policy;
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/graphql/apiclient/internal/transport/NotificationChannels.java
package com.ocadotechnology.newrelic.graphql.apiclient.internal.transport;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Value;

import java.util.List;

@JsonIgnoreProperties(ignoreUnknown = true)
@Value
public class NotificationChannels implements Page<List<NotificationChannel>> {
    List<NotificationChannel> channels;
    String nextCursor;

    @Override
    public List<NotificationChannel> getContent() {
        return getChannels();
    }
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/model/policies/AlertsPolicyChannels.java
package com.ocadotechnology.newrelic.apiclient.model.policies;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;

import java.util.Collection;

@Value
@Builder
@AllArgsConstructor
public class AlertsPolicyChannels {
    @JsonProperty("id")
    Integer policyId;
    @JsonProperty("channel_ids")
    Collection<Integer> channelIds;
}
<file_sep>/newrelic-alerts-configurator/src/test/java/com/ocadotechnology/newrelic/alertsconfigurator/ExternalServiceConditionConfiguratorTest.java
package com.ocadotechnology.newrelic.alertsconfigurator;

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.PolicyConfiguration;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.ApmExternalServiceCondition;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.ExternalServiceCondition;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.ExternalServiceConditionType;
import com.ocadotechnology.newrelic.apiclient.PolicyItemApi;
import com.ocadotechnology.newrelic.apiclient.model.conditions.Terms;
import com.ocadotechnology.newrelic.apiclient.model.conditions.external.AlertsExternalServiceCondition;
import org.junit.Before;

import java.util.Collections;
import static org.mockito.Mockito.when;

public class ExternalServiceConditionConfiguratorTest extends AbstractPolicyItemConfiguratorTest<ExternalServiceConditionConfigurator, AlertsExternalServiceCondition> {


    private static final ApmExternalServiceCondition.Metric METRIC = ApmExternalServiceCondition.Metric.RESPONSE_TIME_AVERAGE;
    private static final String EXTERNAL_SERVICE_URL = "externalServiceUrl";
    private static final ExternalServiceCondition EXTERNAL_SERVICE_CONDITION = createCondition(CONDITION_NAME);
    private static final AlertsExternalServiceCondition ALERTS_CONDITION_SAME = createConditionBuilder().id(1).build();
    private static final AlertsExternalServiceCondition ALERTS_CONDITION_FROM_CONFIG = createConditionBuilder().build();
    private static final AlertsExternalServiceCondition ALERTS_CONDITION_UPDATED = createConditionBuilder().id(2).enabled(!ENABLED).build();
    private static final AlertsExternalServiceCondition ALERTS_CONDITION_DIFFERENT = createConditionBuilder().id(3).name("different").build();

    private static final PolicyConfiguration CONFIGURATION = createConfiguration();

    @Before
    public void setUp() {
        super.setUp();
        when(entityResolverMock.resolveEntities(apiMock, EXTERNAL_SERVICE_CONDITION)).thenReturn(Collections.singletonList(APPLICATION_ENTITY_ID));
        testee = new ExternalServiceConditionConfigurator(apiMock, entityResolverMock);
    }

    @Override
    protected PolicyConfiguration getPolicyConfiguration() {
        return CONFIGURATION;
    }

    @Override
    protected PolicyItemApi<AlertsExternalServiceCondition> getPolicyItemApiMock() {
        return alertsExternalServiceConditionsApiMock;
    }

    @Override
    protected AlertsExternalServiceCondition getItemFromConfig() {
        return ALERTS_CONDITION_FROM_CONFIG;
    }

    @Override
    protected AlertsExternalServiceCondition getItemSame() {
        return ALERTS_CONDITION_SAME;
    }

    @Override
    protected AlertsExternalServiceCondition getItemUpdated() {
        return ALERTS_CONDITION_UPDATED;
    }

    @Override
    protected AlertsExternalServiceCondition getItemDifferent() {
        return ALERTS_CONDITION_DIFFERENT;
    }

    private static PolicyConfiguration createConfiguration() {
        return PolicyConfiguration.builder()
            .policyName(POLICY_NAME)
            .incidentPreference(INCIDENT_PREFERENCE)
            .externalServiceCondition(EXTERNAL_SERVICE_CONDITION)
            .build();
    }

    private static ApmExternalServiceCondition createCondition(String conditionName) {
        return ApmExternalServiceCondition.builder()
            .conditionName(conditionName)
            .enabled(ENABLED)
            .application(APPLICATION_NAME)
            .metric(METRIC)
            .externalServiceUrl(EXTERNAL_SERVICE_URL)
            .term(TERMS_CONFIGURATION)
            .build();
    }

    private static AlertsExternalServiceCondition.AlertsExternalServiceConditionBuilder createConditionBuilder() {
        return AlertsExternalServiceCondition.builder()
            .type(ExternalServiceConditionType.APM.getTypeString())
            .name(CONDITION_NAME)
            .enabled(ENABLED)
            .entity(APPLICATION_ENTITY_ID)
            .metric(METRIC.name().toLowerCase())
            .externalServiceUrl(EXTERNAL_SERVICE_URL)
            .term(Terms.builder()
                .duration(String.valueOf(TERMS_CONFIGURATION.getDurationTerm().getDuration()))
                .operator(TERMS_CONFIGURATION.getOperatorTerm().name().toLowerCase())
                .priority(TERMS_CONFIGURATION.getPriorityTerm().name().toLowerCase())
                .threshold(String.valueOf(TERMS_CONFIGURATION.getThresholdTerm()))
                .timeFunction(TERMS_CONFIGURATION.getTimeFunctionTerm().name().toLowerCase())
                .build()
            );
    }
}
<file_sep>/newrelic-alerts-configurator-examples/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/examples/Defaults.kt
package com.ocadotechnology.newrelic.alertsconfigurator.examples

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.ApplicationConfiguration
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.Channel
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.*
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.*
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.violationclosetimer.ViolationCloseTimer
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.applicationConfiguration
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.channel.emailChannel
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.channel.slackChannel
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.condition.*

fun applicationConfiguration(name: String): ApplicationConfiguration {
    return applicationConfiguration {
        applicationName = name
        appApdexThreshold = 0.5f
        endUserApdexThreshold = 0.7f
        enableRealUserMonitoring = true
    }
}

fun apdexCondition(applicationName: String): Condition {
    return apmAppCondition {
        conditionName = "Apdex"
        enabled = true
        applications = listOf(applicationName)
        metric = ApmAppCondition.Metric.APDEX
        conditionScope = ApmAppCondition.ConditionScope.APPLICATION

        terms {
            PriorityTerm.CRITICAL given TimeFunctionTerm.ALL inLast DurationTerm.DURATION_5 minutes OperatorTerm.BELOW value 0.7f
            PriorityTerm.WARNING given TimeFunctionTerm.ALL inLast DurationTerm.DURATION_5 minutes OperatorTerm.BELOW value 0.85f
        }
    }
}

fun diskSpaceCondition(vararg serverNames: String): Condition {
    return serversMetricCondition {
        conditionName = "Fullest Disk %"
        enabled = true
        servers = listOf(*serverNames)
        metric = ServersMetricCondition.Metric.FULLEST_DISK_PERCENTAGE

        terms {
            PriorityTerm.CRITICAL given TimeFunctionTerm.ALL inLast DurationTerm.DURATION_5 minutes OperatorTerm.ABOVE value 80.0f
            PriorityTerm.WARNING given TimeFunctionTerm.ALL inLast DurationTerm.DURATION_5 minutes OperatorTerm.ABOVE value 65.0f
        }
    }
}

fun cpuUsageCondition(vararg serverNames: String): Condition {
    return serversMetricCondition {
        conditionName = "CPU Usage %"
        enabled = true
        servers = listOf(*serverNames)
        metric = ServersMetricCondition.Metric.CPU_PERCENTAGE

        terms {
            PriorityTerm.CRITICAL given TimeFunctionTerm.ALL inLast DurationTerm.DURATION_5 minutes OperatorTerm.ABOVE value 90.0f
            PriorityTerm.WARNING given TimeFunctionTerm.ALL inLast DurationTerm.DURATION_5 minutes OperatorTerm.ABOVE value 70.0f
        }
    }
}

fun heapUsageCondition(applicationName: String): Condition {
    return apmJvmCondition {
        conditionName = "Heap usage"
        enabled = true
        applications = listOf(applicationName)
        metric = ApmJvmCondition.Metric.HEAP_MEMORY_USAGE
        violationCloseTimer = ViolationCloseTimer.DURATION_24

        terms {
            PriorityTerm.CRITICAL given TimeFunctionTerm.ALL inLast DurationTerm.DURATION_5 minutes OperatorTerm.ABOVE value 85.0f
            PriorityTerm.WARNING given TimeFunctionTerm.ALL inLast DurationTerm.DURATION_5 minutes OperatorTerm.ABOVE value 65.0f
        }
    }
}

fun healthCheckCondition(applicationName: String): NrqlCondition {
    return nrqlCondition {
        conditionName = "Health Check"
        enabled = true
        valueFunction = NrqlCondition.ValueFunction.SINGLE_VALUE
        query = "SELECT count(*) FROM `$applicationName:HealthStatus` WHERE healthy IS false"

        terms {
            term {
                priorityTerm = PriorityTerm.CRITICAL
                durationTerm = NrqlDurationTerm.DURATION_1
                timeFunctionTerm = TimeFunctionTerm.ANY
                operatorTerm = OperatorTerm.ABOVE
                thresholdTerm = 0.0f
            }
        }
    }
}

fun jsErrorsCondition(applicationName: String): BrowserCondition {
    return browserCondition {
        conditionName = "Page views with JS errors"
        enabled = true
        applications = listOf(applicationName)
        metric = BrowserCondition.Metric.PAGE_VIEWS_WITH_JS_ERRORS

        terms {
            term {
                priorityTerm = PriorityTerm.WARNING
                durationTerm = DurationTerm.DURATION_5
                timeFunctionTerm = TimeFunctionTerm.ANY
                operatorTerm = OperatorTerm.ABOVE
                thresholdTerm = 1.0f
            }
        }
    }
}

fun teamEmail(): Channel {
    return emailChannel {
        channelName = "My Team - email"
        emailAddress = "<EMAIL>"
        includeJsonAttachment = true
    }
}

fun slack(): Channel {
    return slackChannel {
        channelName = "My Team - slack"
        slackUrl = "https://hooks.slack.com/services/aaaaaaaaa/bbbbbbbbb/cccccccccccccccccccccccc"
        teamChannel = "newrelic-alerts"
    }
}<file_sep>/newrelic-alerts-configurator-dsl/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/dsl/configuration/condition/NrqlConditionConfigurationDsl.kt
package com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.condition

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.signal.NrqlSignalConfiguration
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.signal.SignalAggregationMethod
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.signal.SignalFillOption
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.signal.SignalLostConfiguration
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.NewRelicConfigurationMarker

@NewRelicConfigurationMarker
class NrqlSignalConfigurationDsl {
    var aggregationWindow: Int? = null
    var aggregationMethod: SignalAggregationMethod? = null
    var aggregationDelay: Int? = null
    var aggregationTimer: Int? = null
    var signalFillOption: SignalFillOption? = null
    var signalFillValue: String? = null
    var signalLostConfiguration: SignalLostConfiguration? = null

    fun signalLost(block: SignalLostConfigurationDsl.() -> Unit) {
        signalLostConfiguration = signalLostConfiguration(block)
    }
}

fun nrqlSignalConfiguration(block: NrqlSignalConfigurationDsl.() -> Unit): NrqlSignalConfiguration {
    val dsl = NrqlSignalConfigurationDsl()
    dsl.block()

    val signalConfigurationBuilder = NrqlSignalConfiguration.builder()
            .aggregationMethod(requireNotNull(dsl.aggregationMethod) { "Aggregation method cannot be null" })
            .aggregationWindow(requireNotNull(dsl.aggregationWindow) { "Aggregation window cannot be null" })
            .signalFillOption(requireNotNull(dsl.signalFillOption) { "Signal fill option cannot be null" })
            .signalLostConfiguration(dsl.signalLostConfiguration)

    if (dsl.aggregationMethod == SignalAggregationMethod.EVENT_FLOW || dsl.aggregationMethod == SignalAggregationMethod.CADENCE) {
        signalConfigurationBuilder.aggregationDelay(requireNotNull(dsl.aggregationDelay) { "Aggregation delay cannot be null for selected aggregation method." })
    } else if (dsl.aggregationMethod == SignalAggregationMethod.EVENT_TIMER) {
        signalConfigurationBuilder.aggregationTimer(requireNotNull(dsl.aggregationTimer) { "Aggregation timer cannot be null for selected aggregation method." })
    }

    if (dsl.signalFillOption == SignalFillOption.STATIC) {
        signalConfigurationBuilder.signalFillValue(requireNotNull(dsl.signalFillValue) {"Signal fill value is required when signal fill is STATIC"})
    }
    return signalConfigurationBuilder
        .build()
}
<file_sep>/newrelic-api-client/src/test/java/com/ocadotechnology/newrelic/apiclient/internal/DefaultSyntheticsMonitorsApiTest.java
package com.ocadotechnology.newrelic.apiclient.internal;

import com.ocadotechnology.newrelic.apiclient.internal.client.NewRelicClient;
import com.ocadotechnology.newrelic.apiclient.internal.model.MonitorList;
import com.ocadotechnology.newrelic.apiclient.model.synthetics.Monitor;
import static java.util.Collections.singletonList;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.mockito.junit.MockitoJUnitRunner;

import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;
import java.util.Collections;
import java.util.UUID;

@RunWith(MockitoJUnitRunner.class)
public class DefaultSyntheticsMonitorsApiTest {
    private Response postResponseMock;
    private Response getResponseMock;
    private NewRelicClient clientMock;
    private WebTarget webTargetMock;
    private Invocation.Builder builderMock;
    private DefaultSyntheticsMonitorsApi syntheticsMonitorsApi;

    @Before
    public void setUp() {
        postResponseMock = mock(Response.class);
        getResponseMock = mock(Response.class);
        clientMock = mock(NewRelicClient.class);
        webTargetMock = mock(WebTarget.class);
        builderMock = mock(Invocation.Builder.class);
        syntheticsMonitorsApi = new DefaultSyntheticsMonitorsApi(clientMock);
    }

    @Test
    public void create_shouldCreateANewMonitorAndReturnDetails() throws Exception {
        final Monitor expectedMonitorObject = new Monitor(UUID.randomUUID().toString(), "test-monitor", "SIMPLE", 1, singletonList("AWS_US_WEST_1"), "http://newrelic-url", "ENABLED", 1.0, null, null);;
        final Monitor actualMonitorToBeCreated = new Monitor("", "test-monitor", "SIMPLE", 1, singletonList("AWS_US_WEST_1"), "http://newrelic-url", "ENABLED", 1.0, null, null);
        final MonitorList monitorList = new MonitorList(Collections.singletonList(expectedMonitorObject));

        when(postResponseMock.getStatus()).thenReturn(201);
        when(builderMock.post(Entity.entity(actualMonitorToBeCreated, APPLICATION_JSON_TYPE))).thenReturn(postResponseMock);
        when(getResponseMock.readEntity(MonitorList.class)).thenReturn(monitorList);
        when(builderMock.get()).thenReturn(getResponseMock);
        when(webTargetMock.request(APPLICATION_JSON_TYPE)).thenReturn(builderMock);
        when(webTargetMock.request()).thenReturn(builderMock);
        when(clientMock.target("/v3/monitors")).thenReturn(webTargetMock).thenReturn(webTargetMock);

        final Monitor monitor = syntheticsMonitorsApi.create(actualMonitorToBeCreated);

        assertThat(monitor).isEqualTo(expectedMonitorObject);
    }

    @Test
    public void create_shouldThrowErrorIfFailedToCreateMonitor() {
        final Monitor actualMonitorToBeCreated = new Monitor("", "test-monitor", "SIMPLE", 1, singletonList("AWS_US_WEST_1"), "http://newrelic-url", "ENABLED", 1.0, null, null);

        when(postResponseMock.getStatus()).thenReturn(400);
        when(builderMock.post(Entity.entity(actualMonitorToBeCreated, APPLICATION_JSON_TYPE))).thenReturn(postResponseMock);
        when(webTargetMock.request(APPLICATION_JSON_TYPE)).thenReturn(builderMock);
        when(clientMock.target("/v3/monitors")).thenReturn(webTargetMock);

        assertThatThrownBy(() -> syntheticsMonitorsApi.create(actualMonitorToBeCreated)).isInstanceOf(Exception.class);
    }

    @Test
    public void delete_shouldDeleteTheMonitor() {
        final Monitor actualMonitorToBeDeleted = new Monitor(UUID.randomUUID().toString(), "test-monitor", "SIMPLE", 1, singletonList("AWS_US_WEST_1"), "http://newrelic-url", "ENABLED", 1.0, null, null);
        when(builderMock.delete()).thenReturn(postResponseMock);
        when(webTargetMock.request(APPLICATION_JSON_TYPE)).thenReturn(builderMock);
        when(clientMock.target("/v3/monitors/"+actualMonitorToBeDeleted.getUuid())).thenReturn(webTargetMock);

        final Monitor monitor = syntheticsMonitorsApi.delete(actualMonitorToBeDeleted);

        assertThat(monitor).isEqualTo(actualMonitorToBeDeleted);
    }
}
<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/condition/ConditionType.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum ConditionType {
    APM_APP("apm_app_metric"),
    APM_KEY_TRANSACTION("apm_kt_metric"),
    SERVERS_METRIC("servers_metric"),
    APM_JVM("apm_jvm_metric"),
    BROWSER_METRIC("browser_metric");

    private final String typeString;
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/internal/model/ObjectList.java
package com.ocadotechnology.newrelic.apiclient.internal.model;

import lombok.EqualsAndHashCode;

import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@EqualsAndHashCode(of = "items")
public abstract class ObjectList<T, LT extends ObjectList<T, LT>> {
    private final List<T> items;
    private final Function<List<T>, LT> constructor;

    protected ObjectList(List<T> items, Function<List<T>, LT> constructor) {
        this.items = items;
        this.constructor = constructor;
    }

    public final List<T> getList() {
        return items;
    }

    public final Optional<T> getSingle() {
        List<T> list = getList();
        if (list.isEmpty()) {
            return Optional.empty();
        }
        if (list.size() == 1) {
            return Optional.of(list.get(0));
        }
        throw new IllegalStateException("Expected single element in the list but found: " + list.size());
    }

    public final LT merge(ObjectList<T, LT> list) {
        List<T> mergedItems = Stream.concat(
                items.stream(),
                list.items.stream())
                .collect(Collectors.toList());
        return constructor.apply(mergedItems);
    }

    public final LT filter(Predicate<? super T> predicate) {
        List<T> filteredItems = items.stream().filter(predicate).collect(Collectors.toList());
        return constructor.apply(filteredItems);
    }
}
<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/channel/internal/UserChannelTypeSupport.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.internal;

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.Channel;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.ChannelTypeSupport;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.UserChannel;
import com.ocadotechnology.newrelic.alertsconfigurator.exception.NewRelicSyncException;
import com.ocadotechnology.newrelic.apiclient.NewRelicApi;
import com.ocadotechnology.newrelic.apiclient.model.channels.AlertsChannelConfiguration;
import com.ocadotechnology.newrelic.apiclient.model.users.User;
import lombok.AllArgsConstructor;

import static java.lang.String.format;

@AllArgsConstructor
public class UserChannelTypeSupport implements ChannelTypeSupport {
    private Channel channel;

    @Override
    public AlertsChannelConfiguration generateAlertsChannelConfiguration(NewRelicApi api) {
        UserChannel userChannel = (UserChannel) channel;
        int userId = getUserId(api, userChannel.getUserEmail());
        AlertsChannelConfiguration.AlertsChannelConfigurationBuilder builder = AlertsChannelConfiguration.builder();
        builder.userId(userId);
        return builder.build();
    }

    private static int getUserId(NewRelicApi api, String userEmail) {
        return api.getUsersApi().getByEmail(userEmail)
                .map(User::getId)
                .orElseThrow(() -> new NewRelicSyncException(format("NewRelic user with email %s does not exist", userEmail)));
    }
}
<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/condition/signal/SignalAggregationMethod.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.signal;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum SignalAggregationMethod {
    EVENT_FLOW("EVENT_FLOW"), EVENT_TIMER("EVENT_TIMER"), CADENCE("CADENCE");

    private final String value;
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/internal/client/NewRelicClient.java
package com.ocadotechnology.newrelic.apiclient.internal.client;

import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Link;

public interface NewRelicClient {

    WebTarget target(String path);

    WebTarget target(Link link);
}
<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/NrqlConditionConfigurator.java
package com.ocadotechnology.newrelic.alertsconfigurator;

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.PolicyConfiguration;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.NrqlCondition;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.signal.ExpirationUtils;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.signal.NrqlSignalConfiguration;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.signal.SignalUtils;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.TermsUtils;
import com.ocadotechnology.newrelic.apiclient.NewRelicApi;
import com.ocadotechnology.newrelic.apiclient.PolicyItemApi;
import com.ocadotechnology.newrelic.apiclient.model.conditions.nrql.AlertsNrqlCondition;
import com.ocadotechnology.newrelic.apiclient.model.conditions.nrql.Nrql;
import java.util.Collection;
import java.util.Optional;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

@Slf4j
class NrqlConditionConfigurator extends AbstractPolicyItemConfigurator<AlertsNrqlCondition, NrqlCondition> {

    NrqlConditionConfigurator(@NonNull NewRelicApi api) {
        super(api);
    }

    @Override
    protected Optional<Collection<NrqlCondition>> getConfigItems(PolicyConfiguration config) {
        return config.getNrqlConditions();
    }

    @Override
    protected PolicyItemApi<AlertsNrqlCondition> getItemsApi() {
        return api.getAlertsNrqlConditionsApi();
    }

    @Override
    protected AlertsNrqlCondition convertFromConfigItem(NrqlCondition condition) {
        AlertsNrqlCondition.AlertsNrqlConditionBuilder nrqlConditionBuilder = AlertsNrqlCondition.builder()
            .name(condition.getConditionName())
            .enabled(condition.isEnabled())
            .runbookUrl(condition.getRunBookUrl())
            .terms(TermsUtils.createNrqlTerms(condition.getTerms()))
            .valueFunction(condition.getValueFunction().getValueString())
            .violationTimeLimitSeconds(condition.getViolationTimeLimitSeconds())
            .nrql(Nrql.builder()
                .query(condition.getQuery())
                .build());

        NrqlSignalConfiguration nrqlSignalConfiguration = condition.getSignal();
        if (nrqlSignalConfiguration != null) {
            nrqlConditionBuilder
                .signal(SignalUtils.createSignal(nrqlSignalConfiguration))
                .expiration(ExpirationUtils.createExpiration(nrqlSignalConfiguration.getSignalLostConfiguration()));
        }

        return nrqlConditionBuilder.build();
    }

    @Override
    protected boolean sameInstance(AlertsNrqlCondition alertsNrqlCondition1, AlertsNrqlCondition alertsNrqlCondition2) {
        return StringUtils.equals(alertsNrqlCondition1.getName(), alertsNrqlCondition2.getName());
    }
}
<file_sep>/newrelic-alerts-configurator/src/test/java/com/ocadotechnology/newrelic/alertsconfigurator/internal/entities/EntityResolverTest.java
package com.ocadotechnology.newrelic.alertsconfigurator.internal.entities;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.Arrays;
import java.util.Collection;

import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.Condition;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.ConditionType;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.ExternalServiceCondition;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.ExternalServiceConditionType;
import com.ocadotechnology.newrelic.apiclient.NewRelicApi;

public class EntityResolverTest {
    @Rule
    public final MockitoRule mockito = MockitoJUnit.rule();

    @Mock
    private NewRelicApi apiMock;
    @Mock
    private IdProvider idProviderMock;
    @Spy
    private final EntityResolver testee = new EntityResolver() {
        @Override
        protected IdProvider getIdProvider(String conditionType) {
            return idProviderMock;
        }

        @Override
        protected UuidProvider getUuidProvider(String conditionType) {
            return null;
        }
    };

    @Test
    public void resolveEntities_shouldResolveConditionEntities() {
        // given
        Condition condition = mock(Condition.class);
        when(condition.getType()).thenReturn(ConditionType.APM_KEY_TRANSACTION);
        when(condition.getEntities()).thenReturn(Arrays.asList("e1", "e2"));

        when(idProviderMock.getId(apiMock, "e1")).thenReturn(1);
        when(idProviderMock.getId(apiMock, "e2")).thenReturn(2);

        // when
        Collection<Integer> ids = testee.resolveEntities(apiMock, condition);

        // then
        assertThat(ids).containsExactly(1, 2);
        verify(testee).getIdProvider(ConditionType.APM_KEY_TRANSACTION.getTypeString());
    }

    @Test
    public void resolveEntities_shouldResolveExternalServiceConditionEntities() {
        // given
        ExternalServiceCondition condition = mock(ExternalServiceCondition.class);
        when(condition.getType()).thenReturn(ExternalServiceConditionType.APM);
        when(condition.getEntities()).thenReturn(Arrays.asList("e1", "e2"));

        when(idProviderMock.getId(apiMock, "e1")).thenReturn(1);
        when(idProviderMock.getId(apiMock, "e2")).thenReturn(2);

        // when
        Collection<Integer> ids = testee.resolveEntities(apiMock, condition);

        // then
        assertThat(ids).containsExactly(1, 2);
        verify(testee).getIdProvider(ExternalServiceConditionType.APM.getTypeString());
    }

    @Test
    public void defaultInstance_shouldSupportAllConditionEntities() {
        for (ConditionType type : ConditionType.values()) {
            assertCanResolveType(type.getTypeString());
        }
    }

    @Test
    public void defaultInstance_shouldSupportAllExternalServiceCondtionEntities() {
        for (ExternalServiceConditionType type : ExternalServiceConditionType.values()) {
            assertCanResolveType(type.getTypeString());
        }
    }

    private void assertCanResolveType(String conditionType) {
        IdProvider provider = EntityResolver.defaultInstance().getIdProvider(conditionType);
        assertNotNull("Should resolve id for " + conditionType, provider);
    }
}
<file_sep>/newrelic-alerts-configurator-examples/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/examples/ApplicationConfigurator.java
package com.ocadotechnology.newrelic.alertsconfigurator.examples;

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.ApplicationConfiguration;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.PolicyConfiguration;

/**
 * Returns configuration for one application in NewRelic. In this sample, each application will have one application
 * configuration and one policy configuration
 */
public interface ApplicationConfigurator {

    ApplicationConfiguration getApplicationConfiguration();

    PolicyConfiguration getPolicyConfigurations();
}
<file_sep>/newrelic-api-client/src/test/java/com/ocadotechnology/newrelic/graphql/apiclient/internal/testutil/QueryExecutorMock.java
package com.ocadotechnology.newrelic.graphql.apiclient.internal.testutil;

import com.ocadotechnology.newrelic.graphql.apiclient.internal.QueryExecutor;
import com.ocadotechnology.newrelic.graphql.apiclient.internal.transport.Query;
import com.ocadotechnology.newrelic.graphql.apiclient.internal.transport.Result;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import static java.lang.String.format;

public class QueryExecutorMock implements QueryExecutor {
    private final Map<Query, Result> resultsByQuery = new HashMap<>();

    @Override
    public Result execute(Query query) {
        return Optional.ofNullable(resultsByQuery.get(query))
                .orElseThrow(() -> new UnsupportedOperationException(format("No scenario found for query:'\n%s\n'", query)));
    }


    public QueryExecutorMock addScenario(Query query, Result result) {
        resultsByQuery.put(query, result);
        return this;
    }
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/internal/DefaultDeploymentsApi.java
package com.ocadotechnology.newrelic.apiclient.internal;

import com.ocadotechnology.newrelic.apiclient.DeploymentsApi;
import com.ocadotechnology.newrelic.apiclient.internal.client.NewRelicClient;
import com.ocadotechnology.newrelic.apiclient.internal.model.DeploymentWrapper;
import com.ocadotechnology.newrelic.apiclient.internal.model.DeploymentsList;
import com.ocadotechnology.newrelic.apiclient.model.deployments.Deployment;

import javax.ws.rs.client.Entity;
import java.util.List;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;

public class DefaultDeploymentsApi extends ApiBase implements DeploymentsApi {

    private static final String DEPLOYMENTS_URL = "/v2/applications/{application_id}/deployments.json";
    private static final String DEPLOYMENT_URL = "/v2/applications/{application_id}/deployments/{deployment_id}.json";

    public DefaultDeploymentsApi(NewRelicClient client) {
        super(client);
    }

    @Override
    public List<Deployment> list(int applicationId) {
        return getPageable(
                client.target(DEPLOYMENTS_URL)
                        .resolveTemplate("application_id", applicationId)
                        .request(APPLICATION_JSON_TYPE),
                DeploymentsList.class)
                .getList();
    }

    @Override
    public Deployment create(int applicationId, Deployment deployment) {
        return client
                .target(DEPLOYMENTS_URL)
                .resolveTemplate("application_id", applicationId)
                .request(APPLICATION_JSON_TYPE)
                .post(Entity.entity(new DeploymentWrapper(deployment), APPLICATION_JSON_TYPE), DeploymentWrapper.class)
                .getDeployment();
    }

    @Override
    public Deployment delete(int applicationId, int deploymentId) {
        return client
                .target(DEPLOYMENT_URL)
                .resolveTemplate("application_id", applicationId)
                .resolveTemplate("deployment_id", deploymentId)
                .request(APPLICATION_JSON_TYPE)
                .delete(DeploymentWrapper.class)
                .getDeployment();
    }
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/graphql/apiclient/internal/transport/AssociatedPolicies.java
package com.ocadotechnology.newrelic.graphql.apiclient.internal.transport;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Value;

import java.util.List;

@JsonIgnoreProperties(ignoreUnknown = true)
@Value
public class AssociatedPolicies {
    List<Identifiable> policies;
}
<file_sep>/newrelic-alerts-configurator-dsl/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/dsl/configuration/condition/SyntheticsConditionDsl.kt
package com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.condition

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.SyntheticsCondition
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.NewRelicConfigurationMarker

@NewRelicConfigurationMarker
class SyntheticsConditionDsl {
    var conditionName: String? = null
    var monitorName: String? = null
    var enabled: Boolean = true
    var runBookUrl: String? = null
}

fun syntheticsCondition(block: SyntheticsConditionDsl.() -> Unit): SyntheticsCondition {
    val dsl = SyntheticsConditionDsl()
    dsl.block()

    return SyntheticsCondition.builder()
            .conditionName(requireNotNull(dsl.conditionName) { "Synthetics condition name cannot be null" })
            .monitorName(requireNotNull(dsl.monitorName) { "Synthetics condition monitor name cannot be null" })
            .enabled(dsl.enabled)
            .runBookUrl(dsl.runBookUrl)
            .build()
}<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/internal/model/AlertsConditionList.java
package com.ocadotechnology.newrelic.apiclient.internal.model;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.ocadotechnology.newrelic.apiclient.model.conditions.AlertsCondition;
import lombok.EqualsAndHashCode;

import java.util.List;

@EqualsAndHashCode(callSuper = true)
public class AlertsConditionList extends ObjectList<AlertsCondition, AlertsConditionList> {
    @JsonCreator
    public AlertsConditionList(@JsonProperty("conditions") List<AlertsCondition> items) {
        super(items, AlertsConditionList::new);
    }
}
<file_sep>/newrelic-alerts-configurator-dsl/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/dsl/configuration/condition/SignalLostConfigurationDsl.kt
package com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.condition

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.signal.SignalLostConfiguration
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.NewRelicConfigurationMarker

@NewRelicConfigurationMarker
class SignalLostConfigurationDsl {
    var signalIsLostAfter: Int? = null
    var openNewViolationOnSignalLost: Boolean = false
    var closeCurrentViolationsOnSignalLost: Boolean = false
}

fun signalLostConfiguration(block: SignalLostConfigurationDsl.() -> Unit): SignalLostConfiguration {
    val dsl = SignalLostConfigurationDsl()
    dsl.block()

    return SignalLostConfiguration.builder()
        .signalIsLostAfter(requireNotNull(dsl.signalIsLostAfter, { "Signal is lost after cannot be null" }))
        .openNewViolationOnSignalLost(dsl.openNewViolationOnSignalLost)
        .closeCurrentViolationsOnSignalLost(dsl.closeCurrentViolationsOnSignalLost)
        .build()
}<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/model/dashboards/widget/WidgetLayout.java
package com.ocadotechnology.newrelic.apiclient.model.dashboards.widget;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;

@Value
@Builder(toBuilder = true)
@AllArgsConstructor
public class WidgetLayout {
    @JsonProperty
    Integer width;
    @JsonProperty
    Integer height;
    @JsonProperty
    Integer row;
    @JsonProperty
    Integer column;
}
<file_sep>/build.gradle
plugins {
    id 'org.jetbrains.kotlin.jvm' version '1.3.72' apply false
}

allprojects {
    group = 'com.ocadotechnology.newrelicalertsconfigurator'

    apply plugin: 'java-library'
    apply plugin: 'maven'
    apply plugin: 'signing'

    sourceCompatibility = 1.8
    targetCompatibility = 1.8
    compileJava.options.encoding = 'UTF-8'
}

subprojects {
    repositories {
        mavenCentral()
    }

    dependencies {
        api 'org.slf4j:slf4j-api:1.7.30'

        compileOnly 'org.projectlombok:lombok:1.18.12'
        annotationProcessor 'org.projectlombok:lombok:1.18.12'

        testImplementation 'junit:junit:4.13'
        testImplementation 'com.google.guava:guava:29.0-jre'
        testImplementation 'org.mockito:mockito-core:3.4.6'
        testImplementation 'com.github.tomakehurst:wiremock:2.27.1'
        testImplementation 'org.assertj:assertj-core:3.16.1'
    }
}

configure(subprojects - project(':newrelic-alerts-configurator-examples')) {
    task sourcesJar(type: Jar, dependsOn: classes) {
        archiveClassifier.set('sources')
        from sourceSets.main.allSource
    }

    task javadocJar(type: Jar, dependsOn: javadoc) {
        archiveClassifier.set('javadoc')
        from javadoc.destinationDir
    }

    artifacts {
        archives javadocJar, sourcesJar
    }

    if (project.hasProperty('release')) {
        signing {
            required { !version.endsWith("-SNAPSHOT") && gradle.taskGraph.hasTask("uploadArchives") }
            useInMemoryPgpKeys(findProperty("signingKey").toString().split("(?<=\\G.{64})").join("\n"), findProperty("signingPassword").toString())
            sign configurations.archives
        }

        uploadArchives {
            doFirst {
                println "Uploading archive ${project.name} in version ${version}"
            }

            repositories {
                mavenDeployer {

                    beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }

                    repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
                        authentication(userName: ossrhUsername, password: <PASSWORD>)
                    }

                    snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") {
                        authentication(userName: ossrhUsername, password: <PASSWORD>)
                    }

                    pom.project {
                        name 'NewRelic Alerts Configurator'
                        packaging 'jar'
                        // optionally artifactId can be defined here
                        description 'NewRelic Alerts Configurator can be used to configure NewRelic alerts for your application. Instead of defining alerts through UI you can define them in code. It allows you to automatize alerts configuration, easily recover them in case of wipe out and have full history of changes in your version control system.'
                        url 'https://github.com/ocadotechnology/newrelic-alerts-configurator'

                        scm {
                            connection 'scm:git:git://github.com/ocadotechnology/newrelic-alerts-configurator.git'
                            developerConnection 'scm:git:git@github.com:ocadotechnology/newrelic-alerts-configurator.git'
                            url 'https://github.com/ocadotechnology/newrelic-alerts-configurator'
                        }

                        licenses {
                            license {
                                name 'The Apache License, Version 2.0'
                                url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                            }
                        }
                        organization {
                            name 'Ocado Technology'
                            url 'http://www.ocadotechnology.com/'
                        }
                        developers {
                            developer {
                                id 'panda.team'
                                name '<NAME>'
                                email '<EMAIL>'
                            }
                        }
                    }
                }
            }
        }
    }
}

project(':newrelic-api-client') {
    dependencies {
        api 'commons-io:commons-io:2.7'
        api 'org.apache.commons:commons-lang3:3.11'
        api 'org.glassfish.jersey.core:jersey-client:2.24.1'
        api 'org.glassfish.jersey.media:jersey-media-json-jackson:2.24.1'
        api 'org.glassfish.jersey.connectors:jersey-apache-connector:2.24.1'
        api 'com.fasterxml.jackson.core:jackson-databind:2.11.1'
        api 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.11.1'
        api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.11.1'
        implementation 'org.apache.commons:commons-text:1.10.0'
        implementation('com.github.k0kubun:graphql-query-builder:0.4.2') {
            exclude group: 'com.google.guava', module: 'guava'
        }

    }
}

project(':newrelic-alerts-configurator') {
    dependencies {
        api project(':newrelic-api-client')
    }
}

project(':newrelic-alerts-configurator-examples') {
    apply plugin: 'kotlin'

    dependencies {
        api project(':newrelic-alerts-configurator')
        api project(':newrelic-alerts-configurator-dsl')
    }
}

project(':newrelic-alerts-configurator-dsl') {
    apply plugin: 'kotlin'

    dependencies {
        api project(':newrelic-alerts-configurator')
        api 'org.jetbrains.kotlin:kotlin-stdlib'
    }
}
<file_sep>/newrelic-alerts-configurator-dsl/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/dsl/configuration/channel/Channels.kt
package com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.channel

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.Channel
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.NewRelicConfigurationMarker

@NewRelicConfigurationMarker
class Channels {
    internal val channels: MutableList<Channel> = mutableListOf()

    fun email(block: EmailChannelDsl.() -> Unit) = channels.add(emailChannel(block))
    fun pagerDuty(block: PagerDutyChannelDsl.() -> Unit) = channels.add(pagerDutyChannel(block))
    fun slack(block: SlackChannelDsl.() -> Unit) = channels.add(slackChannel(block))
    fun user(block: UserChannelDsl.() -> Unit) = channels.add(userChannel(block))
    fun webhook(block: WebhookChannelDsl.() -> Unit) = channels.add(webhookChannel(block))
    operator fun Channel.unaryPlus() = channels.add(this)
}<file_sep>/newrelic-alerts-configurator/src/test/java/com/ocadotechnology/newrelic/alertsconfigurator/ConfiguratorTest.java
package com.ocadotechnology.newrelic.alertsconfigurator;

import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;

import com.ocadotechnology.newrelic.apiclient.NewRelicApi;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import com.google.common.collect.ImmutableList;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.ApplicationConfiguration;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.PolicyConfiguration;

@RunWith(MockitoJUnitRunner.class)
public class ConfiguratorTest {
    @Rule
    public final ExpectedException expectedException = ExpectedException.none();

    @Mock
    private ApplicationConfigurator applicationConfiguratorMock;
    @Mock
    private PolicyConfigurator policyConfiguratorMock;
    @Mock
    private ConditionConfigurator conditionConfiguratorMock;
    @Mock
    private ExternalServiceConditionConfigurator externalServiceConditionConfiguratorMock;
    @Mock
    private NrqlConditionConfigurator nrqlConditionConfiguratorMock;
    @Mock
    private SyntheticsConditionConfigurator syntheticsConditionConfiguratorMock;
    @Mock
    private ChannelConfigurator channelConfiguratorMock;

    @Mock
    private ApplicationConfiguration applicationConfigurationMock;
    @Mock
    private PolicyConfiguration policyConfigurationMock;


    private Configurator testee;

    @Before
    public void setUp() {
        testee = new Configurator(applicationConfiguratorMock,
            policyConfiguratorMock,
            conditionConfiguratorMock,
            externalServiceConditionConfiguratorMock,
            nrqlConditionConfiguratorMock,
            syntheticsConditionConfiguratorMock,
            channelConfiguratorMock);
    }


    @Test
    public void shouldThrowException_whenNoApiKey() {
        // given

        // then - exception
        expectedException.expect(NullPointerException.class);
        expectedException.expectMessage("apiKey");

        // when
        new Configurator((String) null);
    }

    @Test
    public void shouldThrowException_whenNoApi() {
        // given

        // then - exception
        expectedException.expect(NullPointerException.class);
        expectedException.expectMessage("api");

        // when
        new Configurator((NewRelicApi) null);
    }

    @Test
    public void shouldNotSynchronizeAnything_whenNoConfigurationsSet() {
        // given

        // when
        testee.sync();

        // then
        InOrder order = inOrder(applicationConfiguratorMock,
                policyConfiguratorMock,
                conditionConfiguratorMock,
                externalServiceConditionConfiguratorMock,
                nrqlConditionConfiguratorMock,
                syntheticsConditionConfiguratorMock,
                channelConfiguratorMock);
        order.verifyNoMoreInteractions();
    }

    @Test
    public void shouldSynchronizeMoreThanOneConfigurations_whenMoreThanOneConfigurationsSet() {
        // given
        ApplicationConfiguration applicationConfigurationMock2 = mock(ApplicationConfiguration.class);
        PolicyConfiguration policyConfigurationMock2 = mock(PolicyConfiguration.class);
        testee.setApplicationConfigurations(ImmutableList.of(applicationConfigurationMock, applicationConfigurationMock2));
        testee.setPolicyConfigurations(ImmutableList.of(policyConfigurationMock, policyConfigurationMock2));

        // when
        testee.sync();

        // then
        InOrder order = inOrder(applicationConfiguratorMock,
                policyConfiguratorMock,
                conditionConfiguratorMock,
                externalServiceConditionConfiguratorMock,
                nrqlConditionConfiguratorMock,
                syntheticsConditionConfiguratorMock,
                channelConfiguratorMock);
        order.verify(applicationConfiguratorMock).sync(applicationConfigurationMock);
        order.verify(applicationConfiguratorMock).sync(applicationConfigurationMock2);
        order.verify(policyConfiguratorMock).sync(policyConfigurationMock);
        order.verify(policyConfiguratorMock).sync(policyConfigurationMock2);
    }

    @Test
    public void shouldSynchronizeAllConfigurationsForPolicy_whenFullPolicyConfiguration() {
        // given
        testee.setPolicyConfigurations(ImmutableList.of(policyConfigurationMock));

        // when
        testee.sync();

        // then
        InOrder order = inOrder(applicationConfiguratorMock,
                policyConfiguratorMock,
                conditionConfiguratorMock,
                externalServiceConditionConfiguratorMock,
                nrqlConditionConfiguratorMock,
                syntheticsConditionConfiguratorMock,
                channelConfiguratorMock);
        order.verify(policyConfiguratorMock).sync(policyConfigurationMock);
        order.verify(conditionConfiguratorMock).sync(policyConfigurationMock);
        order.verify(externalServiceConditionConfiguratorMock).sync(policyConfigurationMock);
        order.verify(nrqlConditionConfiguratorMock).sync(policyConfigurationMock);
        order.verify(syntheticsConditionConfiguratorMock).sync(policyConfigurationMock);
        order.verify(channelConfiguratorMock).sync(policyConfigurationMock);
        order.verifyNoMoreInteractions();
    }
}
<file_sep>/newrelic-api-client/README.md
New Relic API wrapper.

See https://rpm.newrelic.com/api/explore<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/channel/internal/EmailChannelTypeSupport.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.internal;

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.Channel;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.ChannelTypeSupport;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.EmailChannel;
import com.ocadotechnology.newrelic.apiclient.NewRelicApi;
import com.ocadotechnology.newrelic.apiclient.model.channels.AlertsChannelConfiguration;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.BooleanUtils;

@AllArgsConstructor
public class EmailChannelTypeSupport implements ChannelTypeSupport {
    private Channel channel;

    @Override
    public AlertsChannelConfiguration generateAlertsChannelConfiguration(NewRelicApi api) {
        EmailChannel emailChannel = (EmailChannel) channel;
        AlertsChannelConfiguration.AlertsChannelConfigurationBuilder builder = AlertsChannelConfiguration.builder();
        builder.recipients(emailChannel.getEmailAddress());
        if (BooleanUtils.isTrue(emailChannel.getIncludeJsonAttachment())) {
            builder.includeJsonAttachment(emailChannel.getIncludeJsonAttachment());
        }
        return builder.build();
    }
}
<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/internal/entities/SyntheticsMonitorUuidProvider.java
package com.ocadotechnology.newrelic.alertsconfigurator.internal.entities;

import static java.lang.String.format;

import java.util.Optional;

import com.ocadotechnology.newrelic.alertsconfigurator.exception.NewRelicSyncException;
import com.ocadotechnology.newrelic.apiclient.NewRelicApi;
import com.ocadotechnology.newrelic.apiclient.model.synthetics.Monitor;

class SyntheticsMonitorUuidProvider implements UuidProvider {

    @Override
    public String getUuid(NewRelicApi api, String name) {
        Optional<Monitor> monitorOptional = api.getSyntheticsMonitorsApi().getByName(name);
        Monitor monitor = monitorOptional.orElseThrow(
                () -> new NewRelicSyncException(format("Synthetics Monitor %s does not exist", name)));
        return monitor.getUuid();
    }
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/model/transactions/KeyTransactionLinks.java
package com.ocadotechnology.newrelic.apiclient.model.transactions;


import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;

@Value
public class KeyTransactionLinks {
    @JsonProperty
    Integer application;
}
<file_sep>/newrelic-alerts-configurator-dsl/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/dsl/configuration/condition/terms/TermsConfigurationDsl.kt
package com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.condition.terms

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.*
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.NewRelicConfigurationMarker

@NewRelicConfigurationMarker
class TermsConfigurationDsl {
    var durationTerm: DurationTerm? = null
    var operatorTerm: OperatorTerm? = null
    var priorityTerm: PriorityTerm? = null
    var thresholdTerm: Float? = null
    var timeFunctionTerm: TimeFunctionTerm? = null
}

fun termsConfiguration(block: TermsConfigurationDsl.() -> Unit): TermsConfiguration {
    val dsl = TermsConfigurationDsl()
    dsl.block()

    return TermsConfiguration.builder()
            .durationTerm(requireNotNull(dsl.durationTerm) { "Terms duration cannot be null" })
            .operatorTerm(requireNotNull(dsl.operatorTerm) { "Terms operator cannot be null" })
            .priorityTerm(requireNotNull(dsl.priorityTerm) { "Terms priority cannot be null" })
            .thresholdTerm(requireNotNull(dsl.thresholdTerm) { "Terms threshold cannot be null" })
            .timeFunctionTerm(requireNotNull(dsl.timeFunctionTerm) { "Terms time cannot be null" })
            .build()
}

@NewRelicConfigurationMarker
class TermConfigurations {
    internal val terms: MutableList<TermsConfiguration> = mutableListOf()

    fun term(block: TermsConfigurationDsl.() -> Unit) = terms.add(com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.condition.terms.termsConfiguration(block))
    operator fun TermsConfiguration.unaryPlus() = terms.add(this)

    infix fun PriorityTerm.given(timeFunctionTerm: TimeFunctionTerm) : AfterGiven {
        return AfterGiven(TermsConfiguration.builder().priorityTerm(this).timeFunctionTerm(timeFunctionTerm))
    }

    infix fun AfterGiven.inLast(durationTerm: DurationTerm) : AfterInLast {
        return AfterInLast(this.builder.durationTerm(durationTerm))
    }

    infix fun AfterInLast.minutes(operatorTerm: OperatorTerm) : AfterMinutes {
        return AfterMinutes(this.builder.operatorTerm(operatorTerm))
    }

    infix fun AfterMinutes.value(thresholdTerm: Float) {
        terms.add(this.builder.thresholdTerm(thresholdTerm).build())
    }

    class AfterGiven(internal val builder: TermsConfiguration.TermsConfigurationBuilder)
    class AfterInLast(internal val builder: TermsConfiguration.TermsConfigurationBuilder)
    class AfterMinutes(internal val builder: TermsConfiguration.TermsConfigurationBuilder)
}<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/DashboardsApi.java
package com.ocadotechnology.newrelic.apiclient;

import com.ocadotechnology.newrelic.apiclient.model.dashboards.Dashboard;

import java.util.List;

/**
 * See <a href="https://docs.newrelic.com/docs/insights/insights-api/manage-dashboards/insights-dashboard-api">Doc</a>
 */
public interface DashboardsApi {

    /**
     * View a list of {@link Dashboard}. The list shows dashboard metadata only; no widgets will appear in the list.
     *
     * @param dashboardTitle name of the dashboard(s) registered in NewRelic. This may be a partial name.
     * @return List containing {@link Dashboard} objects, or empty if no dashboard including dashboardTitle exist.
     * This
     */
    List<Dashboard> getByTitle(String dashboardTitle);

    /**
     * View an existing {@link Dashboard} and all accessible widgets for the dashboard id.
     *
     * @param dashboardId id of the dashboard registered in NewRelic
     * @return found {@link Dashboard}
     */
    Dashboard getById(int dashboardId);

    /**
     * Create a new {@link Dashboard}.
     * <p>
     * The API permits a maximum of 300 widgets when creating or updating a dashboard. Attempting to POST more than 300 widgets will produce an error.
     *
     * @param dashboard to be created in NewRelic
     * @return created {@link Dashboard}
     */
    Dashboard create(Dashboard dashboard);

    /**
     * Update an existing {@link Dashboard} for the dashboard id.
     * <p>
     * The API permits a maximum of 300 widgets when creating or updating a dashboard. Attempting to PUT more than 300 widgets will produce an error.
     *
     * @param dashboard to be updated in NewRelic
     * @return updated {@link Dashboard}
     */
    Dashboard update(Dashboard dashboard);

    /**
     * Delete an existing {@link Dashboard} indicated by the dashboard id.
     *
     * @param dashboardId of the dashboard to be deleted
     * @return deleted {@link Dashboard}
     */
    Dashboard delete(int dashboardId);

}
<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/channel/internal/WebhookChannelTypeSupport.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.internal;

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.Channel;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.ChannelTypeSupport;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.WebhookChannel;
import com.ocadotechnology.newrelic.apiclient.NewRelicApi;
import com.ocadotechnology.newrelic.apiclient.model.channels.AlertsChannelConfiguration;
import lombok.AllArgsConstructor;

@AllArgsConstructor
public class WebhookChannelTypeSupport implements ChannelTypeSupport {
    private Channel channel;

    @Override
    public AlertsChannelConfiguration generateAlertsChannelConfiguration(NewRelicApi api) {
        WebhookChannel webhookChannel = (WebhookChannel) channel;
        return AlertsChannelConfiguration.builder()
                .baseUrl(webhookChannel.getBaseUrl())
                .authUsername(webhookChannel.getAuthUsername())
                .authPassword(webhookChannel.getAuthPassword())
                .build();
    }
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/graphql/apiclient/internal/transport/mutation/MutationResult.java
package com.ocadotechnology.newrelic.graphql.apiclient.internal.transport.mutation;

import java.util.List;
import java.util.Map;

import static java.util.Objects.nonNull;

public interface MutationResult<T> {
    T get();

    List<Map<String, Object>> getErrors();

    default boolean hasErrors() {
        return nonNull(getErrors()) && !getErrors().isEmpty();
    }
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/KeyTransactionsApi.java
package com.ocadotechnology.newrelic.apiclient;

import com.ocadotechnology.newrelic.apiclient.model.transactions.KeyTransaction;

import java.util.Optional;

public interface KeyTransactionsApi {
    /**
     * Gets {@link KeyTransaction} object using its name.
     *
     * @param keyTransactionName name of the key transaction registered in NewRelic
     * @return Optional containing {@link KeyTransaction} object, or empty if transaction not found
     */
    Optional<KeyTransaction> getByName(String keyTransactionName);

    /**
     * Gets {@link KeyTransaction} object using its name.
     *
     * @param keyTransactionId id of the key transaction registered in NewRelic
     * @return found {@link KeyTransaction}
     */
    KeyTransaction getById(int keyTransactionId);
}
<file_sep>/newrelic-alerts-configurator-dsl/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/dsl/Configurator.kt
package com.ocadotechnology.newrelic.alertsconfigurator.dsl

import com.ocadotechnology.newrelic.alertsconfigurator.Configurator
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.ApplicationConfiguration
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.PolicyConfiguration
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.ApplicationConfigurations
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.PolicyConfigurations

@NewRelicConfigurationMarker
class ConfiguratorDsl {
    internal val applicationConfigurations: MutableList<ApplicationConfiguration> = mutableListOf()
    internal val policyConfigurations: MutableList<PolicyConfiguration> = mutableListOf()

    fun applications(block: ApplicationConfigurations.() -> Unit) = applicationConfigurations.addAll(ApplicationConfigurations().apply(block).applications)
    fun policies(block: PolicyConfigurations.() -> Unit) = policyConfigurations.addAll(PolicyConfigurations().apply(block).policies)
}

fun configuration(apiKey: String, block: ConfiguratorDsl.() -> Unit) {
    val dsl = ConfiguratorDsl()
    dsl.apply(block)

    val configurator = Configurator(apiKey)
    configurator.setApplicationConfigurations(dsl.applicationConfigurations)
    configurator.setPolicyConfigurations(dsl.policyConfigurations)
    configurator.sync()
}<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/model/applications/ApplicationLinks.java
package com.ocadotechnology.newrelic.apiclient.model.applications;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;

import java.util.List;

@Value
public class ApplicationLinks {
    @JsonProperty("alert_policy")
    Integer alertPolicy;
    @JsonProperty
    List<Integer> servers;
    @JsonProperty("application_hosts")
    List<Integer> hosts;
    @JsonProperty("application_instances")
    List<Integer> instances;
}
<file_sep>/newrelic-alerts-configurator-dsl/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/dsl/configuration/condition/ApmExternalServiceConditionDsl.kt
package com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.condition

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.ApmExternalServiceCondition
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.TermsConfiguration
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.NewRelicConfigurationMarker
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.condition.terms.TermConfigurations

@NewRelicConfigurationMarker
class ApmExternalServiceConditionDsl {
    var conditionName: String? = null
    var enabled: Boolean = true
    var applications: Collection<String> = mutableListOf()
    var externalServiceUrl: String? = null
    var metric: ApmExternalServiceCondition.Metric? = null
    var runBookUrl: String? = null
    internal val terms: MutableList<TermsConfiguration> = mutableListOf()

    fun terms(block: TermConfigurations.() -> Unit) = terms.addAll(TermConfigurations().apply(block).terms)
}

fun apmExternalServiceCondition(block: ApmExternalServiceConditionDsl.() -> Unit): ApmExternalServiceCondition {
    val dsl = ApmExternalServiceConditionDsl()
    dsl.block()

    return ApmExternalServiceCondition.builder()
            .conditionName(requireNotNull(dsl.conditionName) { "Apm external service condition name cannot be null" })
            .enabled(dsl.enabled)
            .applications(dsl.applications)
            .externalServiceUrl(requireNotNull(dsl.externalServiceUrl) { "Apm external service condition url cannot be null" })
            .metric(requireNotNull(dsl.metric) { "Apm external service condition metric cannot be null" })
            .runBookUrl(dsl.runBookUrl)
            .terms(dsl.terms)
            .build()
}<file_sep>/newrelic-alerts-configurator-dsl/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/dsl/configuration/channel/WebhookChannelDsl.kt
package com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.channel

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.WebhookChannel
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.NewRelicConfigurationMarker

@NewRelicConfigurationMarker
class WebhookChannelDsl {
    var channelName: String? = null
    var baseUrl: String? = null
    val authUsername: String? = null
    val authPassword: String? = null
}

fun webhookChannel(block: WebhookChannelDsl.() -> Unit): WebhookChannel {
    val dsl = WebhookChannelDsl()
    dsl.block()

    return WebhookChannel.builder()
            .channelName(requireNotNull(dsl.channelName) { "Webhook channel name cannot be null" })
            .baseUrl(requireNotNull(dsl.baseUrl) { "Webhook channel url cannot be null" })
            .authUsername(dsl.authUsername)
            .authPassword(<PASSWORD>)
            .build()
}<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/graphql/apiclient/internal/transport/mutation/EmptyMutationResult.java
package com.ocadotechnology.newrelic.graphql.apiclient.internal.transport.mutation;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Getter;
import lombok.Value;

import java.util.List;
import java.util.Map;
import java.util.Optional;

import static java.util.Collections.singletonList;
import static lombok.AccessLevel.NONE;

@JsonIgnoreProperties(ignoreUnknown = true)
@Value
@Getter(NONE)
public class EmptyMutationResult implements MutationResult<Void> {
    Map<String, Object> error;

    @Override
    public Void get() {
        return null;
    }

    @Override
    public List<Map<String, Object>> getErrors() {
        return Optional.ofNullable(error).map(e -> singletonList(error)).orElse(null);
    }
}
<file_sep>/newrelic-alerts-configurator-dsl/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/dsl/configuration/channel/EmailChannelDsl.kt
package com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.channel

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.EmailChannel
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.NewRelicConfigurationMarker

@NewRelicConfigurationMarker
class EmailChannelDsl {
    var channelName: String? = null
    var emailAddress: String? = null
    var includeJsonAttachment: Boolean = false
}

fun emailChannel(block: EmailChannelDsl.() -> Unit): EmailChannel {
    val dsl = EmailChannelDsl()
    dsl.block()

    return EmailChannel.builder()
            .channelName(requireNotNull(dsl.channelName) { "Email channel name cannot be null" })
            .emailAddress(requireNotNull(dsl.emailAddress) { "Email channel address cannot be null" })
            .includeJsonAttachment(dsl.includeJsonAttachment)
            .build()
}<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/graphql/apiclient/internal/transport/PageIterator.java
package com.ocadotechnology.newrelic.graphql.apiclient.internal.transport;

import lombok.RequiredArgsConstructor;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;

import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;

@RequiredArgsConstructor
public class PageIterator<C, W extends Page<List<C>>> implements Iterator<List<C>> {
    private W current;

    private final Function<String, Optional<W>> fetch;

    @Override
    public boolean hasNext() {
        return isNull(current) || nonNull(current.getNextCursor());
    }

    @Override
    public List<C> next() {
        if (hasNext()) {
            current = fetch.apply(Optional.ofNullable(current).map(Page::getNextCursor).orElse("")).get();
            return Optional.ofNullable(current.getContent()).orElseGet(ArrayList::new);
        }
        throw new IllegalStateException("Cannot get next");
    }
}
<file_sep>/newrelic-alerts-configurator-dsl/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/dsl/configuration/PolicyConfigurationDsl.kt
package com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.PolicyConfiguration
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.Channel
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.Condition
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.ExternalServiceCondition
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.NrqlCondition
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.SyntheticsCondition
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.NewRelicConfigurationMarker
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.channel.Channels
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.condition.Conditions

@NewRelicConfigurationMarker
class PolicyConfigurationDsl {
    var policyName: String? = null
    var incidentPreference: PolicyConfiguration.IncidentPreference? = null
    internal val channels: MutableList<Channel> = mutableListOf()
    internal val conditions: MutableList<Condition> = mutableListOf()
    internal val externalServiceConditions: MutableList<ExternalServiceCondition> = mutableListOf()
    internal val nrqlConditions: MutableList<NrqlCondition> = mutableListOf()
    internal val syntheticsConditions: MutableList<SyntheticsCondition> = mutableListOf()

    fun channels(block: Channels.() -> Unit) = channels.addAll(Channels().apply(block).channels)

    fun conditions(block: Conditions.() -> Unit) {
        val conds = Conditions().apply(block)
        conditions.addAll(conds.conditions)
        externalServiceConditions.addAll(conds.externalServiceConditions)
        nrqlConditions.addAll(conds.nrqlConditions)
        syntheticsConditions.addAll(conds.syntheticsConditions)
    }
}

fun policyConfiguration(block: PolicyConfigurationDsl.() -> Unit): PolicyConfiguration {
    val dsl = PolicyConfigurationDsl()
    dsl.block()

    return PolicyConfiguration.builder()
            .policyName(requireNotNull(dsl.policyName) { "Policy name cannot be null" })
            .incidentPreference(requireNotNull(dsl.incidentPreference) { "Policy incident preference cannot be null" })
            .channels(dsl.channels)
            .conditions(dsl.conditions)
            .externalServiceConditions(dsl.externalServiceConditions)
            .nrqlConditions(dsl.nrqlConditions)
            .syntheticsConditions(dsl.syntheticsConditions)
            .build()
}

@NewRelicConfigurationMarker
class PolicyConfigurations {
    internal val policies: MutableList<PolicyConfiguration> = mutableListOf()

    fun policy(block: PolicyConfigurationDsl.() -> Unit) = policies.add(policyConfiguration(block))
    operator fun PolicyConfiguration.unaryPlus() = policies.add(this)
}
<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/condition/ServersMetricCondition.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition;

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.TermsConfiguration;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import lombok.Singular;

import java.util.Collection;

/**
 * Servers metric configuration.
 * Configuration parameters:
 * <ul>
 *     <li>{@link #conditionName}</li>
 *     <li>{@link #enabled}</li>
 *     <li>{@link #servers}</li>
 *     <li>{@link #metric}</li>
 *     <li>{@link #runBookUrl}</li>
 *     <li>{@link #terms}</li>
 *     <li>{@link #userDefinedConfiguration}</li>
 * </ul>
 */
@Getter
@Builder
public class ServersMetricCondition implements Condition {
    private final ConditionType type = ConditionType.SERVERS_METRIC;
    /**
     * Name of your servers metric condition.
     */
    @NonNull
    private String conditionName;
    /**
     * If your servers metric condition is enabled. Default is true.
     */
    @Builder.Default
    private boolean enabled = true;
    /**
     * Collection of server names for which this condition is applied.
     * If server with given name does not exist exception will be thrown.
     */
    @NonNull
    @Singular
    private Collection<String> servers;
    /**
     * Metric used in given condition.
     */
    @NonNull
    private Metric metric;
    /**
     * The runbook URL to display in notifications.
     */
    private String runBookUrl;
    /**
     * Collection of terms used for alerts condition.
     */
    @NonNull
    @Singular
    private Collection<TermsConfiguration> terms;
    /**
     * Configuration for user defined metric. Should be provided when {@link #metric} is set to USER_DEFINED
     */
    private UserDefinedConfiguration userDefinedConfiguration;

    @Override
    public Collection<String> getEntities() {
        return getServers();
    }

    @Override
    public String getMetricAsString() {
        return metric.name().toLowerCase();
    }

    @Override
    public UserDefinedConfiguration getUserDefinedMetric() {
        return userDefinedConfiguration;
    }

    public enum Metric {
        CPU_PERCENTAGE, DISK_IO_PERCENTAGE, MEMORY_PERCENTAGE, FULLEST_DISK_PERCENTAGE,
        LOAD_AVERAGE_ONE_MINUTE, USER_DEFINED
    }
}
<file_sep>/CHANGELOG.md
# 5.0.1 (13-07-2023)
- Added support for `violationTimeLimitSeconds`

# 5.0.0 (05-06-2023)
- Added support for new NR API

# 4.3.0 (07-03-2023)
- Added redirect following

# 4.2.0 (19-07-2022)
- Added possibility to define custom duration

# 4.1.0 (06-07-2022)
- Removed deprecated `sinceValue` from Nrql Condition

# 4.0.1 (03-06-2022)
- Allow usage of aggregationTimer when EVENT_TIMER streaming method is used

# 4.0.0 (19-05-2022)
- Adapt to Signal having aggregationMethod

# 3.6.1 (13-10-2021)
- Added support for signal configuration without expiration

# 3.6.0 (09-09-2021)
- Added support for `signalFillValue` in NRQL Signaling
- Updated conditions REST API url

# 3.5.3 (25-08-2021)
- Added SyntheticsMonitorsApi

# 3.5.2 (08-03-2021)
- Added NRQL signal configuration to Kotlin DSL

# 3.5.1 (14-01-2021)
- Fixed setting explicit null value for field 'fill_value' in Signal configuration

# 3.5.0 (02-12-2020)
- Added Signal configuration

# 3.4.1 (26-02-2020)
- Added new GcMetric `GC/MarkSweepCompact` and `GC/Copy` 

# 3.4.0 (06-06-2019)
- Added newrelic-alerts-configurator-dsl module with kotlin dsl

# 3.3.1 (03-10-2018)
- Added more "Since" values to NRQL Condition

# 3.3.0 (07-08-2018)
- Added support for Synthetics Monitor Failure conditions
- Removed unused AlertsNrqlConditionsApi interface

# 3.2.0 (09.04.2018)
- Added new GcMetric `GC/ParNew` and `GC/ConcurrentMarkSweep` 

# 3.1.0 (23.02.2018)
- Added support for New Relic Browser conditions 

# 3.0.0 (17.01.2018)
- Added support for NRQL conditions
- Changed conditions and channels management logic. Changed default conditions state. Those are breaking changes - see details in 
[Migration guide](MIGRATION.md)

# 2.0.3 (06.12.2017)
- Added support for JVM metrics conditions

# 2.0.2 (20.09.2017)
- Add violation_close_timer
- Make incident preference required in policy configuration

# 2.0.0 (15.02.2017)
- Changed groupdId and artifactId
- Changed packages to `com.ocadotechnology.newrelic`
- Changed `AlertsChannelConfiguration` field `payload` to type `Object`
- JSON deserialization configured to convert empty string into null POJO reference 

# 1.4.0 (06.02.2017)
- Added support for user notification channel 
- Simplified channel synchronization logic

# 1.3.0 (24.01.2017)
- Added UserDefined condition type support for `ApmAppCondition` and `ServersMetricCondition`

# 1.2.0 (19.01.2017)
- Added PagerDuty channel support

# 1.1.2
- Added Webhook channel support

# 1.1.1
- fixed Maven publication problems

# 1.1.0
- added support for server metric conditions
- fixed type of `links` property in `KeyTransaction` 
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/graphql/apiclient/internal/ModelTranslator.java
package com.ocadotechnology.newrelic.graphql.apiclient.internal;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.ocadotechnology.newrelic.apiclient.model.channels.AlertsChannel;
import com.ocadotechnology.newrelic.apiclient.model.channels.AlertsChannelConfiguration;
import com.ocadotechnology.newrelic.apiclient.model.channels.AlertsChannelLinks;
import com.ocadotechnology.newrelic.apiclient.model.conditions.Expiration;
import com.ocadotechnology.newrelic.apiclient.model.conditions.Signal;
import com.ocadotechnology.newrelic.apiclient.model.conditions.Terms;
import com.ocadotechnology.newrelic.apiclient.model.conditions.nrql.AlertsNrqlCondition;
import com.ocadotechnology.newrelic.apiclient.model.conditions.nrql.Nrql;
import com.ocadotechnology.newrelic.graphql.apiclient.internal.transport.*;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.text.StringEscapeUtils;

import java.io.StringReader;
import java.io.StringWriter;
import java.util.*;
import java.util.function.Consumer;

import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
import static com.ocadotechnology.newrelic.graphql.apiclient.internal.util.Preconditions.checkArgument;
import static java.lang.String.format;
import static java.util.Collections.emptyList;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;

@RequiredArgsConstructor
public class ModelTranslator {
    private static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper()
            .disable(FAIL_ON_UNKNOWN_PROPERTIES);

    private final ObjectMapper objectMapper;

    public ModelTranslator() {
        this.objectMapper = DEFAULT_OBJECT_MAPPER;
    }

    public AlertsChannel toModel(NotificationChannel notificationChannel) {
        return Optional.ofNullable(notificationChannel)
                .map(channel -> AlertsChannel.builder()
                        .id(Optional.ofNullable(channel.getId()).map(Integer::parseInt).orElse(null))
                        .name(channel.getName())
                        .type(Optional.ofNullable(channel.getType()).map(String::toLowerCase).orElse(null))
                        .links(new AlertsChannelLinks(Optional.ofNullable(channel.getAssociatedPolicies())
                                .map(AssociatedPolicies::getPolicies)
                                .orElse(Collections.emptyList())
                                .stream().map(Identifiable::getId)
                                .collect(toList())))
                        .configuration(toModel(channel.getConfig()))
                        .build())
                .orElse(null);
    }

    public AlertsNrqlCondition toModel(NrqlCondition dto) {
        return AlertsNrqlCondition.builder()
                .id(Optional.ofNullable(dto.getId()).map(Integer::parseInt).orElse(null))
                .name(dto.getName())
                .enabled(dto.getEnabled())
                .runbookUrl(dto.getRunbookUrl())
                .nrql(Optional.ofNullable(dto.getNrql())
                        .map(nrql -> new Nrql(nrql.getQuery()))
                        .orElse(null))
                .expiration(Optional.ofNullable(dto.getExpiration())
                        .map(expiration -> Expiration.builder()
                                .closeViolationsOnExpiration(expiration.isCloseViolationsOnExpiration())
                                .expirationDuration(expiration.getExpirationDuration())
                                .openViolationOnExpiration(expiration.isOpenViolationOnExpiration())
                                .build())
                        .orElse(null))
                .signal(Optional.ofNullable(dto.getSignal())
                        .map(signal -> Signal.builder()
                                .aggregationDelay(signal.getAggregationDelay())
                                .aggregationMethod(Optional.ofNullable(signal.getAggregationMethod()).map(String::toUpperCase).orElse(null))
                                .aggregationTimer(signal.getAggregationTimer())
                                .aggregationWindow(signal.getAggregationWindow())
                                .fillOption(Optional.ofNullable(signal.getFillOption()).map(String::toLowerCase).orElse(null))
                                .fillValue(signal.getFillValue())
                                .build())
                        .orElse(null))
                .terms(Optional.ofNullable(dto.getTerms())
                        .map(terms -> terms.stream()
                                .map(term -> Terms.builder()
                                        .operator(Optional.ofNullable(term.getOperator()).map(String::toLowerCase).orElse(null))
                                        .priority(Optional.ofNullable(term.getPriority()).map(String::toLowerCase).orElse(null))
                                        .threshold(term.getThreshold())
                                        .duration(Optional.ofNullable(term.getThresholdDuration())
                                                .map(Long::parseLong)
                                                .map(durationSeconds -> "" + (long) (durationSeconds / 60))
                                                .orElse(null))
                                        .timeFunction(Optional.ofNullable(term.getThresholdOccurrences())
                                                .map(this::thresholdOccurrencesToTimeFunctionTo)
                                                .map(String::toLowerCase)
                                                .orElse(null))
                                        .build())
                                .collect(toList()))
                        .orElse(null))
                .valueFunction(Optional.ofNullable(dto.getValueFunction()).map(String::toLowerCase).orElse(null))
                .build();
    }

    private AlertsChannelConfiguration toModel(NotificationChannelConfig dto) {
        return Optional.ofNullable(dto)
                .filter(config -> !config.isEmpty())
                .map(config -> AlertsChannelConfiguration.builder()
                        .recipients(Optional.ofNullable(config.getEmails()).orElse(emptyList())
                                .stream().reduce(null, (acc, email) -> (isNull(acc) ? "" : acc + ",") + email))
                        .includeJsonAttachment(config.getIncludeJson())
                        .userId(Optional.ofNullable(config.getUserId())
                                .map(Integer::parseInt).orElse(null))
                        .channel(config.getTeamChannel())
                        .baseUrl(config.getBaseUrl())
                        .headers(Optional.ofNullable(config.getCustomHttpHeaders())
                                .map(httpHeaders -> httpHeaders.stream().collect(toMap(HttpHeader::getName, HttpHeader::getValue)))
                                .filter(httpHeaders -> !httpHeaders.isEmpty())
                                .orElse(null))
                        .authUsername(Optional.ofNullable(config.getBasicAuth()).map(NotificationChannelConfig.BasicAuth::getUsername).orElse(null))
                        .payloadType(payloadTypeToModel(config.getCustomPayloadType()))
                        .payload(payloadToModel(config.getCustomPayloadType(), config.getCustomPayloadBody()))
                        .build()
                ).orElse(null);
    }

    @SuppressWarnings("unchecked")
    public Map<String, Object> toChannelConfig(String type, String name, AlertsChannelConfiguration configuration) {
        checkArgument(nonNull(type), "channel type cannot be null");
        checkArgument(nonNull(name), "channel name cannot be null");
        checkArgument(nonNull(configuration), "configuration name cannot be null");

        Map<String, Object> config = map();
        config.put("name", name);
        String wrapperName = type;
        switch (type) {
            case "email":
                config.put("emails", Optional.ofNullable(configuration.getRecipients())
                        .map(recipients -> Arrays.stream(recipients.split(",")).map(String::trim).collect(toList())).orElse(null));
                config.put("includeJson", Optional.ofNullable(configuration.getIncludeJsonAttachment()).orElse(false));
                break;
            case "pagerduty":
                config.put("apiKey", configuration.getServiceKey());
                wrapperName = "pagerDuty";
                break;
            case "slack":
                config.put("teamChannel", configuration.getChannel());
                config.put("url", configuration.getUrl());
                break;
            case "webhook":
                AlertsWebhookCustomPayloadType payloadType = Optional.ofNullable(configuration.getPayloadType())
                        .map(AlertsWebhookCustomPayloadType::byType).orElse(null);

                config.put("baseUrl", configuration.getBaseUrl());
                config.put("basicAuth", Optional.ofNullable(configuration.getAuthUsername())
                        .map(__ -> {
                            Map<String, Object> basicAuth = map();
                            basicAuth.put("username", configuration.getAuthUsername());
                            basicAuth.put("password", configuration.getAuthPassword());
                            return basicAuth;
                        }).orElse(null));
                config.put("customPayloadType", Optional.ofNullable(payloadType)
                        .map(t -> new JavaLikeEnum(t.toString())).orElse(null));
                config.put("customHttpHeaders", Optional.ofNullable(configuration.getHeaders())
                        .map(headers -> headers.entrySet().stream().map(header -> {
                            Map<String, String> headerObject = map();
                            headerObject.put("name", header.getKey());
                            headerObject.put("value", header.getValue());
                            return headerObject;
                        }).collect(toList())).orElse(null));
                config.put("customPayloadBody", Optional.ofNullable(configuration.getPayload())
                        .flatMap(__ -> Optional.ofNullable(payloadType))
                        .map(pt -> {
                            switch (pt) {
                                case JSON:
                                    return writeJson(configuration.getPayload());
                                case FORM:
                                    return configuration.getPayload() instanceof String
                                            ? configuration.getPayload()
                                            : writeProperties((Map<String, Object>) configuration.getPayload());
                            }
                            return null;
                        }).orElse(null)
                );
                break;
            default:
                throw new IllegalArgumentException(format("Unsupported channel type: '%s'", type));
        }

        Map<String, Object> wrapper = new HashMap<>();
        wrapper.put(wrapperName, config);
        return wrapper;
    }

    public Map<String, Object> toNrqlCondition(AlertsNrqlCondition condition) {
        Map<String, Object> cond = map();
        cond.put("enabled", condition.getEnabled());
        cond.put("name", condition.getName());
        onPresent(condition.getRunbookUrl(), runbookUrl -> cond.put("runbookUrl", runbookUrl));
        Map<String, Object> nrql = map();
        nrql.put("query", Optional.ofNullable(condition.getNrql()).map(Nrql::getQuery).orElse(null));
        cond.put("nrql", nrql);
        onPresent(condition.getExpiration(), expiration -> {
            Map<String, Object> result = map();
            result.put("closeViolationsOnExpiration", expiration.isCloseViolationsOnExpiration());
            result.put("openViolationOnExpiration", expiration.isOpenViolationOnExpiration());
            onPresent(expiration.getExpirationDuration(),
                    expirationDuration -> result.put("expirationDuration", toLong(expiration.getExpirationDuration())));
            cond.put("expiration", result);
        });
        onPresent(condition.getSignal(), signal -> {
            Map<String, Object> result = map();
            onPresent(signal.getAggregationDelay(), aggregationDelay -> result.put("aggregationDelay", toLong(aggregationDelay)));
            onPresent(signal.getAggregationMethod(), aggregationMethod -> result.put("aggregationMethod", toEnum(aggregationMethod)));
            onPresent(signal.getAggregationTimer(), aggregationTimer -> result.put("aggregationTimer", toLong(aggregationTimer)));
            onPresent(signal.getAggregationWindow(), aggregationWindow -> result.put("aggregationWindow", toLong(aggregationWindow)));
            onPresent(signal.getFillOption(), fillOption -> result.put("fillOption", toEnum(fillOption)));
            onPresent(signal.getFillValue(), fillValue -> result.put("fillValue", fillValue));
            cond.put("signal", result);
        });
        cond.put("terms", Optional.ofNullable(condition.getTerms()).orElseGet(ArrayList::new).stream()
                .map(term -> {
                    LinkedHashMap<String, Object> result = map();
                    onPresent(term.getOperator(), operator -> result.put("operator", toEnum(operator)));
                    onPresent(term.getPriority(), priority -> result.put("priority", toEnum(priority)));
                    onPresent(term.getThreshold(), threshold -> result.put("threshold", toFloat(threshold)));
                    onPresent(term.getDuration(), duration -> result.put("thresholdDuration", toLong(duration) * 60));
                    onPresent(term.getTimeFunction(), timeFunction -> result.put("thresholdOccurrences", toEnum(timeFunctionToThresholdOccurrences(timeFunction))));
                    return result;
                })
                .collect(toList()));
        onPresent(condition.getValueFunction(), valueFunction -> cond.put("valueFunction", toEnum(valueFunction)));
        return cond;
    }

    private String timeFunctionToThresholdOccurrences(String input) {
        return input.equalsIgnoreCase("any") ? "at_least_once" : input;
    }

    private String thresholdOccurrencesToTimeFunctionTo(String input) {
        return input.equalsIgnoreCase("at_least_once") ? "any" : input;
    }

    private static <T1, T2> LinkedHashMap<T1, T2> map() {
        return new LinkedHashMap<>();
    }

    private JavaLikeEnum toEnum(String fillOption) {
        return new JavaLikeEnum(fillOption.toUpperCase());
    }

    private long toLong(String value) {
        return Long.parseLong(value);
    }

    private float toFloat(String value) {
        return Float.parseFloat(value);
    }

    private <T> void onPresent(T object, Consumer<T> consumer) {
        if (Objects.nonNull(object)) {
            consumer.accept(object);
        }
    }

    private String payloadTypeToModel(String customPayloadType) {
        return Optional.ofNullable(customPayloadType)
                .map(AlertsWebhookCustomPayloadType::valueOf)
                .map(AlertsWebhookCustomPayloadType::getType)
                .orElse(null);
    }

    private Map<String, Object> payloadToModel(String customPayloadType, String customPayloadBody) {
        return Optional.ofNullable(customPayloadBody)
                .map(__ -> customPayloadType)
                .map(AlertsWebhookCustomPayloadType::valueOf)
                .map(type -> {
                    switch (type) {
                        case JSON:
                            return readJson(StringEscapeUtils.unescapeJson(customPayloadBody));
                        case FORM:
                            return readProperties(customPayloadBody);
                        default:
                            return null;
                    }
                })
                .orElse(null);
    }

    @SneakyThrows
    private String writeJson(Object value) {
        return objectMapper.writeValueAsString(value);
    }

    @SuppressWarnings("unchecked")
    @SneakyThrows
    private Map<String, Object> readJson(String value) {
        return objectMapper.readValue(value, Map.class);
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    @SneakyThrows
    private Map<String, Object> readProperties(String value) {
        Properties properties = new Properties();
        properties.load(new StringReader(value));
        return (Map<String, Object>) (Map) properties;
    }

    @SneakyThrows
    public static String writeProperties(Map<String, Object> value) {
        Properties properties = new Properties();
        properties.putAll(value);
        StringWriter writer = new StringWriter();
        properties.store(writer, null);
        String output = writer.toString();
        String sep = System.getProperty("line.separator");
        return output.substring(output.indexOf(sep) + sep.length()).trim().replaceAll(sep, "\n");
    }
}
<file_sep>/gradle.properties
version=5.0.2-SNAPSHOT<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/condition/ApmAppCondition.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition;

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.TermsConfiguration;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.violationclosetimer.ViolationCloseTimer;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import lombok.Singular;

import java.util.Collection;

/**
 * APM application configuration.
 * Configuration parameters:
 * <ul>
 *     <li>{@link #conditionName}</li>
 *     <li>{@link #enabled}</li>
 *     <li>{@link #applications}</li>
 *     <li>{@link #metric}</li>
 *     <li>{@link #conditionScope}</li>
 *     <li>{@link #runBookUrl}</li>
 *     <li>{@link #terms}</li>
 *     <li>{@link #violationCloseTimer}</li>
 *     <li>{@link #userDefinedConfiguration}</li>
 * </ul>
 */
@Getter
@Builder
public class ApmAppCondition implements Condition {
    private final ConditionType type = ConditionType.APM_APP;
    /**
     * Name of your APM application metric condition
     */
    @NonNull
    private String conditionName;
    /**
     * If your APM application metric condition is enabled. Default is true
     */
    private boolean enabled;
    /**
     * Collection of application names for which this condition is applied.
     * If application with given name does not exist exception will be thrown
     */
    @NonNull
    @Singular
    private Collection<String> applications;
    /**
     * Metric used in given condition
     */
    @NonNull
    private Metric metric;
    /**
     * Condition scope used in given condition
     */
    @NonNull
    private ConditionScope conditionScope;
    /**
     * The runbook URL to display in notifications
     */
    private String runBookUrl;
    /**
     * Collection of terms used for alerts condition
     */
    @NonNull
    @Singular
    private Collection<TermsConfiguration> terms;
    /**
     * Duration (in hours) after which instance-based violations will automatically close.
     * Required when {@link #conditionScope} is set to {@link ConditionScope#INSTANCE}
     */
    private ViolationCloseTimer violationCloseTimer;
    /**
     * Configuration for user defined metric. Should be provided when {@link #metric} is set to USER_DEFINED
     */
    private UserDefinedConfiguration userDefinedConfiguration;

    @Override
    public Collection<String> getEntities() {
        return getApplications();
    }

    @Override
    public String getMetricAsString() {
        return metric.name().toLowerCase();
    }

    @Override
    public String getConditionScopeAsString() {
        return conditionScope.name().toLowerCase();
    }

    @Override
    public String getViolationCloseTimerAsString() {
        return violationCloseTimer == null ? null : violationCloseTimer.getDuration();
    }

    @Override
    public UserDefinedConfiguration getUserDefinedMetric() {
        return userDefinedConfiguration;
    }

    public enum Metric {
        APDEX, ERROR_PERCENTAGE, RESPONSE_TIME_WEB, RESPONSE_TIME_BACKGROUND, THROUGHPUT_WEB,
        THROUGHPUT_BACKGROUND, USER_DEFINED
    }

    public enum ConditionScope {
        APPLICATION, INSTANCE
    }

    public static class ApmAppConditionBuilder {
        private boolean enabled = true;

        public ApmAppCondition build() {
            validateViolationCloseTimer();
            return new ApmAppCondition(conditionName, enabled, applications, metric, conditionScope,
                    runBookUrl, terms, violationCloseTimer, userDefinedConfiguration);
        }

        private void validateViolationCloseTimer() {
            if (conditionScope == ConditionScope.INSTANCE && violationCloseTimer == null) {
                throw new NullPointerException("violationCloseTimer");
            }
        }
    }

}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/AlertsPoliciesApi.java
package com.ocadotechnology.newrelic.apiclient;

import com.ocadotechnology.newrelic.apiclient.model.policies.AlertsPolicy;
import com.ocadotechnology.newrelic.apiclient.model.policies.AlertsPolicyChannels;

import java.util.Optional;

public interface AlertsPoliciesApi {
    /**
     * Gets {@link AlertsPolicy} object using its name.
     *
     * @param alertsPolicyName name of the alert policy registered in NewRelic
     * @return Optional containing {@link AlertsPolicy} object, or empty if alert policy not found
     */
    Optional<AlertsPolicy> getByName(String alertsPolicyName);

    /**
     * Creates Alerts Policy instance.
     *
     * @param policy policy definition to be created
     * @return created {@link AlertsPolicy}
     */
    AlertsPolicy create(AlertsPolicy policy);

    /**
     * Deletes Alerts Policy instance.
     *
     * @param policyId id of the policy to be removed
     * @return deleted {@link AlertsPolicy}
     */
    AlertsPolicy delete(int policyId);

    /**
     * Associates given channels to the policy. This method does not remove previously linked channel but not provided in the list.
     *
     * @param channels {@link AlertsPolicyChannels} object mapping policy id to the list of channels ids
     * @return {@link AlertsPolicyChannels} when successfully set
     */
    AlertsPolicyChannels updateChannels(AlertsPolicyChannels channels);
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/internal/model/DashboardList.java
package com.ocadotechnology.newrelic.apiclient.internal.model;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.ocadotechnology.newrelic.apiclient.model.dashboards.Dashboard;
import lombok.EqualsAndHashCode;

import java.util.List;

@EqualsAndHashCode(callSuper = true)
public class DashboardList extends ObjectList<Dashboard, DashboardList> {
    @JsonCreator
    public DashboardList(@JsonProperty("dashboards") List<Dashboard> items) {
        super(items, DashboardList::new);
    }
}
<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/channel/ChannelType.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel;

public enum ChannelType {
    EMAIL, SLACK, WEBHOOK, PAGERDUTY, USER
}
<file_sep>/newrelic-alerts-configurator/README.md
# Table of contents

- [Configurator](#configurator)
- [Application configuration](#application-configuration)
- [Policy configuration](#policy-configuration)
    - [Alerts condition](#alerts-condition)
        - [APM application metric condition](#apm-application-metric-condition)
        - [APM JVM metric condition](#apm-jvm-metric-condition)
        - [APM key transaction metric condition](#apm-key-transaction-metric-condition)
        - [Server metric condition](#server-metric-condition)
        - [New Relic Browser metric condition](#new-relic-browser-metric-condition)
        - [Alerts external service condition](#alerts-external-service-condition)
            - [APM external service condition](#apm-external-service-condition)
        - [Alerts NRQL condition](#alerts-nrql-condition)
        - [Synthetics Monitor Failure condition](#synthetics-monitor-failure-condition)
        - [User defined configuration](#user-defined-configuration)
        - [Term](#term)
        - [NRQL Term](#nrql-term)
        - [Notification channel](#notification-channel)
            - [Email channel](#email-channel)
            - [Slack channel](#slack-channel)
            - [Webhook channel](#webhook-channel)
            - [PagerDuty channel](#pagerduty-channel)
            - [NewRelic user channel](#newrelic-user-channel)
            - [Condition and Channel management policy](#condition-and-channel-management-policy)

## Configurator

`Configurator` is the main class used to synchronize all defined configurations with NewRelic. Example use of Configurator:

```java
Configurator configurator = new Configurator(yourApiKey);
configurator.setApplicationConfigurations(yourApplicationConfigurationsCollection);
configurator.setPolicyConfigurations(yourPolicyConfigurationsCollection);
configurator.sync();
```

Both `setApplicationConfigurations` and `setPolicyConfigurations` accept Java Collections of configurations.

## Application configuration

To configure your application in NewRelic you just need to create and pass your application configuration to NewRelic Alerts configurator.
Application configuration can be created using simple builder:

```java
ApplicationConfiguration.builder()
```

What you can set in your configuration:
- application name - Your application name.
- application apdex threshold (optional) - Set apdex threshold for application.
- end user apdex threshold (optional) - Set apdex threshold for end user.
- real user monitoring enabled (optional) - If real user monitoring is enabled. Default value is false.

If application with given application name does not exist exception will be thrown.

Example application configuration:

```java
ApplicationConfiguration configuration = ApplicationConfiguration.builder()
    .applicationName("Application name")
    .appApdexThreshold(0.5f)
    .endUserApdexThreshold(7.0f)
    .enableRealUserMonitoring(true)
    .build();
```

## Policy configuration

To configure your alerts policy in NewRelic you just need to create and pass your policy configuration to NewRelic Alerts configurator.
Policy configuration can be created using simple builder:

```java
PolicyConfiguration.builder()
```

What you can set in your configuration:
- policy name - Name of your alerts policy.
- incident preference - Rollup strategy options for your alerts policy. Possible values are:
    - Per policy
    - Per condition
    - Per condition and target
- conditions (optional) - Collection of [alerts conditions](#alerts-condition) which needs to be configured for your alerts policy.
- external service conditions (optional) - Collection of [alerts external service conditions](#alerts-external-service-condition)
  which needs to be configured for your alerts policy.
- nrql conditions (optional) - Collection of [alerts NRQL conditions](#alerts-nrql-condition) which needs to be configured for 
  your alerts policy.
- synthetics conditions (optional) - Collection of [Synthetics Monitor Failure conditions](#synthetics-monitor-failure-condition) 
  which need to be configured for your alerts policy.
- channels (optional) - Collection of [alerts channels](#notification-channel) which needs to be configured for your alerts policy.

If policy with given policy name exists - it will be updated.

If policy with given policy name does not exist - it will be created.

Example alerts policy configuration:

```java
PolicyConfiguration configuration = PolicyConfiguration.builder()
    .policyName("Policy name")
    .incidentPreference(PolicyConfiguration.IncidentPreference.PER_POLICY)
    .condition(apmAppCondition)
    .condition(apmKeyTransactionCondition)
    .externalServiceCondition(apmExternalServiceCondition)
    .nrqlCondition(nrqlCondition)
    .channel(emailChannel)
    .channel(slackChannel)
    .build()
```

### Alerts condition

Currently supported types of alerts policy conditions are:
- [APM application metric condition](#apm-application-metric-condition)
- [APM JVM metric condition](#apm-jvm-metric-condition)
- [APM key transaction metric condition](#apm-key-transaction-metric-condition)
- [Server metric condition](#server-metric-condition)
- [New Relic Browser metric condition](#new-relic-browser-metric-condition)

Condition creation/update/removal policy is described in [Condition and Channel management policy](#condition-and-channel-management-policy)

#### APM application metric condition

To create APM application metric condition for your alerts policy use simple builder:

```java
ApmAppCondition.builder()
```

What you can set for APM application metric condition:
- condition name - Name of your APM application metric condition.
- enabled (optional) - If your APM application metric condition is enabled. Default is true.
- applications - Collection of application names for which this condition is applied. If application with given name does not exist exception will be thrown.
- metric - Metric used in given condition. Possible values are:
    - Apdex
    - Error percentage
    - Response time (web)
    - Response time (background)
    - Throughput (web)
    - Throughput (background)
    - User defined
- condition scope - Possible values are:
    -  Application - The average of all application instances' data is used during evaluation.
    -  Instance - Each application instance's data is evaluated individually
- run book url (optional) - The runbook URL to display in notifications.
- terms - Collection of [terms](#term) used for alerts condition.
- violation close timer - Duration (in hours) after which instance-based violations will automatically close.
  This field is only required when condition scope is set to 'Instance'. Possible values are:
    - 1
    - 2
    - 4
    - 8
    - 12
    - 24
- user defined configuration - configuration for [user defined metric](#user-defined-configuration).

Example APM application metric condition configuration:

```java
Condition apmAppCondition = ApmAppCondition.builder()
    .conditionName("Condition name")
    .enabled(true)
    .application("Application name")
    .metric(ApmAppCondition.Metric.APDEX)
    .conditionScope(ApmAppCondition.ConditionScope.APPLICATION)
    .term(term)
    .build();
```

#### APM JVM metric condition

To create APM JVM metric condition for your alerts policy use simple builder:

```java
ApmJvmCondition.builder()
```

What you can set for APM JVM metric condition:
- condition name - Name of your APM application metric condition.
- enabled (optional) - If your APM application metric condition is enabled. Default is true.
- applications - Collection of application names for which this condition is applied. If application with given name does not exist exception will be thrown.
- metric - Metric used in given condition. Possible values are:
    - Deadlocked threads
    - Heap memory usage
    - CPU utilization time
    - Garbage collection CPU time
- gc metric - Metric used to get GC CPU time. To use it metric has to be set to measure garbage collection CPU time. Possible values are:
    - PS MarkSweep
    - PS Scavenge
    - ParNew
    - ConcurrentMarkSweep
- ###### run book url (optional) - The runbook URL to display in notifications.
- terms - Collection of [terms](#term) used for alerts condition.
- violation close timer - Duration (in hours) after which instance-based violations will automatically close.
  Possible values are:
    - 1
    - 2
    - 4
    - 8
    - 12
    - 24

Example APM JVM metric condition configuration:

```java
Condition apmJvmCondition = ApmJvmCondition.builder()
    .conditionName("Condition name")
    .enabled(true)
    .application("Application name")
    .metric(ApmJvmCondition.Metric.GC_CPU_TIME)
    .gcMetric(ApmJvmCondition.GcMetric.GC_MARK_SWEEP)
    .term(term)
    .violationCloseTimer(ViolationCloseTimer.DURATION_24)
    .build();
```

#### APM key transaction metric condition

To create APM key transaction metric condition for your alerts policy use simple builder:

```java
ApmKeyTransactionCondition.builder()
```

What you can set for APM key transaction metric condition:
- condition name - Name of your APM key transaction metric condition.
- enabled (optional) - If your APM key transaction metric condition is enabled. Default is true.
- keyTransactions - Collection of key transaction names for which this condition is applied. If key transaction with given name does not exist exception will be thrown.
- metric - Metric used in given condition. Possible values are:
    - Apdex
    - Error percentage
    - Error count
    - Response time
    - Throughput
- run book url (optional) - The runbook URL to display in notifications.
- terms - Collection of [terms](#term) used for alerts condition.

Example APM key transaction metric condition configuration:

```java
Condition apmKeyTransactionCondition = ApmKeyTransactionCondition.builder()
    .conditionName("Condition name")
    .enabled(true)
    .keyTransastion("Key Transaction name")
    .metric(ApmKeyTransactionCondition.Metric.APDEX)
    .term(term)
    .build();
```

**Note that key transaction cannot be created via New Relic API. Before configuration of condition, key transaction needs to be manually created in New Relic.**

#### Server metric condition

To create server metric condition for your alerts policy use simple builder:

```java
ServersMetricCondition.builder()
```

What you can set for server metric condition:
- condition name - Name of your server metric condition.
- enabled (optional) - If your server metric condition is enabled. Default is true.
- servers - Collection of server names for which this condition is applied. If server with given name does not exist exception will be thrown.
- metric - Metric used in given condition. Possible values are:
    - CPU percentage
    - Disk I/O percentage
    - Memory percentage
    - Fullest disk percentage
    - Load average one minute
    - User defined
- run book url (optional) - The runbook URL to display in notifications.
- terms - Collection of [terms](#term) used for alerts condition.
- user defined configuration - configuration for [user defined metric](#user-defined-configuration).

Example server metric condition configuration:

```java
Condition serverMetricCondition = ServerMetricCondition.builder()
    .conditionName("Condition name")
    .enabled(true)
    .server("some-host")
    .metric(ServersMetricCondition.Metric.CPU_PERCENTAGE)
    .term(term)
    .build();
```

#### New Relic Browser metric condition

To create New Relic Browser metric condition for your alerts policy use simple builder:

```java
BrowserCondition.builder()
```

What you can set for New Relic Browser metric condition:
- condition name - Name of your New Relic Browser metric condition
- enabled (optional) - If your New Relic Browser metric condition is enabled. Default is true.
- applications - Collection of application names for which this condition is applied. If application with given name does not exist exception will be thrown.
- metric - Metric used in given condition. Possible values are:
    - End user Apdex
    - Total page load
    - Page rendering
    - Web application
    - Network
    - DOM processing
    - Request queuing
    - AJAX response time
    - Page views with JS errors
    - Page view throughput
    - AJAX throughput
    - User defined
- run book url (optional) - The runbook URL to display in notifications.
- terms - Collection of [terms](#term) used for alerts condition.
- user defined configuration - configuration for [user defined metric](#user-defined-configuration).

Example New Relic Browser metric condition configuration:

```java
Condition browserCondition = BrowserCondition.builder()
	.conditionName("Page views with JS errors")
	.enabled(true)
	.application("Application name")
	.metric(BrowserCondition.Metric.PAGE_VIEWS_WITH_JS_ERRORS)
	.term(term)
	.build();
```

**Please note New Relic Browser agent has to be installed.** 

### Alerts external service condition

Currently supported types of alerts policy external service conditions are:
- [APM external service condition](#apm-external-service-condition)

External Service Condition creation/update/removal policy is described in [Condition and Channel management policy](#condition-and-channel-management-policy)

#### APM external service condition

To create APM external service condition for your alerts policy use simple builder:

```java
ApmExternalServiceCondition.builder()
```

What you can set for APM external service condition:
- condition name - Name of your APM external service condition.
- enabled (optional) - If your APM external service condition is enabled. Default is true.
- applications - Collection of application names for which this condition is applied. If application with given name does not exist exception will be thrown.
- metric - Metric used in given condition. Possible values are:
    - Response time (average)
    - Response time (minimum)
    - Response time (maximum)
    - Throughput
- external service url - URL of the external service to be monitored. This string must not include the protocol.
- run book url (optional) - The runbook URL to display in notifications.
- terms - Collection of [terms](#term) used for alerts condition.

Example APM external service condition configuration:

```java
ExternalServiceCondition apmExternalServiceCondition = ApmExternalServiceCondition.builder()
    .conditionName("Condition name")
    .enabled(true)
    .application("Application name")
    .externalServiceUrl("externalServiceUrl")
    .metric(ApmExternalServiceCondition.Metric.RESPONSE_TIME_AVERAGE)
    .term(term)
    .build();
```

### Alerts NRQL condition

NRQL Condition creation/update/removal policy is described in [Condition and Channel management policy](#condition-and-channel-management-policy)

To create NRQL condition for your alerts policy use simple builder:

```java
NrqlCondition.builder()
```

What you can set for NRQL condition:
- condition name - Name of your NRQL condition.
- enabled (optional) - If your NRQL condition is enabled. Default is true.
- run book url (optional) - The runbook URL to display in notifications.
- terms - Collection of [NRQL terms](#nrql-term) used for alerts condition.
- value function - How condition should be evaluated. Possible values are:
    - single_value - the condition will be evaluated by the raw value returned.
    - sum - the condition will evaluate on the sum of the query results.
- query - Query in NRQL.
- signal configuration - Determines how data should be collected and what should happened when there is lack of data:
    - aggregation window - Time (in seconds) for how long NewRelic collects data before running the NRQL query.
    - evaluation windows - Number of windows to evaluate data.
    - signal fill option - Configuration of filling data gaps/signal lost. Possible values are:
        - NONE - do not fill any data
        - LAST_KNOWN_VALUE - fill with last known value
    - signal lost configuration:
        - signal is lost after - Time (in seconds) for the signal treated as lost.
        - open new violation on signal lost - Opens a loss of signal violation when no signal.
        - closeCurrentViolationsOnSignalLost - cCoses all currently open violations when no signal is heard.
     
Example NRQL condition configuration:

```java
NrqlCondition.builder()
    .conditionName("Condition name")
    .enabled(true)
    .valueFunction(NrqlCondition.ValueFunction.SINGLE_VALUE)
    .term(term)
    .query("SELECT count(*) FROM `myApp:HealthStatus` WHERE healthy IS false")
    .build();
```

### Synthetics Monitor Failure condition

Synthetics Monitor Failure condition creation/update/removal policy is described in [Condition and Channel management policy](#condition-and-channel-management-policy)

To create Synthetics Monitor Failure condition for your alerts policy use simple builder:

```java
SyntheticsCondition.builder()
```

What you can set for Synthetics Monitor Failure condition:
- condition name - Name of your Synthetics Monitor Failure condition.
- monitorName - The name of the Synthetics Monitor you want to alert on.
- enabled (optional) - If your Synthetics Monitor Failure condition is enabled. Default is true.
- run book url (optional) - The runbook URL to display in notifications.

Example Synthetics Monitor Failure condition configuration:

```java
SyntheticsCondition.builder()
    .conditionName("MyMonitor failed")
    .enabled(true)
    .monitorName("MyMonitor")
    .build();
```
### Synthetic Monitor Create
Synthetic monitor can be created as follows. 
```java
new NewRelicApi("api-key").getSyntheticsMonitorsApi().create(new Monitor(...))
```

### Synthetic Monitor Delete
Synthetic monitor can be delete as follows. 
```java
new NewRelicApi("api-key").getSyntheticsMonitorsApi().delete(new Monitor(...))
```

### User defined configuration

User defined configuration allows to use custom (user defined) metric in alert definition.

Example user defined configuration:

```java
UserDefinedConfiguration config = UserDefinedConfiguration.builder()
    .metric("MY_CUSTOM_METRIC")
    .valueFunction(UserDefinedConfiguration.ValueFunction.MAX)
    .build();
```

### Term

Terms are used both in alerts conditions and alerts external service conditions.
To create term configuration for condition use simple builder:

```java
TermsConfiguration.builder()
```

What you can set in term configuration:
- duration - Time (in minutes) for the condition to persist before triggering an event. Possible values are:
    - 5
    - 10
    - 15
    - 30
    - 60
    - 120
- operator - Determines what comparison will be used between the monitored value and the threshold term value to trigger an event. Possible values are:
    - Above
    - Below
    - Equal
- priority - Severity level for given term in condition. Possible values are:
    - Critical
    - Warning
- time function - Time function in which threshold term have to be reached in duration term to trigger an event. Possible values are:
    - All (for whole time)
    - Any (at least once in)
- threshold - Threshold that the monitored value must be compared to using the operator term for an event to be triggered.

Example term configuration:

```java
TermsConfiguration term = TermsConfiguration.builder()
    .durationTerm(DurationTerm.DURATION_5)
    .operatorTerm(OperatorTerm.BELOW)
    .thresholdTerm(0.8f)
    .priorityTerm(PriorityTerm.CRITICAL)
    .timeFunctionTerm(TimeFunctionTerm.ALL)
    .build();
```

### NRQL Term

NRQL Terms are used in NRQL conditions.
To create term configuration for condition use simple builder:

```java
NrqlTermsConfiguration.builder()
```

What you can set in term configuration:
- duration - Time (in minutes) for the condition to persist before triggering an event. Possible values are:
    - 1
    - 2
    - 3
    - 4
    - 5
    - 10
    - 15
    - 30
    - 60
    - 120
- operator - Determines what comparison will be used between the monitored value and the threshold term value to trigger an event. Possible values are:
    - Above
    - Below
    - Equal
- priority - Severity level for given term in condition. Possible values are:
    - Critical
    - Warning
- time function - Time function in which threshold term have to be reached in duration term to trigger an event. Possible values are:
    - All (for whole time)
    - Any (at least once in)
- threshold - Threshold that the monitored value must be compared to using the operator term for an event to be triggered.

Example term configuration:

```java
NrqlTermsConfiguration term = NrqlTermsConfiguration.builder()
    .durationTerm(NrqlDurationTerm.DURATION_1)
    .operatorTerm(OperatorTerm.BELOW)
    .thresholdTerm(0.8f)
    .priorityTerm(PriorityTerm.CRITICAL)
    .timeFunctionTerm(TimeFunctionTerm.ALL)
    .build();
```

### Notification channel

Currently supported types of alerts notification channels are:
- [Email channel](#email-channel)
- [Slack channel](#slack-channel)
- [Webhook channel](#webhook-channel)
- [PagerDuty channel](#pagerduty-channel)
- [NewRelic user channel](#newrelic-user-channel)

Channels creation/update/removal policy is described in [Condition and Channel management policy](#condition-and-channel-management-policy)
Only exception is NewRelic user channel - we will not delete it.

#### Email channel

To create email channel use simple builder:

```java
EmailChannel.builder()
```

What you can set in email channel configuration:
- channel name - Name of your alerts channel.
- email address - Email address to which alert event should be sent.
- include json attachment (optional) - If json attachment should be included in alert event email. Default value is false.

Example email channel configuration:

```java
Channel emailChannel = EmailChannel.builder()
    .channelName("Channel name")
    .emailAddress("<EMAIL>")
    .includeJsonAttachment(true)
    .build();
```

#### Slack channel

To create slack channel use simple builder:

```java
SlackChannel.builder()
```

What you can set in slack channel configuration:
- channel name - Name of your alerts channel.
- slack url - Slack url to your channel.
- team channel (optional) - Name of your team channel.

Example slack channel configuration:

```java
Channel slackChannel = SlackChannel.builder()
    .channelName("Channel name")
    .slackUrl("https://hooks.slack.com/services/...")
    .teamName("My team slack name")
    .build();
```

#### Webhook channel

To create webhook channel use simple builder:

```java
WebhookChannel.builder()
```

What you can set in webhook channel configuration:
- channel name - Name of your alerts channel.
- base URL - URL webhook address to which alert event should be sent.
- auth username - User name for BASIC authorization (optional)
- auth password - User password for BASIC authorization (optional)

Example webhook channel configuration:

```java
Channel webhookChannel = WebhookChannel.builder()
    .channelName("Channel name")
    .baseUrl("https://example.com/webhook/abcd123")
    .authUsername("john.doea")
    .authPassword("<PASSWORD>")
    .build();
```

#### PagerDuty channel

To create [PagerDuty](https://www.pagerduty.com/) channel use simple builder:

```java
PagerDutyChannel.builder()
```

What you can set in PagerDUty channel configuration:
- channel name - Name of your alerts channel.
- serviceKey - Service key integration ID defined in PagerDuty.

Example PagerDuty channel configuration:

```java
Channel pagerDutyChannel = PagerDutyChannel.builder()
    .channelName("Channel name")
    .serviceKey("<KEY>")
    .build();
```

#### NewRelic user channel

To create NewRelic user channel use simple builder:

```java
UserChannel.builder()
```

What you can set in NewRelic user channel:
- email address - NewRelic user's email address 

Example email channel configuration:

```java
Channel userChannel = UserChannel.builder()
    .userEmail("<EMAIL>")
    .build();
```

#### Condition and Channel management policy

All conditions and channels are managed in the same way.

1. If you don't define any conditions in the policy, synchronization process will not modify already existing conditions.
   Example:
   ```java
   PolicyConfiguration configuration = PolicyConfiguration.builder()
       .policyName("Policy name")
       .incidentPreference(PolicyConfiguration.IncidentPreference.PER_POLICY)
       .build()
   ```
   This configuration will not remove, create or update any existing condition.
   
1. If you define some conditions in the policy, then:
    - conditions which are defined in configuration, but are missing will be created.
    - conditions which are defined in configuration and already exist will be updated.
    - conditions which are not defined in configuration, but already exist will be removed.
   Example:
   ```java
   PolicyConfiguration configuration = PolicyConfiguration.builder()
       .policyName("Policy name")
       .incidentPreference(PolicyConfiguration.IncidentPreference.PER_POLICY)
       .condition(condition1)
       .condition(condition2)
       .build()
   ```
   This configuration will create/update condition1 and condition2, and will remove all other conditions.
   
1. If you define empty conditions list in the policy, then synchronization process will remove all existing conditions.
   Example:
   ```java
   PolicyConfiguration configuration = PolicyConfiguration.builder()
       .policyName("Policy name")
       .incidentPreference(PolicyConfiguration.IncidentPreference.PER_POLICY)
       .conditions(Collections.emptyList())
       .build()
   ```
   This configuration will not create nor update any conditions, but will remove all existing ones.
   <file_sep>/newrelic-alerts-configurator-dsl/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/dsl/configuration/channel/SlackChannelDsl.kt
package com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.channel

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.SlackChannel
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.NewRelicConfigurationMarker

@NewRelicConfigurationMarker
class SlackChannelDsl {
    var channelName: String? = null
    var slackUrl: String? = null
    var teamChannel: String? = null
}

fun slackChannel(block: SlackChannelDsl.() -> Unit): SlackChannel {
    val dsl = SlackChannelDsl()
    dsl.block()

    return SlackChannel.builder()
            .channelName(requireNotNull(dsl.channelName) { "Slack channel name cannot be null" })
            .slackUrl(requireNotNull(dsl.slackUrl) {"Slack channel url cannot be null"})
            .teamChannel(dsl.teamChannel)
            .build()
}<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/AlertsChannelsApi.java
package com.ocadotechnology.newrelic.apiclient;

import com.ocadotechnology.newrelic.apiclient.model.channels.AlertsChannel;

import java.util.List;

public interface AlertsChannelsApi {
    /**
     * Lists all existing Alerts Channels.
     * This method supports pagination - which means it returns list of entries combined from all pages.
     *
     * @return list of all existing {@link AlertsChannel} from NewRelic
     */
    List<AlertsChannel> list();

    /**
     * Creates Alerts Channel.
     * <p>
     * Although New Relic returns list of channels in its response this method assumes there will be exactly one channel returned.
     *
     * @param channel channel definition to be created
     * @return created {@link AlertsChannel}
     */
    AlertsChannel create(AlertsChannel channel);

    /**
     * Deletes Alerts Channel.
     *
     * @param channelId id of the channel to be deleted
     * @return deleted {@link AlertsChannel}
     */
    AlertsChannel delete(int channelId);

    /**
     * Removes Alerts Channel from Policy definition.
     *
     * @param policyId  id of the policy to be updated
     * @param channelId id of the channel to be removed from the given policy
     * @return {@link AlertsChannel} for the given channel id regardless of being part of the specified policy
     */
    AlertsChannel deleteFromPolicy(int policyId, int channelId);
}
<file_sep>/newrelic-alerts-configurator-examples/README.md
# Sample

This is the sample project that uses NewRelic Alerts Configurator library to configure NewRelic alerts for few applications.
It contains configuration in both Java and Kotlin DSL

## Goals

We have **3** applications:
- `Application1` deployed on `app-1-host`
- `Application2` deployed on `app-2-host`
- `Application3` deployed on `app-3-host`

For **each** application we need to:
- Raise warning alert if **apdex** falls below `0.85`
- Raise critical alert if **apdex** falls below `0.7`
- Send alert notification on **slack** to `newrelic-alerts` channel
- In application configuration **application apdex** threshold should be configured to `0.5f`
- In application configuration **end user apdex** threshold should be configured to `0.7f`
- Real user monitoring have to be enabled
- Raise critical alert if **heap memory usage** is above `85%` for longer than 5 minutes
- Raise warning alert if **heap memory usage** is above `65%` for longer than 5 minutes

For **Application1** we need to additionally:
- Raise warning alert if **disk space** on application host raises above `65%`
- Raise critical alert if **disk space** on application host raises above `80%`
- Send alert notifications to **email** `<EMAIL>`

For **Application2** we need to additionally:
- Raise critical alert if **HealthStatus** returns false. Health statuses are available in **Application2:HealthStatus** transactions.
- Raise warning alert if there are above 1% page views with JS error within 5 min

For **Application3** we need to additionally:
- Raise warning alert if **cpu usage** on application host raised above `70%`
- Raise critical alert if **cpu usage** on application host raised above `90%`
- Send alert notifications to **email** `<EMAIL>`

Moreover, described configuration should be deployed on **two** different NewRelic account:
- one account monitoring staging environment
- one account monitoring production environment<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/PolicyItemConfigurator.java
package com.ocadotechnology.newrelic.alertsconfigurator;

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.PolicyConfiguration;
import lombok.NonNull;

interface PolicyItemConfigurator {
    void sync(@NonNull PolicyConfiguration config);
}
<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/condition/signal/SignalFillOption.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.signal;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum SignalFillOption {
    NONE("none"), LAST_KNOWN_VALUE("last_value"), STATIC("static");

    private final String value;

}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/ServersApi.java
package com.ocadotechnology.newrelic.apiclient;

import com.ocadotechnology.newrelic.apiclient.model.servers.Server;

import java.util.Optional;

public interface ServersApi {
    /**
     * Gets {@link Server} object using its name.
     *
     * @param serverName name of the server registered in NewRelic
     * @return Optional containing {@link Server} object, or empty if server not found
     */
    Optional<Server> getByName(String serverName);

    /**
     * Gets {@link Server} object using its id.
     *
     * @param serverId id of the server registered in NewRelic
     * @return found {@link Server}
     */
    Server getById(int serverId);
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/internal/model/AlertsNrqlConditionWrapper.java
package com.ocadotechnology.newrelic.apiclient.internal.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.ocadotechnology.newrelic.apiclient.model.conditions.nrql.AlertsNrqlCondition;
import lombok.Value;

@Value
public class AlertsNrqlConditionWrapper {
    @JsonProperty("nrql_condition")
    AlertsNrqlCondition nrqlCondition;
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/model/applications/ApplicationSummary.java
package com.ocadotechnology.newrelic.apiclient.model.applications;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;

@Value
public class ApplicationSummary {
    @JsonProperty("response_time")
    Float responseTime;
    @JsonProperty
    Float throughput;
    @JsonProperty("error_rate")
    Float errorRate;
    @JsonProperty("apdex_target")
    Float apdexTarget;
    @JsonProperty("apdex_score")
    Float apdexScore;
    @JsonProperty("host_count")
    Integer hostCount;
    @JsonProperty("instance_count")
    Integer instanceCount;
    @JsonProperty("concurrent_instance_count")
    Integer concurrentInstanceCount;
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/internal/NewRelicInternalApi.java
package com.ocadotechnology.newrelic.apiclient.internal;

import com.ocadotechnology.newrelic.apiclient.*;
import com.ocadotechnology.newrelic.apiclient.internal.client.NewRelicClient;
import com.ocadotechnology.newrelic.apiclient.internal.client.NewRelicClientFactory;
import com.ocadotechnology.newrelic.apiclient.model.conditions.AlertsCondition;
import com.ocadotechnology.newrelic.apiclient.model.conditions.external.AlertsExternalServiceCondition;
import com.ocadotechnology.newrelic.apiclient.model.conditions.nrql.AlertsNrqlCondition;
import com.ocadotechnology.newrelic.apiclient.model.conditions.synthetics.AlertsSyntheticsCondition;
import lombok.Getter;

/**
 * API facade - object exposing NewRelic API endpoints as Java methods. Requires API key.
 */
@Getter
public class NewRelicInternalApi {

    private final ApplicationsApi applicationsApi;

    private final AlertsChannelsApi alertsChannelsApi;

    private final AlertsPoliciesApi alertsPoliciesApi;

    private final PolicyItemApi<AlertsCondition> alertsConditionsApi;

    private final PolicyItemApi<AlertsNrqlCondition> alertsNrqlConditionsApi;

    private final PolicyItemApi<AlertsExternalServiceCondition> alertsExternalServiceConditionsApi;

    private final PolicyItemApi<AlertsSyntheticsCondition> alertsSyntheticsConditionApi;

    private final KeyTransactionsApi keyTransactionsApi;

    private final DeploymentsApi deploymentsApi;

    private final ServersApi serversApi;

    private final UsersApi usersApi;

    private final SyntheticsMonitorsApi syntheticsMonitorsApi;

    private final DashboardsApi dashboardsApi;

    /**
     * NewRelic API constructor.
     *
     * @param restApiUrl       NewRelic REST API URL, for example https://api.newrelic.com
     * @param syntheticsApiUrl NewRelic Synthetics API URL
     * @param clientFactory    NewRelic client factory
     */
    public NewRelicInternalApi(String restApiUrl, String syntheticsApiUrl, NewRelicClientFactory clientFactory) {
        NewRelicClient client = clientFactory.create(restApiUrl);
        applicationsApi = new DefaultApplicationsApi(client);
        alertsChannelsApi = new DefaultAlertsChannelsApi(client);
        alertsPoliciesApi = new DefaultAlertsPoliciesApi(client);
        alertsConditionsApi = new DefaultAlertsConditionsApi(client);
        alertsExternalServiceConditionsApi = new DefaultAlertsExternalServiceConditionsApi(client);
        alertsNrqlConditionsApi = new DefaultAlertsNrqlConditionsApi(client);
        alertsSyntheticsConditionApi = new DefaultAlertsSyntheticsConditionsApi(client);
        keyTransactionsApi = new DefaultKeyTransactionsApi(client);
        deploymentsApi = new DefaultDeploymentsApi(client);
        serversApi = new DefaultServersApi(client);
        usersApi = new DefaultUsersApi(client);
        dashboardsApi = new DefaultDashboardsApi(client);

        NewRelicClient syntheticsClient = clientFactory.create(syntheticsApiUrl);
        syntheticsMonitorsApi = new DefaultSyntheticsMonitorsApi(syntheticsClient);
    }
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/internal/DefaultKeyTransactionsApi.java
package com.ocadotechnology.newrelic.apiclient.internal;

import com.ocadotechnology.newrelic.apiclient.KeyTransactionsApi;
import com.ocadotechnology.newrelic.apiclient.internal.client.NewRelicClient;
import com.ocadotechnology.newrelic.apiclient.internal.model.KeyTransactionList;
import com.ocadotechnology.newrelic.apiclient.internal.model.KeyTransactionWrapper;
import com.ocadotechnology.newrelic.apiclient.model.transactions.KeyTransaction;
import org.glassfish.jersey.uri.UriComponent;

import java.util.Optional;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
import static org.glassfish.jersey.uri.UriComponent.Type.QUERY_PARAM_SPACE_ENCODED;

public class DefaultKeyTransactionsApi extends ApiBase implements KeyTransactionsApi {

    private static final String KEY_TRANSACTIONS_URL = "/v2/key_transactions.json";
    private static final String KEY_TRANSACTION_URL = "/v2/key_transactions/{key_transaction_id}.json";

    public DefaultKeyTransactionsApi(NewRelicClient client) {
        super(client);
    }

    @Override
    public Optional<KeyTransaction> getByName(String keyTransactionName) {
        String keyTransactionNameEncoded = UriComponent.encode(keyTransactionName, QUERY_PARAM_SPACE_ENCODED);
        return client
                .target(KEY_TRANSACTIONS_URL)
                .queryParam("filter[name]", keyTransactionNameEncoded)
                .request(APPLICATION_JSON_TYPE)
                .get(KeyTransactionList.class)
                .getSingle();
    }

    @Override
    public KeyTransaction getById(int keyTransactionId) {
        return client
                .target(KEY_TRANSACTION_URL)
                .resolveTemplate("key_transaction_id", keyTransactionId)
                .request(APPLICATION_JSON_TYPE)
                .get(KeyTransactionWrapper.class)
                .getKeyTransaction();
    }
}
<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/condition/SyntheticsCondition.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition;

import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;

/**
 * Synthetics condition.
 * Configuration parameters:
 * <ul>
 *     <li>{@link #conditionName}</li>
 *     <li>{@link #monitorName}</li>
 *     <li>{@link #enabled} (optional)</li>
 *     <li>{@link #runBookUrl} (optional)</li>
 * </ul>
 */
@Getter
@Builder
public class SyntheticsCondition {
    /**
     * Name of your Synthetics condition
     */
    @NonNull
    private String conditionName;
    /**
     * Name of Synthetics monitor
     */
    @NonNull
    private String monitorName;
    /**
     * If your Synthetics condition is enabled. Default is true
     */
    @Builder.Default
    private boolean enabled = true;
    /**
     * The runbook URL to display in notifications
     */
    private String runBookUrl;

}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/internal/DefaultDashboardsApi.java
package com.ocadotechnology.newrelic.apiclient.internal;


import com.ocadotechnology.newrelic.apiclient.DashboardsApi;
import com.ocadotechnology.newrelic.apiclient.internal.client.NewRelicClient;
import com.ocadotechnology.newrelic.apiclient.internal.model.DashboardList;
import com.ocadotechnology.newrelic.apiclient.internal.model.DashboardWrapper;
import com.ocadotechnology.newrelic.apiclient.model.dashboards.Dashboard;
import org.glassfish.jersey.uri.UriComponent;

import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation;
import java.util.List;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
import static org.glassfish.jersey.uri.UriComponent.Type.QUERY_PARAM_SPACE_ENCODED;

public class DefaultDashboardsApi extends ApiBase implements DashboardsApi {

    private static final String DASHBOARDS_URL = "/v2/dashboards.json";

    private static final String DASHBOARD_URL = "/v2/dashboards/{dashboard_id}.json";

    public DefaultDashboardsApi(NewRelicClient client) {
        super(client);
    }

    @Override
    public Dashboard getById(int dashboardId) {
        return client
                .target(DASHBOARD_URL)
                .resolveTemplate("dashboard_id", dashboardId)
                .request(APPLICATION_JSON_TYPE)
                .get(DashboardWrapper.class)
                .getDashboard();
    }

    @Override
    public List<Dashboard> getByTitle(String dashboardTitle) {
        String dashboardTitleEncoded = UriComponent.encode(dashboardTitle, QUERY_PARAM_SPACE_ENCODED);
        Invocation.Builder builder = client
                .target(DASHBOARDS_URL)
                .queryParam("filter[title]", dashboardTitleEncoded)
                .request(APPLICATION_JSON_TYPE);
        return getPageable(builder, DashboardList.class)
                .getList();
    }

    @Override
    public Dashboard create(Dashboard dashboard) {
        return client
                .target(DASHBOARDS_URL)
                .request(APPLICATION_JSON_TYPE)
                .post(Entity.entity(new DashboardWrapper(dashboard), APPLICATION_JSON_TYPE), DashboardWrapper.class)
                .getDashboard();
    }

    @Override
    public Dashboard update(Dashboard dashboard) {
        return client
                .target(DASHBOARD_URL)
                .resolveTemplate("dashboard_id", dashboard.getId())
                .request(APPLICATION_JSON_TYPE)
                .put(Entity.entity(new DashboardWrapper(dashboard), APPLICATION_JSON_TYPE), DashboardWrapper.class)
                .getDashboard();
    }

    @Override
    public Dashboard delete(int dashboardId) {
        return client
                .target(DASHBOARD_URL)
                .resolveTemplate("dashboard_id", dashboardId)
                .request(APPLICATION_JSON_TYPE)
                .delete(DashboardWrapper.class)
                .getDashboard();
    }
}
<file_sep>/README.md
# NewRelic Alerts Configurator

NewRelic Alerts Configurator can be used to configure NewRelic alerts for your application. Instead of defining alerts 
through UI you can define them in code.
It allows you to automatize alerts configuration, easily recover them in case of wipe out and have full history of changes in 
your version control system.

## How to use NewRelic Alerts Configurator

First step is to obtain API key for given NewRelic account. Then You can create configuration. For example:

```java
// Create alert condition
TermsConfiguration above90Percent = TermsConfiguration.builder()
        .operatorTerm(OperatorTerm.ABOVE)
        .thresholdTerm(90f)
        .durationTerm(DurationTerm.DURATION_5)
        .timeFunctionTerm(TimeFunctionTerm.ALL)
        .priorityTerm(PriorityTerm.CRITICAL)
        .build();

Condition cpuCondition = ServersMetricCondition.builder()
        .conditionName("CPU Usage")
        .enabled(true)
        .server("some-host")
        .metric(ServersMetricCondition.Metric.CPU_PERCENTAGE)
        .term(above90Percent)
        .build();

// Create email channel for notifications
Channel emailChannel = EmailChannel.builder()
        .channelName("Team email")
        .emailAddress("<EMAIL>")
        .build();

// Create policy
PolicyConfiguration policy = PolicyConfiguration.builder()
        .policyName("My application policy")
        .condition(cpuCondition)
        .channel(emailChannel)
        .build();

// Synchronize changes
Configurator configurator = new Configurator("MY_REST_API_KEY");
configurator.setPolicyConfigurations(Collections.singletonList(policy));
configurator.sync();
```

That's all!

This code creates alert policy that will raise critical alert whenever some-host's cpu usage will raise above 90% in last 5
 minutes. Information about alert will be emailed to <EMAIL>
 
More examples can be found in [newrelic-alerts-configurator-examples](newrelic-alerts-configurator-examples) module.

### Experimental configurator
NewRelic actively deprecates parts of it's REST API encouraging a shift towards GraphQL API. 
There is therefore an alternative configurator that uses [NewRelic GraphQL API](https://docs.newrelic.com/docs/apis/nerdgraph/get-started/introduction-new-relic-nerdgraph/) where possible.
At this point it's experimental.
```java
NewRelicApi api = new NewRelicHybridApi("MY_REST_API_KEY", MY_ACCOUNT_ID);
Configurator configurator = new Configurator(api);
```
 
## How to obtain New Relic REST API key and account ID

In above examples we used **MY_REST_API_KEY** and **MY_ACCOUNT_ID**.  
Details on how to obtain API key can be found in
[NewRelic's REST API docs](https://docs.newrelic.com/docs/apis/rest-api-v2/getting-started/api-keys)  
Details on how to obtain account ID can be found in
[NewRelic's account docs](https://docs.newrelic.com/docs/accounts/accounts-billing/account-structure/account-id/)

**Note that for some configurations you will need Admin User's New Relic API key!**

## Binaries

Library is available in Maven Central
```xml
<dependency>
    <groupId>com.ocadotechnology.newrelicalertsconfigurator</groupId>
    <artifactId>newrelic-alerts-configurator</artifactId>
    <version>5.0.1</version>
</dependency>
```

## Features

Detailed list of supported features can be found in [newrelic-alerts-configurator](newrelic-alerts-configurator/) module.

## NewRelic API Client

This project contains following library useful for developers that monitor their application using New Relic:
- [NewRelic API client](newrelic-api-client/)
  Client for New Relic rest API

## Testing

To run tests call:

```bash
./gradlew test
```

## Build

To build call:

```bash
./gradlew clean build
```

## Contributing

We encourage you to contribute to NewRelic Alerts Configurator. Please check out the [Contributing guide](CONTRIBUTING.md) for 
guidelines about how to proceed.

## License

This product is licensed under Apache License 2.0. For details see [license file](LICENSE)<file_sep>/newrelic-alerts-configurator-dsl/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/dsl/NewRelicConfigurationMarker.kt
package com.ocadotechnology.newrelic.alertsconfigurator.dsl

@DslMarker
annotation class NewRelicConfigurationMarker<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/internal/DefaultAlertsNrqlConditionsApi.java
package com.ocadotechnology.newrelic.apiclient.internal;

import com.ocadotechnology.newrelic.apiclient.PolicyItemApi;
import com.ocadotechnology.newrelic.apiclient.internal.client.NewRelicClient;
import com.ocadotechnology.newrelic.apiclient.internal.model.AlertsNrqlConditionList;
import com.ocadotechnology.newrelic.apiclient.internal.model.AlertsNrqlConditionWrapper;
import com.ocadotechnology.newrelic.apiclient.model.conditions.nrql.AlertsNrqlCondition;

import javax.ws.rs.client.Entity;
import java.util.List;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;

public class DefaultAlertsNrqlConditionsApi extends ApiBase implements PolicyItemApi<AlertsNrqlCondition> {

    private static final String CONDITIONS_URL = "/v2/alerts_nrql_conditions.json";
    private static final String CONDITION_URL = "/v2/alerts_nrql_conditions/{condition_id}.json";
    private static final String CONDITION_POLICY_URL = "/v2/alerts_nrql_conditions/policies/{policy_id}.json";

    public DefaultAlertsNrqlConditionsApi(NewRelicClient client) {
        super(client);
    }

    @Override
    public List<AlertsNrqlCondition> list(int policyId) {
        return getPageable(
                client.target(CONDITIONS_URL).queryParam("policy_id", policyId).request(APPLICATION_JSON_TYPE),
                AlertsNrqlConditionList.class)
                .getList();
    }

    @Override
    public AlertsNrqlCondition create(int policyId, AlertsNrqlCondition nrqlCondition) {
        return client
                .target(CONDITION_POLICY_URL)
                .resolveTemplate("policy_id", policyId)
                .request(APPLICATION_JSON_TYPE)
                .post(Entity.entity(new AlertsNrqlConditionWrapper(nrqlCondition), APPLICATION_JSON_TYPE),
                        AlertsNrqlConditionWrapper.class)
                .getNrqlCondition();
    }

    @Override
    public AlertsNrqlCondition update(int conditionId, AlertsNrqlCondition nrqlCondition) {
        return client
                .target(CONDITION_URL)
                .resolveTemplate("condition_id", conditionId)
                .request(APPLICATION_JSON_TYPE)
                .put(Entity.entity(new AlertsNrqlConditionWrapper(nrqlCondition), APPLICATION_JSON_TYPE),
                        AlertsNrqlConditionWrapper.class)
                .getNrqlCondition();
    }

    @Override
    public AlertsNrqlCondition delete(int policyId, int conditionId) {
        return client
                .target(CONDITION_URL)
                .resolveTemplate("condition_id", conditionId)
                .request(APPLICATION_JSON_TYPE)
                .delete(AlertsNrqlConditionWrapper.class)
                .getNrqlCondition();
    }
}
<file_sep>/newrelic-alerts-configurator-dsl/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/dsl/configuration/condition/Conditions.kt
package com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.condition

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.Condition
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.ExternalServiceCondition
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.NrqlCondition
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.SyntheticsCondition
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.NewRelicConfigurationMarker

@NewRelicConfigurationMarker
class Conditions {
    internal val conditions: MutableList<Condition> = mutableListOf()
    internal val externalServiceConditions: MutableList<ExternalServiceCondition> = mutableListOf()
    internal val nrqlConditions: MutableList<NrqlCondition> = mutableListOf()
    internal val syntheticsConditions: MutableList<SyntheticsCondition> = mutableListOf()

    fun apmApp(block: ApmAppConditionDsl.() -> Unit) = conditions.add(apmAppCondition(block))
    fun apmExternalService(block: ApmExternalServiceConditionDsl.() -> Unit) = externalServiceConditions.add(apmExternalServiceCondition(block))
    fun apmJvm(block: ApmJvmConditionDsl.() -> Unit) = conditions.add(apmJvmCondition(block))
    fun apmKeyTransation(block: ApmKeyTransactionConditionDsl.() -> Unit) = conditions.add(apmKeyTransactionCondition(block))
    fun browser(block: BrowserConditionDsl.() -> Unit) = conditions.add(browserCondition(block))
    fun nrql(block: NrqlConditionDsl.() -> Unit) = nrqlConditions.add(nrqlCondition(block))
    fun serversMetric(block: ServersMetricConditionDsl.() -> Unit) = conditions.add(serversMetricCondition(block))
    fun synthetics(block: SyntheticsConditionDsl.() -> Unit) = syntheticsConditions.add(syntheticsCondition(block))
    operator fun Condition.unaryPlus() = conditions.add(this)
    operator fun ExternalServiceCondition.unaryPlus() = externalServiceConditions.add(this)
    operator fun NrqlCondition.unaryPlus() = nrqlConditions.add(this)
    operator fun SyntheticsCondition.unaryPlus() = syntheticsConditions.add(this)
}<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/condition/UserDefinedConfiguration.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition;

import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;

/**
 * User defined metric configuration.
 * Configuration parameters:
 * <ul>
 *     <li>{@link #metric}</li>
 *     <li>{@link #valueFunction}</li>
 * </ul>
 */
@Builder
@Getter
public class UserDefinedConfiguration {
    /**
     * This is the name of a user defined custom metric to be used to determine if an event should be triggered.
     */
    @NonNull
    private String metric;
    /**
     * This is the numeric value obtained from the custom metric specified by {@link #metric}
     */
    @NonNull
    private ValueFunction valueFunction;

    public enum ValueFunction {
        AVERAGE, MIN, MAX, TOTAL, SAMPLE_SIZE;

        public String getAsString() {
            return this.name().toLowerCase();
        }
    }
}
<file_sep>/newrelic-alerts-configurator-dsl/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/dsl/configuration/channel/PagerDutyChannelDsl.kt
package com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.channel

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.PagerDutyChannel
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.NewRelicConfigurationMarker

@NewRelicConfigurationMarker
class PagerDutyChannelDsl {
    var channelName: String? = null
    var serviceKey: String? = null
}

fun pagerDutyChannel(block: PagerDutyChannelDsl.() -> Unit): PagerDutyChannel {
    val dsl = PagerDutyChannelDsl()
    dsl.block()

    return PagerDutyChannel.builder()
            .channelName(requireNotNull(dsl.channelName) { "PagerDuty channel name cannot be null" })
            .serviceKey(requireNotNull(dsl.serviceKey) { "PagerDuty channel key cannot be null" })
            .build()
}<file_sep>/newrelic-alerts-configurator-dsl/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/dsl/configuration/ApplicationConfigurationDsl.kt
package com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.ApplicationConfiguration
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.NewRelicConfigurationMarker

@NewRelicConfigurationMarker
class ApplicationConfigurationDsl {
    var applicationName: String = ""
    var appApdexThreshold: Float = 0.0f
    var endUserApdexThreshold: Float = 0.0f
    var enableRealUserMonitoring: Boolean = false
}

fun applicationConfiguration(block: ApplicationConfigurationDsl.() -> Unit): ApplicationConfiguration {
    val dsl = ApplicationConfigurationDsl()
    dsl.block()

    return ApplicationConfiguration.builder()
            .applicationName(dsl.applicationName)
            .appApdexThreshold(dsl.appApdexThreshold)
            .endUserApdexThreshold(dsl.endUserApdexThreshold)
            .enableRealUserMonitoring(dsl.enableRealUserMonitoring)
            .build()
}

@NewRelicConfigurationMarker
class ApplicationConfigurations {
    internal val applications:  MutableList<ApplicationConfiguration> = mutableListOf()

    fun application(block: ApplicationConfigurationDsl.() -> Unit) = applications.add(applicationConfiguration(block))
    operator fun ApplicationConfiguration.unaryPlus() = applications.add(this)
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/internal/model/ApplicationWrapper.java
package com.ocadotechnology.newrelic.apiclient.internal.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.ocadotechnology.newrelic.apiclient.model.applications.Application;
import lombok.Value;

@Value
public class ApplicationWrapper {
    @JsonProperty
    Application application;
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/model/conditions/nrql/AlertsNrqlCondition.java
package com.ocadotechnology.newrelic.apiclient.model.conditions.nrql;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.ocadotechnology.newrelic.apiclient.model.PolicyItem;
import com.ocadotechnology.newrelic.apiclient.model.conditions.Expiration;
import com.ocadotechnology.newrelic.apiclient.model.conditions.Signal;
import com.ocadotechnology.newrelic.apiclient.model.conditions.Terms;
import java.util.Collection;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Singular;
import lombok.Value;
import lombok.experimental.NonFinal;

/**
 * See <a href="https://docs.newrelic.com/docs/alerts-applied-intelligence/new-relic-alerts/advanced-alerts/rest-api-alerts/rest-api-calls-alerts/#conditions-list">Doc</a>
 */
@Value
@Builder
@AllArgsConstructor
@NonFinal
public class AlertsNrqlCondition implements PolicyItem {
    @JsonProperty
    Integer id;
    @JsonProperty
    String name;
    @JsonProperty
    Boolean enabled;
    @JsonProperty("runbook_url")
    String runbookUrl;
    @JsonProperty
    @Singular
    Collection<Terms> terms;
    @JsonProperty("value_function")
    String valueFunction;
    @JsonProperty
    Nrql nrql;
    @JsonProperty
    Signal signal;
    @JsonProperty
    Expiration expiration;
    @JsonProperty("violation_time_limit_seconds")
    Integer violationTimeLimitSeconds;
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/internal/model/AlertsChannelWrapper.java
package com.ocadotechnology.newrelic.apiclient.internal.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.ocadotechnology.newrelic.apiclient.model.channels.AlertsChannel;
import lombok.Value;

@Value
public class AlertsChannelWrapper {
    @JsonProperty
    AlertsChannel channel;
}
<file_sep>/newrelic-alerts-configurator-examples/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/examples/Main.kt
package com.ocadotechnology.newrelic.alertsconfigurator.examples

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.PolicyConfiguration
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.policyConfiguration
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration

object APP1 {
    private const val NAME = "Application1"

    val applicationConfig = applicationConfiguration(NAME)

    val policyConfig = policyConfiguration {
        policyName = "Application1 Policy"
        incidentPreference = PolicyConfiguration.IncidentPreference.PER_POLICY

        conditions {
            +apdexCondition(NAME)
            +diskSpaceCondition("app-1-host")
            +heapUsageCondition(NAME)
        }

        channels {
            +teamEmail()
            +slack()
        }
    }
}

object APP2 {
    private const val NAME = "Application2"

    val applicationConfig = applicationConfiguration(NAME)

    val policyConfig = policyConfiguration {
        policyName = "Application2 Policy"
        incidentPreference = PolicyConfiguration.IncidentPreference.PER_POLICY

        conditions {
            +apdexCondition(NAME)
            +heapUsageCondition(NAME)
            +jsErrorsCondition(NAME)
            +healthCheckCondition(NAME)
        }

        channels {
            +slack()
        }
    }
}

object APP3 {
    private const val NAME = "Application3"

    val applicationConfig = applicationConfiguration(NAME)

    val policyConfig = policyConfiguration {
        policyName = "Application3 Policy"
        incidentPreference = PolicyConfiguration.IncidentPreference.PER_POLICY

        conditions {
            +apdexCondition(NAME)
            +cpuUsageCondition("app-3-host")
            +heapUsageCondition(NAME)
        }

        channels {
            +teamEmail()
            +slack()
        }
    }
}

fun main() {
    val stagingEnvironmentNewRelicKey = "KEY1"
    val productionEnvironmentNewRelicKey = "KEY2"

    configure(stagingEnvironmentNewRelicKey)
    configure(productionEnvironmentNewRelicKey)
}

fun configure(newRelicKey: String) {
    configuration(newRelicKey) {
        applications {
            +APP1.applicationConfig
            +APP2.applicationConfig
            +APP3.applicationConfig
        }
        policies {
            +APP1.policyConfig
            +APP2.policyConfig
            +APP3.policyConfig
        }
    }
}<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/channel/ChannelTypeSupport.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel;

import com.ocadotechnology.newrelic.apiclient.NewRelicApi;
import com.ocadotechnology.newrelic.apiclient.model.channels.AlertsChannelConfiguration;

/**
 * This interface represents support object, that is used to transform implementation of {@link Channel} to
 * {@link AlertsChannelConfiguration}, thet will be used by api client.
 * Each implementation of {@link Channel} should provide it's own implementation of this interface, that will build correct
 * object of {@link AlertsChannelConfiguration}.
 */
public interface ChannelTypeSupport {

    /**
     * Builds alerts channel configuration.
     *
     * @param api NewRelic api client. Can be used to obtain additional paremeters required to build alerts channel
     *            configuration, like {@code userId}
     * @return generated alerts channel configuration
     */
    AlertsChannelConfiguration generateAlertsChannelConfiguration(NewRelicApi api);
}
<file_sep>/newrelic-alerts-configurator-dsl/src/main/kotlin/com/ocadotechnology/newrelic/alertsconfigurator/dsl/configuration/condition/terms/NrqlTermsConfigurationDsl.kt
package com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.condition.terms

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.*
import com.ocadotechnology.newrelic.alertsconfigurator.dsl.NewRelicConfigurationMarker

@NewRelicConfigurationMarker
class NrqlTermsConfigurationDsl {
    var durationTerm: NrqlDurationTerm? = null
    var operatorTerm: OperatorTerm? = null
    var priorityTerm: PriorityTerm? = null
    var thresholdTerm: Float? = null
    var timeFunctionTerm: TimeFunctionTerm? = null
}

fun nrqlTermsConfiguration(block: NrqlTermsConfigurationDsl.() -> Unit): NrqlTermsConfiguration {
    val dsl = NrqlTermsConfigurationDsl()
    dsl.block()

    return NrqlTermsConfiguration.builder()
            .durationTerm(requireNotNull(dsl.durationTerm) { "Nrql terms duration cannot be null" })
            .operatorTerm(requireNotNull(dsl.operatorTerm) { "Nrql terms operator cannot be null" })
            .priorityTerm(requireNotNull(dsl.priorityTerm) { "Nrql terms priority cannot be null" })
            .thresholdTerm(requireNotNull(dsl.thresholdTerm) { "Nrql terms threshold cannot be null" })
            .timeFunctionTerm(requireNotNull(dsl.timeFunctionTerm) { "Nrql terms time function cannot be null" })
            .build()
}

@NewRelicConfigurationMarker
class NrqlTermConfigurations {
    internal val terms: MutableList<NrqlTermsConfiguration> = mutableListOf()

    fun term(block: NrqlTermsConfigurationDsl.() -> Unit) = terms.add(com.ocadotechnology.newrelic.alertsconfigurator.dsl.configuration.condition.terms.nrqlTermsConfiguration(block))
    operator fun NrqlTermsConfiguration.unaryPlus() = terms.add(this)

    infix fun PriorityTerm.given(timeFunctionTerm: TimeFunctionTerm) : AfterGiven {
        return AfterGiven(NrqlTermsConfiguration.builder().priorityTerm(this).timeFunctionTerm(timeFunctionTerm))
    }

    infix fun AfterGiven.inLast(durationTerm: NrqlDurationTerm) : AfterInLast {
        return AfterInLast(this.builder.durationTerm(durationTerm))
    }

    infix fun AfterInLast.minutes(operatorTerm: OperatorTerm) : AfterMinutes {
        return AfterMinutes(this.builder.operatorTerm(operatorTerm))
    }

    infix fun AfterMinutes.value(thresholdTerm: Float) {
        terms.add(this.builder.thresholdTerm(thresholdTerm).build())
    }

    class AfterGiven(internal val builder: NrqlTermsConfiguration.NrqlTermsConfigurationBuilder)
    class AfterInLast(internal val builder: NrqlTermsConfiguration.NrqlTermsConfigurationBuilder)
    class AfterMinutes(internal val builder: NrqlTermsConfiguration.NrqlTermsConfigurationBuilder)
}<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/model/deployments/Deployment.java
package com.ocadotechnology.newrelic.apiclient.model.deployments;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;

import java.time.OffsetDateTime;

/**
 * See <a href="https://rpm.newrelic.com/api/explore/application_deployments/list">Doc</a>
 */
@Value
@Builder
@AllArgsConstructor
public class Deployment {
    @JsonProperty
    Integer id;
    @JsonProperty
    String revision;
    @JsonProperty
    String changelog;
    @JsonProperty
    String description;
    @JsonProperty
    String user;
    @JsonProperty
    OffsetDateTime timestamp;
    @JsonProperty
    DeploymentLinks links;
}
<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/condition/Condition.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition;

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.TermsConfiguration;

import java.util.Collection;

/**
 * Alerts condition configuration.
 * Implementations:
 * <ul>
 *     <li>{@link ApmAppCondition}</li>
 *     <li>{@link ApmKeyTransactionCondition}</li>
 *     <li>{@link ServersMetricCondition}</li>
 * </ul>
 */
public interface Condition {
    /**
     * Returns condition type. Each Condition implementation should have unique type.
     *
     * @return condition type
     */
    ConditionType getType();

    /**
     * Returns name of the condition.
     *
     * @return condition name
     */
    String getConditionName();

    /**
     * Returns if condition is enabled.
     *
     * @return {@code true} if condition is enabled, {@code false} otherwise
     */
    boolean isEnabled();

    /**
     * Returns collection of entities for which this condition is applied.
     * The type of the entity depends on concrete condition implementation. This can be an application
     * name or host name for example.
     * <p>
     * If entity with given name does not exist an exception will be thrown.
     *
     * @return entity names
     */
    Collection<String> getEntities();

    /**
     * Returns metric in string format.
     *
     * @return metric
     */
    String getMetricAsString();

    /**
     * Returns the runbook URL to display in notifications.
     *
     * @return runbook URL
     */
    String getRunBookUrl();

    /**
     * Returns collection of terms used for alerts condition.
     *
     * @return terms
     */
    Collection<TermsConfiguration> getTerms();

    /**
     * Returns condition scope in string format.
     *
     * @return condition scope
     */
    default String getConditionScopeAsString() {
        return null;
    }

    /**
     * Returns time (in hours) after which instance-based violations will automatically close.
     * @return violation close timer
     */
    default String getViolationCloseTimerAsString() {
        return null;
    }

    /**
     * Returns configuration for user defined metric. Should be provided when metric is set to USER_DEFINED.
     *
     * @return user defined metric
     */
    default UserDefinedConfiguration getUserDefinedMetric() {
        return null;
    }

    /**
     * Returns type of GC metric for jvm condition if metric set to GC_CPU_TIME.
     *
     * @return type of GC metric
     */
    default String getGcMetricAsString() {
        return null;
    }

}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/PolicyItemApi.java
package com.ocadotechnology.newrelic.apiclient;

import java.util.List;

public interface PolicyItemApi<T> {
    /**
     * Lists items for the given policy.
     *
     * @param policyId id of the policy containing items
     * @return list of all existing {@link T} from the given policy
     */
    List<T> list(int policyId);

    /**
     * Creates item instance within specified policy.
     *
     * @param policyId id of the policy to be updated
     * @param item     item definition to be created
     * @return created {@link T}
     */
    T create(int policyId, T item);

    /**
     * Updates item definition.
     *
     * @param id   id of the item to be updated
     * @param item item definition to be updated
     * @return created {@link T}
     */
    T update(int id, T item);

    /**
     * Deletes item.
     *
     * @param policyId policy id
     * @param id       id of the item to be deleted
     * @return deleted {@link T}
     */
    T delete(int policyId, int id);
}
<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/condition/terms/NrqlDurationTerm.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms;

import lombok.Getter;

@Getter
public final class NrqlDurationTerm {
//    defaults as used in original, enum-based implementation
    public static final NrqlDurationTerm DURATION_1 = new NrqlDurationTerm(1);
    public static final NrqlDurationTerm DURATION_2 = new NrqlDurationTerm(2);
    public static final NrqlDurationTerm DURATION_3 = new NrqlDurationTerm(3);
    public static final NrqlDurationTerm DURATION_4 = new NrqlDurationTerm(4);
    public static final NrqlDurationTerm DURATION_5 = new NrqlDurationTerm(5);
    public static final NrqlDurationTerm DURATION_10 = new NrqlDurationTerm(10);
    public static final NrqlDurationTerm DURATION_15 = new NrqlDurationTerm(15);
    public static final NrqlDurationTerm DURATION_30 = new NrqlDurationTerm(30);
    public static final NrqlDurationTerm DURATION_60 = new NrqlDurationTerm(60);
    public static final NrqlDurationTerm DURATION_120 = new NrqlDurationTerm(120);

    private final int duration;

    public NrqlDurationTerm(int duration) {
        assertMoreThanZero(duration);
        this.duration = duration;
    }

    private static void assertMoreThanZero(int duration) {
        if (duration <= 0) {
            throw new IllegalArgumentException("Duration Term for alert should not be 0 nor negative");
        }
    }
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/graphql/apiclient/internal/transport/NotificationChannel.java
package com.ocadotechnology.newrelic.graphql.apiclient.internal.transport;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Value;

@JsonIgnoreProperties(ignoreUnknown = true)
@Value
public class NotificationChannel {
    String id;
    String name;
    String type;
    AssociatedPolicies associatedPolicies;
    NotificationChannelConfig config;

}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/model/policies/AlertsPolicy.java
package com.ocadotechnology.newrelic.apiclient.model.policies;

import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;

/**
 * See <a href="https://rpm.newrelic.com/api/explore/alerts_policies/list">Doc</a>
 */
@Value
@Builder
@AllArgsConstructor
public class AlertsPolicy {
    @JsonProperty
    Integer id;
    @JsonProperty("incident_preference")
    @JsonAlias({"incidentPreference"})
    String incidentPreference;
    @JsonProperty
    String name;
    @JsonProperty("created_at")
    Long createdAt;
    @JsonProperty("updated_at")
    Long updatedAt;
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/model/deployments/DeploymentLinks.java
package com.ocadotechnology.newrelic.apiclient.model.deployments;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;

@Value
public class DeploymentLinks {
    @JsonProperty
    Integer application;
}
<file_sep>/newrelic-alerts-configurator-examples/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/examples/Defaults.java
package com.ocadotechnology.newrelic.alertsconfigurator.examples;

import com.ocadotechnology.newrelic.alertsconfigurator.configuration.ApplicationConfiguration;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.Channel;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.EmailChannel;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.channel.SlackChannel;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.ApmAppCondition;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.BrowserCondition;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.ApmJvmCondition;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.Condition;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.NrqlCondition;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.ServersMetricCondition;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.DurationTerm;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.NrqlDurationTerm;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.NrqlTermsConfiguration;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.OperatorTerm;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.PriorityTerm;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.TermsConfiguration;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.TimeFunctionTerm;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.violationclosetimer.ViolationCloseTimer;

import java.util.Arrays;

/**
 * Utility class used to obtain {@link Channel}, {@link Condition} or {@link ApplicationConfiguration} predefined
 * with default values.
 */
public final class Defaults {


    private Defaults() {
    }

    public static ApplicationConfiguration applicationConfiguration(String applicationName) {
        return ApplicationConfiguration.builder()
                .applicationName(applicationName)
                .appApdexThreshold(0.5f)
                .endUserApdexThreshold(0.7f)
                .enableRealUserMonitoring(true)
                .build();
    }

    public static Condition apdexCondition(String applicationName) {
        return ApmAppCondition.builder()
                .conditionName("Apdex")
                .enabled(true)
                .application(applicationName)
                .metric(ApmAppCondition.Metric.APDEX)
                .conditionScope(ApmAppCondition.ConditionScope.APPLICATION)
                // Raise critical if in the last 5 minutes apdex was below 0.7
                .term(TermsConfiguration.builder()
                        .durationTerm(DurationTerm.DURATION_5)
                        .operatorTerm(OperatorTerm.BELOW)
                        .priorityTerm(PriorityTerm.CRITICAL)
                        .timeFunctionTerm(TimeFunctionTerm.ALL)
                        .thresholdTerm(0.7f)
                        .build()
                )
                // Raise warning if in the last 5 minutes apdex was below 0.85
                .term(TermsConfiguration.builder()
                        .durationTerm(DurationTerm.DURATION_5)
                        .operatorTerm(OperatorTerm.BELOW)
                        .priorityTerm(PriorityTerm.WARNING)
                        .timeFunctionTerm(TimeFunctionTerm.ALL)
                        .thresholdTerm(0.85f)
                        .build()
                )
                .build();
    }

    public static Condition diskSpaceCondition(String... serverNames) {
        return ServersMetricCondition.builder()
                .conditionName("Fullest Disk %")
                .enabled(true)
                .servers(Arrays.asList(serverNames))
                .metric(ServersMetricCondition.Metric.FULLEST_DISK_PERCENTAGE)
                // Raise critical if in the last 5 minutes disk was over 80% full
                .term(TermsConfiguration.builder()
                        .priorityTerm(PriorityTerm.CRITICAL)
                        .durationTerm(DurationTerm.DURATION_5)
                        .operatorTerm(OperatorTerm.ABOVE)
                        .timeFunctionTerm(TimeFunctionTerm.ALL)
                        .thresholdTerm(80.0f)
                        .build()
                )
                // Raise warning if in the last 5 minutes disk was over 65% full
                .term(TermsConfiguration.builder()
                        .priorityTerm(PriorityTerm.WARNING)
                        .durationTerm(DurationTerm.DURATION_5)
                        .operatorTerm(OperatorTerm.ABOVE)
                        .timeFunctionTerm(TimeFunctionTerm.ALL)
                        .thresholdTerm(65.0f)
                        .build()
                )
                .build();
    }

    public static Condition cpuUsageCondition(String... serverNames) {
        return ServersMetricCondition.builder()
                .conditionName("CPU Usage %")
                .enabled(true)
                .servers(Arrays.asList(serverNames))
                .metric(ServersMetricCondition.Metric.CPU_PERCENTAGE)
                // Raise critical if in the last 5 minutes cpu was over 90%
                .term(TermsConfiguration.builder()
                        .priorityTerm(PriorityTerm.CRITICAL)
                        .durationTerm(DurationTerm.DURATION_5)
                        .operatorTerm(OperatorTerm.ABOVE)
                        .timeFunctionTerm(TimeFunctionTerm.ALL)
                        .thresholdTerm(90.0f)
                        .build()
                )
                // Raise warning if in the last 5 minutes cpu was over 70%
                .term(TermsConfiguration.builder()
                        .priorityTerm(PriorityTerm.WARNING)
                        .durationTerm(DurationTerm.DURATION_5)
                        .operatorTerm(OperatorTerm.ABOVE)
                        .timeFunctionTerm(TimeFunctionTerm.ALL)
                        .thresholdTerm(70.0f)
                        .build()
                )
                .build();
    }

    public static Condition heapUsageCondition(String applicationName) {
        return ApmJvmCondition.builder()
                .conditionName("Heap usage")
                .enabled(true)
                .application(applicationName)
                .metric(ApmJvmCondition.Metric.HEAP_MEMORY_USAGE)
                // Raise critical if in the last 5 minutes heap memory usage was above 85%
                .term(TermsConfiguration.builder()
                        .durationTerm(DurationTerm.DURATION_5)
                        .operatorTerm(OperatorTerm.ABOVE)
                        .priorityTerm(PriorityTerm.CRITICAL)
                        .timeFunctionTerm(TimeFunctionTerm.ALL)
                        .thresholdTerm(85f)
                        .build()
                )
                // Raise warning if in the last 5 minutes heap memory usage was above 65%
                .term(TermsConfiguration.builder()
                        .durationTerm(DurationTerm.DURATION_5)
                        .operatorTerm(OperatorTerm.ABOVE)
                        .priorityTerm(PriorityTerm.WARNING)
                        .timeFunctionTerm(TimeFunctionTerm.ALL)
                        .thresholdTerm(65f)
                        .build()
                )
                .violationCloseTimer(ViolationCloseTimer.DURATION_24)
                .build();
    }

    public static NrqlCondition healthCheckCondition(String applicationName) {
        return NrqlCondition.builder()
                .conditionName("Health Check")
                .enabled(true)
                .valueFunction(NrqlCondition.ValueFunction.SINGLE_VALUE)
                .term(NrqlTermsConfiguration.builder()
                        .priorityTerm(PriorityTerm.CRITICAL)
                        .durationTerm(NrqlDurationTerm.DURATION_1)
                        .timeFunctionTerm(TimeFunctionTerm.ANY)
                        .operatorTerm(OperatorTerm.ABOVE)
                        .thresholdTerm(0f)
                        .build())
                .query("SELECT count(*) FROM `" + applicationName + ":HealthStatus` WHERE healthy IS false")
                .build();
    }

    public static BrowserCondition jsErrorsCondition(String applicationName) {
        return BrowserCondition.builder()
                .conditionName("Page views with JS errors")
                .enabled(true)
                .application(applicationName)
                .metric(BrowserCondition.Metric.PAGE_VIEWS_WITH_JS_ERRORS)
                .term(TermsConfiguration.builder()
                    .durationTerm(DurationTerm.DURATION_5)
                    .operatorTerm(OperatorTerm.ABOVE)
                    .priorityTerm(PriorityTerm.WARNING)
                    .thresholdTerm(1f)
                    .timeFunctionTerm(TimeFunctionTerm.ANY)
                    .build()
                )
                .build();
    }

    public static Channel teamEmailChannel() {
        return EmailChannel.builder()
                .channelName("My Team - email")
                .emailAddress("<EMAIL>")
                .includeJsonAttachment(true)
                .build();
    }

    public static Channel slackChannel() {
        return SlackChannel.builder()
                .channelName("My Team - slack")
                .slackUrl("https://hooks.slack.com/services/aaaaaaaaa/bbbbbbbbb/cccccccccccccccccccccccc")
                .teamChannel("newrelic-alerts")
                .build();
    }
}
<file_sep>/newrelic-alerts-configurator/src/main/java/com/ocadotechnology/newrelic/alertsconfigurator/configuration/condition/signal/NrqlSignalConfiguration.java
package com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.signal;

import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;

/**
 * NRQL Signal configuration.
 * Configuration parameters:
 * <ul>
 *     <li>{@link #aggregationMethod}</li>
 *     <li>{@link #aggregationWindow}</li>
 *     <li>{@link #signalFillOption}</li>
 *     <li>{@link #signalFillValue}</li>
 *     <li>{@link #signalLostConfiguration}</li>
 * </ul>
 */
@Builder
@Getter
public class NrqlSignalConfiguration {

    /**
     * Configuration of signal aggregation method.
     * For reference see: <a href="https://docs.newrelic.com/docs/alerts-applied-intelligence/new-relic-alerts/advanced-alerts/understand-technical-concepts/streaming-alerts-key-terms-concepts/#aggregation-methods">NR Docs</a>
     */
    @NonNull
    private SignalAggregationMethod aggregationMethod;

    /**
     * Delay is how long we wait for events that belong in each aggregation window.
     * Depending on your data a longer delay may increase accuracy but delay notifications.
     * This value is used for EVENT_FLOW and CADENCE aggregation methods.
     */
    private Integer aggregationDelay;

    /**
     * The timer tells us how long to wait after each data point to make sure we've processed the whole batch.
     * This value should match data frequency, and preferably be at least equal to aggregationWindow.
     * See <a href="https://docs.newrelic.com/docs/alerts-applied-intelligence/new-relic-alerts/advanced-alerts/understand-technical-concepts/streaming-alerts-key-terms-concepts/#aggregation-methods">NR Docs</a> for more information.
     * This value is used for EVENT_TIMER aggregation method.
     */
    private Integer aggregationTimer;

    /**
     * Time (in seconds) for how long NewRelic collects data before running the NRQL query.
     */
    @NonNull
    private Integer aggregationWindow;

    /**
     * Configuration of filling data gaps/signal lost.
     */
    @NonNull
    private SignalFillOption signalFillOption;

    /**
     * Value for filling data gaps/signal lost
     */
    private String signalFillValue;

    /**
     * Configuration of signal lost behaviour.
     */
    private SignalLostConfiguration signalLostConfiguration;

}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/graphql/apiclient/internal/transport/NrqlCondition.java
package com.ocadotechnology.newrelic.graphql.apiclient.internal.transport;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Value;

import java.util.List;

@JsonIgnoreProperties(ignoreUnknown = true)
@Value
public class NrqlCondition {
    Boolean enabled;
    String id;
    String name;
    String type;
    String runbookUrl;
    Nrql nrql;
    Expiration expiration;
    Signal signal;
    List<Term> terms;
    String valueFunction;

    @JsonIgnoreProperties(ignoreUnknown = true)
    @Value
    public static class Nrql {
        String query;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    @Value
    public static class Expiration {
        String expirationDuration;
        boolean openViolationOnExpiration;
        boolean closeViolationsOnExpiration;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    @Value
    public static class Signal {
        String aggregationMethod;
        String aggregationDelay;
        String aggregationTimer;
        String aggregationWindow;
        String fillOption;
        String fillValue;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    @Value
    public static class Term {
        String operator;
        String priority;
        String threshold;
        String thresholdDuration;
        String thresholdOccurrences;
    }
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/model/dashboards/widget/WidgetData.java
package com.ocadotechnology.newrelic.apiclient.model.dashboards.widget;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;

@Value
@Builder(toBuilder = true)
@AllArgsConstructor
public class WidgetData {
    @JsonProperty
    String nrql;
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/graphql/apiclient/internal/transport/Result.java
package com.ocadotechnology.newrelic.graphql.apiclient.internal.transport;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.ocadotechnology.newrelic.apiclient.model.policies.AlertsPolicy;
import com.ocadotechnology.newrelic.graphql.apiclient.internal.transport.mutation.EmptyMutationResult;
import com.ocadotechnology.newrelic.graphql.apiclient.internal.transport.mutation.MutationResult;
import com.ocadotechnology.newrelic.graphql.apiclient.internal.transport.mutation.NotificationChannelResult;
import com.ocadotechnology.newrelic.graphql.apiclient.internal.transport.mutation.NotificationChannelsResult;
import lombok.AllArgsConstructor;
import lombok.ToString;
import lombok.Value;

import java.util.*;


@JsonIgnoreProperties(ignoreUnknown = true)
@ToString
@AllArgsConstructor
public class Result {
    private final Data data;

    private final List<Error> errors;

    @JsonIgnoreProperties(ignoreUnknown = true)
    @Value
    static class Data {
        Actor actor;
        AlertsPolicyCreate alertsPolicyCreate;
        EmptyMutationResult alertsNotificationChannelDelete;
        NotificationChannelsResult alertsNotificationChannelsAddToPolicy;
        NotificationChannelsResult alertsNotificationChannelsRemoveFromPolicy;

        NotificationChannelResult alertsNotificationChannelCreate;

        NrqlCondition alertsNrqlConditionStaticCreate;
        NrqlCondition alertsNrqlConditionStaticUpdate;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    @Value
    static class Actor {
        EntitySearch entitySearch;
        Account account;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    @Value
    static class AlertsPolicyCreate {
        String id;
        String name;
        String incidentPreference;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    @Value
    static class Account {
        Alerts alerts;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    @Value
    static class Alerts {
        AlertsPolicy policy;
        NotificationChannel notificationChannel;
        NotificationChannels notificationChannels;
        PoliciesSearch policiesSearch;
        NrqlConditionsSearch nrqlConditionsSearch;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    @Value
    static class PoliciesSearch {
        List<AlertsPolicy> policies;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    @Value
    static class EntitySearch {
        Results results;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    @Value
    static class Results {
        List<Map<Object, Object>> entities;
    }

    public boolean hasErrors() {
        return Objects.nonNull(errors);
    }

    public boolean notFound() {
        return Optional.ofNullable(errors)
                .orElseGet(ArrayList::new).stream()
                .anyMatch(error -> "Not Found".equalsIgnoreCase(error.getMessage()));
    }

    public List<Error> getErrors() {
        return Optional.ofNullable(errors).orElseGet(ArrayList::new);
    }

    public List<AlertsPolicy> getPolicies() {
        return getAlerts()
                .map(alerts -> alerts.policiesSearch)
                .map(policiesSearch -> policiesSearch.policies).orElseGet(ArrayList::new);
    }

    public Optional<AlertsPolicy> getAlertsPolicyCreate() {
        return getData()
                .map(data -> data.alertsPolicyCreate)
                .map(alertsPolicyCreate -> AlertsPolicy.builder()
                        .id(Optional.ofNullable(alertsPolicyCreate.id).map(Integer::parseInt).orElse(null))
                        .name(alertsPolicyCreate.name)
                        .incidentPreference(alertsPolicyCreate.incidentPreference)
                        .build());
    }

    public Optional<MutationResult<List<Integer>>> getAlertsNotificationChannelsAddToPolicy() {
        return getData().map(data -> data.alertsNotificationChannelsAddToPolicy);
    }

    public Optional<MutationResult<List<Integer>>> getAlertsNotificationChannelsRemoveFromPolicy() {
        return getData().map(data -> data.alertsNotificationChannelsRemoveFromPolicy);
    }

    public Optional<NotificationChannelResult> getAlertsNotificationChannelCreate() {
        return getData().map(data -> data.alertsNotificationChannelCreate);
    }

    public Optional<MutationResult<Void>> getAlertsNotificationChannelDelete() {
        return getData().map(data -> data.alertsNotificationChannelDelete);
    }

    public Optional<AlertsPolicy> getAlertsPolicy() {
        return getAlerts().map(alerts -> alerts.policy);
    }

    public Optional<NotificationChannel> getNotificationChannel() {
        return getAlerts().map(alerts -> alerts.notificationChannel);
    }

    public Optional<NotificationChannels> getNotificationChannels() {
        return getAlerts().map(alerts -> alerts.notificationChannels);
    }

    public Optional<NrqlConditionsSearch> getNrqlConditionsSearch() {
        return getAlerts().map(alerts -> alerts.nrqlConditionsSearch);
    }

    public Optional<NrqlCondition> getAlertsNrqlConditionStaticCreate() {
        return getData().map(data -> data.alertsNrqlConditionStaticCreate);
    }

    public Optional<NrqlCondition> getAlertsNrqlConditionStaticUpdate() {
        return getData().map(data -> data.alertsNrqlConditionStaticUpdate);
    }

    private Optional<Data> getData() {
        return Optional.ofNullable(data);
    }

    private Optional<Alerts> getAlerts() {
        return getData()
                .map(data -> data.actor)
                .map(actor -> actor.account)
                .map(account -> account.alerts);
    }

}
<file_sep>/newrelic-alerts-configurator/src/test/java/com/ocadotechnology/newrelic/alertsconfigurator/AbstractPolicyItemConfiguratorTest.java
package com.ocadotechnology.newrelic.alertsconfigurator;

import com.google.common.collect.ImmutableList;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.PolicyConfiguration;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.ApmAppCondition;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.terms.*;
import com.ocadotechnology.newrelic.alertsconfigurator.configuration.condition.violationclosetimer.ViolationCloseTimer;
import com.ocadotechnology.newrelic.alertsconfigurator.exception.NewRelicSyncException;
import com.ocadotechnology.newrelic.alertsconfigurator.internal.entities.EntityResolver;
import com.ocadotechnology.newrelic.apiclient.PolicyItemApi;
import com.ocadotechnology.newrelic.apiclient.model.PolicyItem;
import com.ocadotechnology.newrelic.apiclient.model.policies.AlertsPolicy;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.InOrder;
import org.mockito.Mock;

import java.util.Collections;
import java.util.Optional;

import static java.lang.String.format;
import static org.mockito.Mockito.*;

public abstract class AbstractPolicyItemConfiguratorTest<T extends PolicyItemConfigurator, U extends PolicyItem> extends AbstractConfiguratorTest {
    @Rule
    public final ExpectedException expectedException = ExpectedException.none();

    protected static final ApmAppCondition.Metric APP_METRIC = ApmAppCondition.Metric.APDEX;
    protected static final String POLICY_NAME = "policyName";
    protected static final PolicyConfiguration.IncidentPreference INCIDENT_PREFERENCE = PolicyConfiguration.IncidentPreference.PER_POLICY;
    protected static final AlertsPolicy POLICY = AlertsPolicy.builder()
            .id(42)
            .name(POLICY_NAME)
            .incidentPreference(INCIDENT_PREFERENCE.name())
            .build();


    protected static final String CONDITION_NAME = "conditionName";
    protected static final ApmAppCondition.ConditionScope CONDITION_SCOPE = ApmAppCondition.ConditionScope.INSTANCE;
    protected static final boolean ENABLED = true;
    protected static final String APPLICATION_NAME = "applicationName";
    protected static final int APPLICATION_ENTITY_ID = 1;
    protected static final ViolationCloseTimer VIOLATION_CLOSE_TIMER = ViolationCloseTimer.DURATION_1;
    protected static final TermsConfiguration TERMS_CONFIGURATION = createTermsConfiguration().build();

    @Mock
    EntityResolver entityResolverMock;
    protected T testee;

    @Before
    public void setUp() {
        when(alertsPoliciesApiMock.getByName(POLICY_NAME)).thenReturn(Optional.of(POLICY));
    }

    @Test
    public void shouldThrowException_whenPolicyDoesNotExist() {
        // given
        when(alertsPoliciesApiMock.getByName(POLICY_NAME)).thenReturn(Optional.empty());

        // then - exception
        expectedException.expect(NewRelicSyncException.class);
        expectedException.expectMessage(format("Policy %s does not exist", POLICY_NAME));

        // when
        testee.sync(getPolicyConfiguration());
    }

    @Test
    public void shouldDoNothing_whenNullConditionsInConfiguration() {
        PolicyItemApi<U> itemApiMock = getPolicyItemApiMock();

        // given
        PolicyConfiguration config = PolicyConfiguration.builder()
            .policyName(POLICY_NAME)
            .incidentPreference(PolicyConfiguration.IncidentPreference.PER_POLICY)
            .build();

        // when
        testee.sync(config);

        // then
        verifyZeroInteractions(itemApiMock);
    }

    @Test
    public void shouldDoNothing_whenEmptyConditionsInConfiguration() {
        PolicyItemApi<U> itemApiMock = getPolicyItemApiMock();

        // given
        PolicyConfiguration config = PolicyConfiguration.builder()
                .policyName(POLICY_NAME)
                .incidentPreference(PolicyConfiguration.IncidentPreference.PER_POLICY)
                .conditions(Collections.emptyList())
                .nrqlConditions(Collections.emptyList())
                .externalServiceConditions(Collections.emptyList())
                .build();

        // when
        testee.sync(config);

        // then
        InOrder order = inOrder(itemApiMock);
        order.verify(itemApiMock).list(POLICY.getId());
        order.verifyNoMoreInteractions();
    }

    @Test
    public void shouldThrowException_whenConditionScopeInstanceButViolationCloseTimerIsNotSet() throws Exception {
        // given

        // then - exception
        expectedException.expect(NullPointerException.class);
        expectedException.expectMessage("violationCloseTimer");

        // when
        ApmAppCondition.builder()
                .conditionName("condition name")
                .enabled(ENABLED)
                .application(APPLICATION_NAME)
                .metric(APP_METRIC)
                .conditionScope(ApmAppCondition.ConditionScope.INSTANCE)
                .term(TERMS_CONFIGURATION)
                .build();
    }


    @Test
    public void shouldCreateCondition() {
        U itemFromConfig = getItemFromConfig();
        U itemSame = getItemSame();
        PolicyConfiguration policyConfiguration = getPolicyConfiguration();
        PolicyItemApi<U> itemApiMock = getPolicyItemApiMock();

        // given
        when(itemApiMock.list(POLICY.getId())).thenReturn(ImmutableList.of());
        when(itemApiMock.create(POLICY.getId(), itemFromConfig)).thenReturn(itemSame);

        // when
        testee.sync(policyConfiguration);

        // then
        InOrder order = inOrder(itemApiMock);
        order.verify(itemApiMock).list(POLICY.getId());
        order.verify(itemApiMock).create(POLICY.getId(), itemFromConfig);
        order.verifyNoMoreInteractions();
    }


    @Test
    public void shouldUpdateCondition() {
        U itemFromConfig = getItemFromConfig();
        U itemUpdated = getItemUpdated();
        PolicyConfiguration policyConfiguration = getPolicyConfiguration();
        PolicyItemApi<U> itemApiMock = getPolicyItemApiMock();

        // given
        when(itemApiMock.list(POLICY.getId())).thenReturn(ImmutableList.of(itemUpdated));
        when(itemApiMock.update(itemUpdated.getId(), itemFromConfig)).thenReturn(itemUpdated);

        // when
        testee.sync(policyConfiguration);

        // then
        InOrder order = inOrder(itemApiMock);
        order.verify(itemApiMock).list(POLICY.getId());
        order.verify(itemApiMock).update(itemUpdated.getId(), itemFromConfig);
        order.verifyNoMoreInteractions();
    }

    @Test
    public void shouldRemoveOldCondition() {
        U itemFromConfig = getItemFromConfig();
        U itemSame = getItemSame();
        U itemDifferent = getItemDifferent();
        PolicyConfiguration policyConfiguration = getPolicyConfiguration();
        PolicyItemApi<U> itemApiMock = getPolicyItemApiMock();

        // given
        when(itemApiMock.list(POLICY.getId())).thenReturn(ImmutableList.of(itemDifferent));
        when(itemApiMock.create(POLICY.getId(), itemFromConfig)).thenReturn(itemSame);

        // when
        testee.sync(policyConfiguration);

        // then
        InOrder order = inOrder(itemApiMock);
        order.verify(itemApiMock).list(POLICY.getId());
        order.verify(itemApiMock).create(POLICY.getId(), itemFromConfig);
        order.verify(itemApiMock).delete(POLICY.getId(), itemDifferent.getId());
        order.verifyNoMoreInteractions();
    }

    protected abstract PolicyConfiguration getPolicyConfiguration();

    protected abstract PolicyItemApi<U> getPolicyItemApiMock();

    protected abstract U getItemFromConfig();

    protected abstract U getItemSame();

    protected abstract U getItemUpdated();

    protected abstract U getItemDifferent();


    private static TermsConfiguration.TermsConfigurationBuilder createTermsConfiguration() {
        return TermsConfiguration.builder()
                .durationTerm(DurationTerm.DURATION_5)
                .operatorTerm(OperatorTerm.ABOVE)
                .priorityTerm(PriorityTerm.CRITICAL)
                .thresholdTerm(0.5f)
                .timeFunctionTerm(TimeFunctionTerm.ALL);
    }

    static ApmAppCondition createAppCondition(String conditionName) {
        return ApmAppCondition.builder()
                .conditionName(conditionName)
                .enabled(ENABLED)
                .application(APPLICATION_NAME)
                .metric(APP_METRIC)
                .conditionScope(CONDITION_SCOPE)
                .violationCloseTimer(VIOLATION_CLOSE_TIMER)
                .term(TERMS_CONFIGURATION)
                .build();
    }
}
<file_sep>/newrelic-api-client/src/main/java/com/ocadotechnology/newrelic/apiclient/internal/DefaultAlertsChannelsApi.java
package com.ocadotechnology.newrelic.apiclient.internal;

import com.ocadotechnology.newrelic.apiclient.AlertsChannelsApi;
import com.ocadotechnology.newrelic.apiclient.internal.client.NewRelicClient;
import com.ocadotechnology.newrelic.apiclient.internal.model.AlertsChannelList;
import com.ocadotechnology.newrelic.apiclient.internal.model.AlertsChannelWrapper;
import com.ocadotechnology.newrelic.apiclient.model.channels.AlertsChannel;

import javax.ws.rs.client.Entity;
import java.util.List;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;

public class DefaultAlertsChannelsApi extends ApiBase implements AlertsChannelsApi {

    private static final String CHANNELS_URL = "/v2/alerts_channels.json";
    private static final String CHANNEL_URL = "/v2/alerts_channels/{channel_id}.json";
    private static final String POLICY_CHANNELS_URL = "/v2/alerts_policy_channels.json";

    public DefaultAlertsChannelsApi(NewRelicClient client) {
        super(client);
    }

    @Override
    public List<AlertsChannel> list() {
        return getPageable(
                client.target(CHANNELS_URL).request(APPLICATION_JSON_TYPE),
                AlertsChannelList.class)
                .getList();
    }

    @Override
    public AlertsChannel create(AlertsChannel channel) {
        return client
                .target(CHANNELS_URL)
                .request(APPLICATION_JSON_TYPE)
                .post(Entity.entity(new AlertsChannelWrapper(channel), APPLICATION_JSON_TYPE), AlertsChannelList.class)
                .getSingle()
                .orElseThrow(() -> new IllegalStateException("Failed to create channel: empty list returned"));
    }

    @Override
    public AlertsChannel delete(int channelId) {
        return client
                .target(CHANNEL_URL)
                .resolveTemplate("channel_id", channelId)
                .request(APPLICATION_JSON_TYPE)
                .delete(AlertsChannelWrapper.class)
                .getChannel();
    }

    @Override
    public AlertsChannel deleteFromPolicy(int policyId, int channelId) {
        return client
                .target(POLICY_CHANNELS_URL)
                .queryParam("policy_id", policyId)
                .queryParam("channel_id", channelId)
                .request(APPLICATION_JSON_TYPE)
                .delete(AlertsChannelWrapper.class)
                .getChannel();
    }
}
