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

com.devcycle.sdk.server.cloud.api.DevCycleCloudClient Maven / Gradle / Ivy

package com.devcycle.sdk.server.cloud.api;

import com.devcycle.sdk.server.cloud.model.DevCycleCloudOptions;
import com.devcycle.sdk.server.common.api.IDevCycleApi;
import com.devcycle.sdk.server.common.api.IDevCycleClient;
import com.devcycle.sdk.server.common.exception.DevCycleException;
import com.devcycle.sdk.server.common.logging.DevCycleLogger;
import com.devcycle.sdk.server.common.model.*;
import com.devcycle.sdk.server.common.model.Variable.TypeEnum;
import com.devcycle.sdk.server.openfeature.DevCycleProvider;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import dev.openfeature.sdk.FeatureProvider;
import retrofit2.Call;
import retrofit2.Response;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public final class DevCycleCloudClient implements IDevCycleClient {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private final IDevCycleApi api;
    private final DevCycleCloudOptions dvcOptions;
    private final DevCycleProvider openFeatureProvider;

    public DevCycleCloudClient(String sdkKey) {
        this(sdkKey, DevCycleCloudOptions.builder().build());
    }

    public DevCycleCloudClient(String sdkKey, DevCycleCloudOptions options) {
        if (sdkKey == null || sdkKey.equals("")) {
            throw new IllegalArgumentException("Missing environment key! Call initialize with a valid environment key");
        }

        if (!isValidServerKey(sdkKey)) {
            throw new IllegalArgumentException("Invalid environment key provided. Please call initialize with a valid server environment key");
        }

        if (options.getCustomLogger() != null) {
            DevCycleLogger.setCustomLogger(options.getCustomLogger());
        }

        this.dvcOptions = options;
        api = new DevCycleCloudApiClient(sdkKey, options).initialize();
        OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);

        this.openFeatureProvider = new DevCycleProvider(this);
    }

    /**
     * Get all features for user data
     *
     * @param user (required)
     * @return Map>String, Feature<
     */
    public Map allFeatures(DevCycleUser user) throws DevCycleException {
        validateUser(user);

        Call> response = api.getFeatures(user, dvcOptions.getEnableEdgeDB());
        return getResponse(response);
    }

    @Override
    public boolean isInitialized() {
        // Cloud Bucketing SDKs always count as initialized
        return true;
    }

    /**
     * Get variable value by key for user data
     *
     * @param user         (required)
     * @param key          Feature key (required)
     * @param defaultValue Default value to use if the variable could not be fetched
     *                     (required)
     * @return Variable value
     * @throws IllegalArgumentException If there are any issues with the key or default value provided
     */
    public  T variableValue(DevCycleUser user, String key, T defaultValue) {
        return variable(user, key, defaultValue).getValue();
    }

    /**
     * Get variable by key for user data
     *
     * @param user         (required)
     * @param key          Variable key (required)
     * @param defaultValue Default value to use if the variable could not be fetched (required)
     * @return Variable
     * @throws IllegalArgumentException If there are any issues with the key or default value provided
     */
    @SuppressWarnings("unchecked")
    public  Variable variable(DevCycleUser user, String key, T defaultValue) {
        validateUser(user);

        if (key == null || key.equals("")) {
            ErrorResponse errorResponse = new ErrorResponse(500, "Missing parameter: key", null);
            throw new IllegalArgumentException("Missing parameter: key");
        }

        if (defaultValue == null) {
            ErrorResponse errorResponse = new ErrorResponse(500, "Missing parameter: defaultValue", null);
            throw new IllegalArgumentException("Missing parameter: defaultValue");
        }

        TypeEnum variableType = TypeEnum.fromClass(defaultValue.getClass());
        Variable variable;

        try {
            Call response = api.getVariableByKey(user, key, dvcOptions.getEnableEdgeDB());
            variable = getResponseWithRetries(response, 5);
            if (variable.getType() != variableType) {
                throw new IllegalArgumentException("Variable type mismatch, returning default value");
            }
            variable.setIsDefaulted(false);
        } catch (Exception exception) {
            variable = (Variable) Variable.builder()
                    .key(key)
                    .type(variableType)
                    .value(defaultValue)
                    .defaultValue(defaultValue)
                    .isDefaulted(true)
                    .build();
        }
        return variable;
    }

    @Override
    public void close() {
        // no-op - Cloud SDKs don't hold onto any resources
    }


    /**
     * @return the OpenFeature provider for this client.
     */
    @Override
    public FeatureProvider getOpenFeatureProvider() {
        return this.openFeatureProvider;
    }

    @Override
    public String getSDKPlatform() {
        return "Cloud";
    }

    /**
     * Get all variables by key for user data
     *
     * @param user (required)
     * @return Map>String, BaseVariable<
     */
    public Map allVariables(DevCycleUser user) throws DevCycleException {
        validateUser(user);

        Call> response = api.getVariables(user, dvcOptions.getEnableEdgeDB());
        try {
            Map variablesResponse = getResponse(response);
            return variablesResponse;
        } catch (DevCycleException exception) {
            if (exception.getHttpResponseCode() == HttpResponseCode.SERVER_ERROR) {
                return new HashMap<>();
            }
            throw exception;
        }
    }

    /**
     * Post events to DevCycle for user
     *
     * @param user  (required)
     * @param event (required)
     */
    public void track(DevCycleUser user, DevCycleEvent event) throws DevCycleException {
        validateUser(user);

        if (event == null || event.getType() == null || event.getType().equals("")) {
            throw new IllegalArgumentException("Invalid DevCycleEvent");
        }

        DevCycleUserAndEvents userAndEvents = DevCycleUserAndEvents.builder()
                .user(user)
                .events(Collections.singletonList(event))
                .build();

        Call response = api.track(userAndEvents, dvcOptions.getEnableEdgeDB());
        getResponseWithRetries(response, 5);
    }


    private  T getResponseWithRetries(Call call, int maxRetries) throws DevCycleException {
        // attempt 0 is the initial request, attempt > 0 are all retries
        int attempt = 0;
        do {
            try {
                return getResponse(call);
            } catch (DevCycleException e) {
                attempt++;

                // if out of retries or this is an unauthorized error, throw up exception
                if (!e.isRetryable() || attempt > maxRetries) {
                    throw e;
                }

                try {
                    // exponential backoff
                    long waitIntervalMS = (long) (10 * Math.pow(2, attempt));
                    Thread.sleep(waitIntervalMS);
                } catch (InterruptedException ex) {
                    // no-op
                }

                // prep the call for a retry
                call = call.clone();
            }
        } while (attempt <= maxRetries);

        // getting here should not happen, but is technically possible
        ErrorResponse errorResponse = ErrorResponse.builder().build();
        errorResponse.setMessage("Out of retry attempts");
        throw new DevCycleException(HttpResponseCode.SERVER_ERROR, errorResponse);
    }


    private  T getResponse(Call call) throws DevCycleException {
        ErrorResponse errorResponse = ErrorResponse.builder().build();
        Response response;

        try {
            response = call.execute();
        } catch (MismatchedInputException mie) {
            // got a badly formatted JSON response from the server
            errorResponse.setMessage(mie.getMessage());
            throw new DevCycleException(HttpResponseCode.NO_CONTENT, errorResponse);
        } catch (IOException e) {
            // issues reaching the server or reading the response
            errorResponse.setMessage(e.getMessage());
            throw new DevCycleException(HttpResponseCode.byCode(500), errorResponse);
        }

        HttpResponseCode httpResponseCode = HttpResponseCode.byCode(response.code());
        errorResponse.setMessage("Unknown error");

        if (response.errorBody() != null) {
            try {
                errorResponse = OBJECT_MAPPER.readValue(response.errorBody().string(), ErrorResponse.class);
            } catch (IOException e) {
                errorResponse.setMessage(e.getMessage());
                throw new DevCycleException(httpResponseCode, errorResponse);
            }
            throw new DevCycleException(httpResponseCode, errorResponse);
        }

        if (response.body() == null) {
            throw new DevCycleException(httpResponseCode, errorResponse);
        }

        if (response.isSuccessful()) {
            return response.body();
        } else {
            if (httpResponseCode == HttpResponseCode.UNAUTHORIZED) {
                errorResponse.setMessage("Invalid SDK Key");
            } else if (!response.message().equals("")) {
                try {
                    errorResponse = OBJECT_MAPPER.readValue(response.message(), ErrorResponse.class);
                } catch (JsonProcessingException e) {
                    errorResponse.setMessage(e.getMessage());
                    throw new DevCycleException(httpResponseCode, errorResponse);
                }
            }

            throw new DevCycleException(httpResponseCode, errorResponse);
        }
    }

    private boolean isValidServerKey(String serverKey) {
        return serverKey.startsWith("server") || serverKey.startsWith("dvc_server");
    }

    private void validateUser(DevCycleUser user) {
        if (user == null) {
            throw new IllegalArgumentException("DevCycleUser cannot be null");
        }
        if (user.getUserId().equals("")) {
            throw new IllegalArgumentException("userId cannot be empty");
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy