package com.satalyst.powerbi.impl; import com.github.rholder.retry.AttemptTimeLimiters; import com.github.rholder.retry.Retryer; import com.github.rholder.retry.RetryerBuilder; import com.github.rholder.retry.StopStrategies; import com.satalyst.powerbi.*; import org.jboss.resteasy.specimpl.ResteasyUriBuilder; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.Invocation; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriBuilder; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; /** * @author Aidan Morgan */ public class DefaultPowerBiConnection implements PowerBiConnection { private Authenticator authenticator; private ExecutorService executor; private ClientBuilder clientBuilder; private String baseUrl; private long maximumWaitTime; private int maximumRetries; /** * Constructor. * * @param authenticator the {@see Authenticator} instance to use for authentication. * @param executor the {@see ExecutorService} to use for background processing. */ public DefaultPowerBiConnection(Authenticator authenticator, ExecutorService executor) { this.authenticator = authenticator; this.executor = executor; this.clientBuilder = ClientBuilder.newBuilder(); } public DefaultPowerBiConnection setMaximumWaitTime(long val, TimeUnit timeUnit) { this.maximumWaitTime = timeUnit.toMillis(val); return this; } public DefaultPowerBiConnection setMaximumRetries(int val) { this.maximumRetries = val; return this; } @Override public <T> Future<T> execute(final PowerBiOperation<T> val) { // use a retryer to keep attempting to send data to powerBI if we receive a rate limit exception. // use exponential backoff to create a window of time for the request to come through. // TODO : the time to wait is actually in the response header, come back and add that value. final Retryer<T> retryer = RetryerBuilder.<T>newBuilder() // we are retrying on these exceptions because they are able to be handled by just retrying this callable // again (assuming that the maximum retries value is greater than 1 and this isn't the last retry attempt // before giving up. .retryIfExceptionOfType(RateLimitExceededException.class) .retryIfExceptionOfType(RequestAuthenticationException.class) .withAttemptTimeLimiter(AttemptTimeLimiters.<T>fixedTimeLimit(maximumWaitTime, TimeUnit.MILLISECONDS)) .withStopStrategy(StopStrategies.stopAfterAttempt(maximumRetries)) .build(); return executor.submit(new Callable<T>() { @Override public T call() throws Exception { return retryer.call(new PowerBiCallable<>(val, clientBuilder)); } }); } public DefaultPowerBiConnection setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; return this; } private class PowerBiCallable<T> implements Callable<T> { private PowerBiOperation<T> command; private ClientBuilder clientBuilder; public PowerBiCallable(PowerBiOperation<T> val, ClientBuilder clientBuilder) { this.command = val; this.clientBuilder = clientBuilder; } @Override public T call() throws Exception { UriBuilder uri = new ResteasyUriBuilder().path(baseUrl); command.buildUri(uri); Client client = null; try { client = clientBuilder.build(); WebTarget target = client.target(uri.build()); Invocation.Builder request = target.request(); request.header("Authorization", "Bearer " + authenticator.authenticate()); request.accept(MediaType.APPLICATION_JSON_TYPE); PowerBiRequest r = new PowerBiRequestImpl(request); // delegate to the command to perform the processing now. command.execute(r); } catch (RequestAuthenticationException e) { // we are intentionally resetting the authenticator here as the previous token has expired, so the next time // through this call the authenticator will not used the cached token and will retrieve a new one. if (e.isTokenExpired()) { authenticator.reset(); } throw e; } finally { if (client != null) { client.close(); } } return command.get(); } } }