/*
* Copyright 2014 Load Impact
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.loadimpact;
import com.loadimpact.exception.*;
import com.loadimpact.resource.DataStore;
import com.loadimpact.resource.HttpMethods;
import com.loadimpact.resource.LoadZone;
import com.loadimpact.resource.Test;
import com.loadimpact.resource.TestConfiguration;
import com.loadimpact.resource.UserScenario;
import com.loadimpact.resource.UserScenarioValidation;
import com.loadimpact.resource.testresult.CustomMetricResult;
import com.loadimpact.resource.testresult.PageMetricResult;
import com.loadimpact.resource.testresult.ServerMetricResult;
import com.loadimpact.resource.testresult.StandardMetricResult;
import com.loadimpact.resource.testresult.UrlMetricResult;
import com.loadimpact.util.ObjectUtils;
import com.loadimpact.util.StringUtils;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
import org.glassfish.jersey.filter.LoggingFilter;
import org.glassfish.jersey.jsonp.JsonProcessingFeature;
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
import org.glassfish.jersey.media.multipart.MultiPart;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.media.multipart.file.FileDataBodyPart;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonStructure;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.logging.Logger;
/**
* Primary entry point for the Load Impact API Java SDK. <h2>Sample Usage</h2>
*
* @author jens
*/
@SuppressWarnings("UnusedDeclaration")
public class ApiTokenClient {
private static final String baseUri = "https://api.loadimpact.com/v2";
private static final String HEX_PATTERN = "[a-fA-F0-9]+";
private static final int TOKEN_LENGTH = 64;
private static final String USER_SCENARIOS = "user-scenarios";
private static final String DATA_STORES = "data-stores";
private static final String LOAD_ZONES = "load-zones";
private static final String TEST_CONFIGS = "test-configs";
private static final String USER_SCENARIO_VALIDATIONS = "user-scenario-validations";
private static final String TESTS = "tests";
private static final String RESULTS = "results";
private static final String ABORT = "abort";
private static final String BUILD_DATA = "/buildData.properties";
private static final String AGENT_REQHDR = "X-Load-Impact-Agent";
@Deprecated
private static List<String> HTTP_METHODS = Arrays.asList("GET", "POST", "HEAD", "PUT", "DELETE"); //excluded: OPTIONS, TRACE
private final String apiToken;
private final Logger log;
private WebTarget wsBase;
private String agentRequestHeaderValue;
{ // Initialize the logger
log = Logger.getLogger(this.getClass().getName());
}
/**
* Constructor intended for unit-testing only.
*/
protected ApiTokenClient() {
apiToken = null;
wsBase = null;
}
/**
* Creates a client and initializes the REST client with the given API token.
*
* @param apiToken API token to use for authentication
*/
public ApiTokenClient(String apiToken) {
checkApiToken(apiToken);
this.apiToken = apiToken;
this.wsBase = configure(this.apiToken, false, this.log, 0);
}
public Properties getBuildData() {
Properties buildData = new Properties();
InputStream is = getClass().getResourceAsStream(BUILD_DATA);
if (is != null) {
try {
buildData.load(is);
} catch (IOException ignore) {
}
}
return buildData;
}
public String getVersion() {
return getBuildData().getProperty("version", "0.0.0");
}
protected String getAgentRequestHeaderValue() {
if (agentRequestHeaderValue == null) {
agentRequestHeaderValue = String.format("LoadImpactJavaSDK/%s", getVersion());
}
return agentRequestHeaderValue;
}
public void setAgentRequestHeaderValue(String agentRequestHeaderValue) {
this.agentRequestHeaderValue = agentRequestHeaderValue;
}
/**
* Enables/disabled REQ/RES debug logging. This method re-configures the web-target, via {@link #configure(String,
* boolean, java.util.logging.Logger, int)}.
*
* @param debug true for logging
* @return itself (for chaining)
*/
public ApiTokenClient setDebug(boolean debug) {
return setDebug(debug, 10000);
}
/**
* Enables REQ/RES debug logging. This method re-configures the web-target, via {@link #configure(String, boolean,
* java.util.logging.Logger, int)}.
*
* @param debug true for logging
* @param maxEntitySize max number of chars for dumping the content of an entity (e.g. response body)
* @return itself (for chaining)
*/
public ApiTokenClient setDebug(boolean debug, int maxEntitySize) {
return setDebug(debug, maxEntitySize, log);
}
/**
* Enables REQ/RES debug logging. This method re-configures the web-target, via {@link #configure(String, boolean,
* java.util.logging.Logger, int)}.
*
* @param debug true for logging
* @param maxEntitySize max number of chars for dumping the content of an entity (e.g. response body)
* @param log non-standard log stream
* @return itself (for chaining)
*/
public ApiTokenClient setDebug(boolean debug, int maxEntitySize, Logger log) {
wsBase = configure(apiToken, debug, log, maxEntitySize);
return this;
}
/**
* Configures this client, by settings HTTP AUTH filter, the REST URL and logging.
*
* @param token its API Token
* @param debug true for Jersey REQ/RES logging
* @param log debug log stream (if debug==true)
* @param maxLog max size of logged entity (if debug==true)
* @return configured REST URL target
*/
private WebTarget configure(String token, boolean debug, Logger log, int maxLog) {
Client client = ClientBuilder.newBuilder()
.register(MultiPartFeature.class)
.register(JsonProcessingFeature.class)
.build();
client.register(HttpAuthenticationFeature.basic(token, ""));
if (debug) client.register(new LoggingFilter(log, maxLog));
return client.target(baseUri);
}
/**
* Invocation closure used to modify the web-target before requests processing.
*/
static interface QueryClosure {
/**
* Intercepts right before requests processing starts.
*
* @param webTarget object to modify
* @return modified object
*/
WebTarget modify(WebTarget webTarget);
}
/**
* Invocation closure used to setup the request, such as GET, POST etc.
*
* @param <JsonType> type such as JsonObject, JsonArray, etc
*/
static interface RequestClosure<JsonType extends JsonStructure> {
/**
* Creates the request and returns the response entity type
*
* @param request the request object
* @return response entity type
*/
JsonType call(Invocation.Builder request);
}
/**
* Invocation closure used to process the response entity.
*
* @param <JsonType> response entity type (same as for {@link ApiTokenClient.RequestClosure#call(javax.ws.rs.client.Invocation.Builder)}
* )
* @param <ValueType> return type (classes from the <code>data</code> package)
*/
static interface ResponseClosure<JsonType extends JsonStructure, ValueType> {
/**
* Converts the response JSON into a value-type (package <code>data</code>)
*
* @param json JSON type, such as JsonObject, JsonArray
* @return a value-type object or list of it
*/
ValueType call(JsonType json);
}
public static class OffsetRange implements Serializable {
public final int start;
public final int end;
public OffsetRange(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public String toString() {
return String.format("|%d:%d", start, end);
}
public static OffsetRange mk(int start, int end) {
return new OffsetRange(start, end);
}
}
/**
* Performs a REST API invocation. This is wrapper function for {@link #invoke(String, String, String,
* ApiTokenClient.QueryClosure, ApiTokenClient.RequestClosure,
* ApiTokenClient.ResponseClosure)}
*
* @param operation operation name, such as <code>tests</code>, <code>data-stores</code>, etc
* @param requestClosure closure to create the request, such as GET, POST, PUT, DELETE
* @param responseClosure closure to convert the response JSON into a value-object
* @param <JsonType> JSON type, such as JsonObject, JsonArray
* @param <ValueType> value-type, such as {@link com.loadimpact.resource.Test}, {@link
* com.loadimpact.resource.UserScenario}, etc
* @return a single value-object or a list of it
* @throws com.loadimpact.exception.ApiException if anything goes wrong, such as the server returns HTTP status not being 20x
*/
protected <JsonType extends JsonStructure, ValueType>
ValueType invoke(String operation, RequestClosure<JsonType> requestClosure, ResponseClosure<JsonType, ValueType> responseClosure) {
return invoke(operation, null, null, null, requestClosure, responseClosure);
}
/**
* Performs a REST API invocation. This is wrapper function for {@link #invoke(String, String, String,
* ApiTokenClient.QueryClosure, ApiTokenClient.RequestClosure,
* ApiTokenClient.ResponseClosure)}
*
* @param operation operation name, such as <code>tests</code>, <code>data-stores</code>, etc
* @param id resource ID (if fetching a specific resource)
* @param requestClosure closure to create the request, such as GET, POST, PUT, DELETE
* @param responseClosure closure to convert the response JSON into a value-object
* @param <JsonType> JSON type, such as JsonObject, JsonArray
* @param <ValueType> value-type, such as {@link com.loadimpact.resource.Test}, {@link
* com.loadimpact.resource.UserScenario}, etc
* @return a single value-object or a list of it
* @throws com.loadimpact.exception.ApiException if anything goes wrong, such as the server returns HTTP status not being 20x
*/
protected <JsonType extends JsonStructure, ValueType>
ValueType invoke(String operation, int id, RequestClosure<JsonType> requestClosure, ResponseClosure<JsonType, ValueType> responseClosure) {
return invoke(operation, Integer.toString(id), null, null, requestClosure, responseClosure);
}
/**
* Performs a REST API invocation. This is wrapper function for {@link #invoke(String, String, String,
* ApiTokenClient.QueryClosure, ApiTokenClient.RequestClosure,
* ApiTokenClient.ResponseClosure)}
*
* @param operation operation name, such as <code>tests</code>, <code>data-stores</code>, etc
* @param id resource ID (if fetching a specific resource)
* @param action optional action, such as <code>clone</code>, <code>abort</code>, etc
* @param requestClosure closure to create the request, such as GET, POST, PUT, DELETE
* @param responseClosure closure to convert the response JSON into a value-object
* @param <JsonType> JSON type, such as JsonObject, JsonArray
* @param <ValueType> value-type, such as {@link com.loadimpact.resource.Test}, {@link
* com.loadimpact.resource.UserScenario}, etc
* @return a single value-object or a list of it
* @throws com.loadimpact.exception.ApiException if anything goes wrong, such as the server returns HTTP status not being 20x
*/
protected <JsonType extends JsonStructure, ValueType>
ValueType invoke(String operation, int id, String action, RequestClosure<JsonType> requestClosure, ResponseClosure<JsonType, ValueType> responseClosure) {
return invoke(operation, Integer.toString(id), action, null, requestClosure, responseClosure);
}
protected <JsonType extends JsonStructure, ResultType>
List<ResultType> invokeForResults(int testId, final String ids, final OffsetRange range, final Class<ResultType> resultType) {
return invoke(TESTS, Integer.toString(testId), RESULTS,
new QueryClosure() {
@Override
public WebTarget modify(WebTarget webTarget) {
return webTarget.queryParam("ids", ids + (range != null ? range : ""));
}
},
new RequestClosure<JsonObject>() {
@Override
public JsonObject call(Invocation.Builder request) {
return request.get(JsonObject.class);
}
},
new ResponseClosure<JsonObject, List<ResultType>>() {
@Override
public List<ResultType> call(JsonObject json) {
JsonArray jsonArray = json.getJsonArray(ids);
// if (jsonArray == null) throw new ResponseParseException("Expected JSON array '" + ids + "'");
if (jsonArray == null) {
String[] parts = ids.split(":");
String metricId = parts[0];
jsonArray = json.getJsonArray(metricId);
if (jsonArray == null) {
throw new ResponseParseException("Expected JSON array with ID = '" + ids + "'");
}
}
Constructor<ResultType> resultConstructor = ObjectUtils.getConstructor(resultType, JsonObject.class);
List<ResultType> results = new ArrayList<ResultType>(jsonArray.size());
for (int k = 0; k < jsonArray.size(); ++k) {
JsonObject jsonObject = jsonArray.getJsonObject(k);
ResultType result = ObjectUtils.newInstance(resultConstructor, jsonObject);
results.add(result);
}
return results;
}
}
);
}
@SuppressWarnings("unchecked")
protected <JsonType extends JsonStructure, ResultType>
List<? extends StandardMetricResult> invokeForResults(int testId, final String ids, final OffsetRange range, final StandardMetricResult.Metrics metric) {
return invoke(TESTS, Integer.toString(testId), RESULTS,
new QueryClosure() {
@Override
public WebTarget modify(WebTarget webTarget) {
return webTarget.queryParam("ids", ids + (range != null ? range : ""));
}
},
new RequestClosure<JsonObject>() {
@Override
public JsonObject call(Invocation.Builder request) {
return request.get(JsonObject.class);
}
},
new ResponseClosure<JsonObject, List<? extends StandardMetricResult>>() {
@Override
public List<? extends StandardMetricResult> call(JsonObject json) {
JsonArray jsonArray = json.getJsonArray(ids);
if (jsonArray == null) {
String[] parts = ids.split(":");
String metricId = parts[0];
jsonArray = json.getJsonArray(metricId);
if (jsonArray == null) {
throw new ResponseParseException("Expected JSON array with ID = '" + ids + "'");
}
}
Constructor<? extends StandardMetricResult> resultConstructor = ObjectUtils.getConstructor(metric.resultType, StandardMetricResult.Metrics.class, JsonObject.class);
List<StandardMetricResult> results = new ArrayList<StandardMetricResult>(jsonArray.size());
for (int k = 0; k < jsonArray.size(); ++k) {
JsonObject jsonObject = jsonArray.getJsonObject(k);
StandardMetricResult result = ObjectUtils.newInstance(resultConstructor, metric, jsonObject);
results.add(result);
}
return results;
}
}
);
}
/**
* Performs a REST API invocation.
*
* @param operation operation name, such as <code>tests</code>, <code>data-stores</code>, etc
* @param id resource ID (if fetching a specific resource)
* @param action optional action, such as <code>clone</code>, <code>abort</code>, etc
* @param queryClosure optional closure to modify the web-target before requests processing
* @param requestClosure closure to create the request, such as GET, POST, PUT, DELETE
* @param responseClosure closure to convert the response JSON into a value-object
* @param <JsonType> JSON type, such as JsonObject, JsonArray
* @param <ValueType> value-type, such as {@link com.loadimpact.resource.Test}, {@link
* com.loadimpact.resource.UserScenario}, etc
* @return a single value-object or a list of it
* @throws com.loadimpact.exception.ApiException if anything goes wrong, such as the server returns HTTP status not being 20x
*/
protected <JsonType extends JsonStructure, ValueType>
ValueType invoke(String operation, String id, String action, QueryClosure queryClosure, RequestClosure<JsonType> requestClosure, ResponseClosure<JsonType, ValueType> responseClosure) {
try {
WebTarget ws = wsBase.path(operation);
if (id != null) ws = ws.path(id);
if (action != null) ws = ws.path(action);
if (queryClosure != null) ws = queryClosure.modify(ws);
Invocation.Builder request = ws.request(MediaType.APPLICATION_JSON_TYPE);
request.header(AGENT_REQHDR, getAgentRequestHeaderValue());
JsonType json = requestClosure.call(request);
return (responseClosure != null) ? responseClosure.call(json) : null;
} catch (WebApplicationException e) {
Response.StatusType status = e.getResponse().getStatusInfo();
switch (status.getStatusCode()) {
case 400:
throw new BadRequestException(operation, id, action, e);
case 401:
throw new MissingApiTokenException(status.getReasonPhrase());
case 403:
throw new UnauthorizedException(operation, id, action);
case 404:
throw new NotFoundException(operation, id);
case 409:
throw new ConflictException(operation, id, action, e);
case 422:
throw new CoercionException(operation, id, action, e);
case 427:
throw new RateLimitedException(operation, id, action);
case 429:
throw new ResponseParseException(operation, id, action, e);
case 500: {
String message = "";
Response response = e.getResponse();
String contentType = response.getHeaderString("Content-Type");
if (contentType.equals("application/json")) {
InputStream is = (InputStream) response.getEntity();
JsonObject errJson = Json.createReader(is).readObject();
message = errJson.getString("message");
} else if (contentType.startsWith("text/html")) {
InputStream is = (InputStream) response.getEntity();
message = StringUtils.toString(is);
}
throw new ServerException(status.getReasonPhrase() + ": " + message);
}
default:
throw new ApiException(e);
}
} catch (ApiException e) {
throw e;
} catch (Exception e) {
throw new ApiException(e);
}
}
/**
* Syntax checks the API Token.
*
* @param apiToken value to check
* @throws IllegalArgumentException if invalid
*/
protected void checkApiToken(String apiToken) {
if (StringUtils.isBlank(apiToken)) throw new MissingApiTokenException("Empty key");
if (apiToken.length() != TOKEN_LENGTH) throw new MissingApiTokenException("Wrong length");
if (!apiToken.matches(HEX_PATTERN)) throw new MissingApiTokenException("Not a HEX value");
}
/**
* Returns true if we can successfully logon and fetch some data.
*
* @return true if can logon
*/
public boolean isValidToken() {
try {
// Response response = wsBase.path(TEST_CONFIGS).request(MediaType.APPLICATION_JSON_TYPE).get();
// return response.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL;
LoadZone zone = getLoadZone(LoadZone.AMAZON_US_ASHBURN.uid);
return zone == LoadZone.AMAZON_US_ASHBURN;
} catch (Exception e) {
log.info("API token validation failed: " + e);
}
return false;
}
/**
* Retrieves a single test configuration
*
* @param id test configuration its id
* @return {@link com.loadimpact.resource.TestConfiguration}
*/
public TestConfiguration getTestConfiguration(int id) {
return invoke(TEST_CONFIGS, id,
new RequestClosure<JsonObject>() {
@Override
public JsonObject call(Invocation.Builder request) {
return request.get(JsonObject.class);
}
},
new ResponseClosure<JsonObject, TestConfiguration>() {
@Override
public TestConfiguration call(JsonObject json) {
return new TestConfiguration(json);
}
}
);
}
/**
* Retrieves all test configurations.
*
* @return list of {@link com.loadimpact.resource.TestConfiguration}
*/
public List<TestConfiguration> getTestConfigurations() {
return invoke(TEST_CONFIGS,
new RequestClosure<JsonArray>() {
@Override
public JsonArray call(Invocation.Builder request) {
return request.get(JsonArray.class);
}
},
new ResponseClosure<JsonArray, List<TestConfiguration>>() {
@Override
public List<TestConfiguration> call(JsonArray json) {
List<TestConfiguration> testConfigs = new ArrayList<TestConfiguration>(json.size());
for (int k = 0; k < json.size(); ++k) {
testConfigs.add(new TestConfiguration(json.getJsonObject(k)));
}
return testConfigs;
}
}
);
}
/**
* Makes a copy of an existing test configuration.
*
* @param id id of the config
* @param name its new name
* @return {@link com.loadimpact.resource.TestConfiguration}
*/
public TestConfiguration cloneTestConfiguration(int id, final String name) {
return invoke(TEST_CONFIGS, id, "clone",
new RequestClosure<JsonObject>() {
@Override
public JsonObject call(Invocation.Builder request) {
String json = Json.createObjectBuilder().add("name", name).build().toString();
Entity<String> data = Entity.entity(json, MediaType.APPLICATION_JSON_TYPE);
return request.post(data, JsonObject.class);
}
},
new ResponseClosure<JsonObject, TestConfiguration>() {
@Override
public TestConfiguration call(JsonObject json) {
return new TestConfiguration(json);
}
}
);
}
/**
* Deletes a test configuration.
*
* @param id its id
* @throws com.loadimpact.exception.NotFoundException if it was unsuccessful
*/
public void deleteTestConfiguration(final int id) {
invoke(TEST_CONFIGS, id,
new RequestClosure<JsonStructure>() {
@Override
public JsonStructure call(Invocation.Builder request) {
Response response = request.delete();
if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) {
throw new NotFoundException(TEST_CONFIGS, Integer.toString(id));
}
return null;
}
},
null
);
}
/**
* Updates an existing test configuration.
*
* @param testConfiguration a test configuration object
* @return {@link com.loadimpact.resource.TestConfiguration} stored at the server
*/
public TestConfiguration updateTestConfiguration(final TestConfiguration testConfiguration) {
return invoke(TEST_CONFIGS, testConfiguration.id,
new RequestClosure<JsonObject>() {
@Override
public JsonObject call(Invocation.Builder request) {
String json = testConfiguration.toJSON().toString();
Entity<String> data = Entity.entity(json, MediaType.APPLICATION_JSON_TYPE);
return request.put(data, JsonObject.class);
}
},
new ResponseClosure<JsonObject, TestConfiguration>() {
@Override
public TestConfiguration call(JsonObject json) {
return new TestConfiguration(json);
}
}
);
}
/**
* Creates a new test configuration.
*
* @param testConfiguration a prepared test configuration object (with no ID)
* @return {@link com.loadimpact.resource.TestConfiguration} stored at the server
*/
public TestConfiguration createTestConfiguration(final TestConfiguration testConfiguration) {
return invoke(TEST_CONFIGS,
new RequestClosure<JsonObject>() {
@Override
public JsonObject call(Invocation.Builder request) {
String json = testConfiguration.toJSON().toString();
Entity<String> data = Entity.entity(json, MediaType.APPLICATION_JSON_TYPE);
return request.post(data, JsonObject.class);
}
},
new ResponseClosure<JsonObject, TestConfiguration>() {
@Override
public TestConfiguration call(JsonObject json) {
return new TestConfiguration(json);
}
}
);
}
/**
* Retrieves a load-zone.
*
* @param id its id
* @return {@link com.loadimpact.resource.LoadZone}
*/
public LoadZone getLoadZone(String id) {
return invoke(LOAD_ZONES, id, null, null,
new RequestClosure<JsonArray>() {
@Override
public JsonArray call(Invocation.Builder request) {
return request.get(JsonArray.class);
}
},
new ResponseClosure<JsonArray, LoadZone>() {
@Override
public LoadZone call(JsonArray json) {
return LoadZone.valueOf(json.getJsonObject(0));
}
}
);
}
/**
* Retrieves all load-zones.
*
* @return list of {@link com.loadimpact.resource.LoadZone}
*/
public List<LoadZone> getLoadZone() {
return invoke(LOAD_ZONES,
new RequestClosure<JsonArray>() {
@Override
public JsonArray call(Invocation.Builder request) {
return request.get(JsonArray.class);
}
},
new ResponseClosure<JsonArray, List<LoadZone>>() {
@Override
public List<LoadZone> call(JsonArray json) {
List<LoadZone> zones = new ArrayList<LoadZone>(json.size());
for (int k = 0; k < json.size(); ++k) {
zones.add(LoadZone.valueOf(json.getJsonObject(k)));
}
return zones;
}
}
);
}
/**
* Retrieves a data store.
*
* @param id ist id
* @return {@link com.loadimpact.resource.DataStore}
*/
public DataStore getDataStore(int id) {
return invoke(DATA_STORES, id,
new RequestClosure<JsonObject>() {
@Override
public JsonObject call(Invocation.Builder request) {
return request.get(JsonObject.class);
}
},
new ResponseClosure<JsonObject, DataStore>() {
@Override
public DataStore call(JsonObject json) {
return new DataStore(json);
}
}
);
}
/**
* Retrieves all data stores.
*
* @return list of {@link com.loadimpact.resource.DataStore}
*/
public List<DataStore> getDataStores() {
return invoke(DATA_STORES,
new RequestClosure<JsonArray>() {
@Override
public JsonArray call(Invocation.Builder request) {
return request.get(JsonArray.class);
}
},
new ResponseClosure<JsonArray, List<DataStore>>() {
@Override
public List<DataStore> call(JsonArray json) {
List<DataStore> ds = new ArrayList<DataStore>(json.size());
for (int k = 0; k < json.size(); ++k) {
ds.add(new DataStore(json.getJsonObject(k)));
}
return ds;
}
}
);
}
/**
* Deletes a data store.
*
* @param id its id
* @throws com.loadimpact.exception.ResponseParseException if it was unsuccessful
*/
public void deleteDataStore(final int id) {
invoke(DATA_STORES, id,
new RequestClosure<JsonStructure>() {
@Override
public JsonStructure call(Invocation.Builder request) {
Response response = request.delete();
if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) {
throw new ResponseParseException(DATA_STORES, id, null, null);
}
return null;
}
},
null
);
}
/**
* Creates a new data store.
*
* @param file CSV file that should be uploaded (N.B. max 50MB)
* @param name name to use in the Load Impact web-console
* @param fromline Payload from this line (1st line is 1). Set to value 2, if the CSV file starts with a headings line
* @param separator field separator, one of {@link com.loadimpact.resource.DataStore.Separator}
* @param delimiter surround delimiter for text-strings, one of {@link com.loadimpact.resource.DataStore.StringDelimiter}
* @return {@link com.loadimpact.resource.DataStore}
*/
public DataStore createDataStore(final File file, final String name, final int fromline, final DataStore.Separator separator, final DataStore.StringDelimiter delimiter) {
return invoke(DATA_STORES,
new RequestClosure<JsonObject>() {
@Override
public JsonObject call(Invocation.Builder request) {
MultiPart form = new FormDataMultiPart()
.field("name", name)
.field("fromline", Integer.toString(fromline))
.field("separator", separator.param())
.field("delimiter", delimiter.param())
.bodyPart(new FileDataBodyPart("file", file, new MediaType("text", "csv")));
return request.post(Entity.entity(form, form.getMediaType()), JsonObject.class);
}
},
new ResponseClosure<JsonObject, DataStore>() {
@Override
public DataStore call(JsonObject json) {
return new DataStore(json);
}
}
);
}
/**
* Retrieves a user scenario.
*
* @param id its id
* @return {@link com.loadimpact.resource.UserScenario}
*/
public UserScenario getUserScenario(int id) {
return invoke(USER_SCENARIOS, id,
new RequestClosure<JsonObject>() {
@Override
public JsonObject call(Invocation.Builder request) {
return request.get(JsonObject.class);
}
},
new ResponseClosure<JsonObject, UserScenario>() {
@Override
public UserScenario call(JsonObject json) {
return new UserScenario(json);
}
}
);
}
/**
* Retrieves all user scenarios.
*
* @return list of {@link com.loadimpact.resource.UserScenario}
*/
public List<UserScenario> getUserScenarios() {
return invoke(USER_SCENARIOS,
new RequestClosure<JsonArray>() {
@Override
public JsonArray call(Invocation.Builder request) {
return request.get(JsonArray.class);
}
},
new ResponseClosure<JsonArray, List<UserScenario>>() {
@Override
public List<UserScenario> call(JsonArray json) {
List<UserScenario> ds = new ArrayList<UserScenario>(json.size());
for (int k = 0; k < json.size(); ++k) {
ds.add(new UserScenario(json.getJsonObject(k)));
}
return ds;
}
}
);
}
/**
* Makes a copy of a user scenario.
*
* @param id its id
* @param name new name of the copy
* @return {@link com.loadimpact.resource.UserScenario}
*/
public UserScenario cloneUserScenario(int id, final String name) {
return invoke(USER_SCENARIOS, id, "clone",
new RequestClosure<JsonObject>() {
@Override
public JsonObject call(Invocation.Builder request) {
String json = Json.createObjectBuilder().add("name", name).build().toString();
Entity<String> data = Entity.entity(json, MediaType.APPLICATION_JSON_TYPE);
return request.post(data, JsonObject.class);
}
},
new ResponseClosure<JsonObject, UserScenario>() {
@Override
public UserScenario call(JsonObject json) {
return new UserScenario(json);
}
}
);
}
/**
* Deletes a user scenario.
*
* @param id its id
* @throws com.loadimpact.exception.ResponseParseException if unsuccessful
*/
public void deleteUserScenario(final int id) {
invoke(USER_SCENARIOS, id,
new RequestClosure<JsonStructure>() {
@Override
public JsonStructure call(Invocation.Builder request) {
Response response = request.delete();
if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) {
throw new ResponseParseException(USER_SCENARIOS, id, null, null);
}
return null;
}
},
null
);
}
/**
* Updates a user scenario.
*
* @param scenario modified scenario
* @return server stored {@link com.loadimpact.resource.UserScenario}
*/
public UserScenario updateUserScenario(final UserScenario scenario) {
return invoke(USER_SCENARIOS, scenario.id,
new RequestClosure<JsonObject>() {
@Override
public JsonObject call(Invocation.Builder request) {
String json = scenario.toJSON().toString();
Entity<String> data = Entity.entity(json, MediaType.APPLICATION_JSON_TYPE);
return request.put(data, JsonObject.class);
}
},
new ResponseClosure<JsonObject, UserScenario>() {
@Override
public UserScenario call(JsonObject json) {
return new UserScenario(json);
}
}
);
}
/**
* Creates a new user scenario.
*
* @param scenario scenario configuration (no ID)
* @return server stored {@link com.loadimpact.resource.UserScenario}
*/
public UserScenario createUserScenario(final UserScenario scenario) {
return invoke(USER_SCENARIOS,
new RequestClosure<JsonObject>() {
@Override
public JsonObject call(Invocation.Builder request) {
String json = scenario.toJSON().toString();
Entity<String> data = Entity.entity(json, MediaType.APPLICATION_JSON_TYPE);
return request.post(data, JsonObject.class);
}
},
new ResponseClosure<JsonObject, UserScenario>() {
@Override
public UserScenario call(JsonObject json) {
return new UserScenario(json);
}
}
);
}
/**
* Creates (starts) a user scenario validation.
*
* @param scenarioId id of the scenario that should be validated
* @return {@link com.loadimpact.resource.UserScenarioValidation}
*/
public UserScenarioValidation createUserScenarioValidation(final int scenarioId) {
return invoke(USER_SCENARIO_VALIDATIONS,
new RequestClosure<JsonObject>() {
@Override
public JsonObject call(Invocation.Builder request) {
JsonObject json = Json.createObjectBuilder().add("user_scenario_id", scenarioId).build();
Entity<String> data = Entity.entity(json.toString(), MediaType.APPLICATION_JSON_TYPE);
return request.post(data, JsonObject.class);
}
},
new ResponseClosure<JsonObject, UserScenarioValidation>() {
@Override
public UserScenarioValidation call(JsonObject json) {
return new UserScenarioValidation(json);
}
}
);
}
/**
* Retrieves a user scenario validation.
*
* @param id its id
* @return {@link com.loadimpact.resource.UserScenarioValidation}
*/
public UserScenarioValidation getUserScenarioValidation(int id) {
return invoke(USER_SCENARIO_VALIDATIONS, id,
new RequestClosure<JsonObject>() {
@Override
public JsonObject call(Invocation.Builder request) {
return request.get(JsonObject.class);
}
},
new ResponseClosure<JsonObject, UserScenarioValidation>() {
@Override
public UserScenarioValidation call(JsonObject json) {
return new UserScenarioValidation(json);
}
}
);
}
/**
* Retrieves the results of a scenario validation and populates the list {@link
* com.loadimpact.resource.UserScenarioValidation#results}
*
* @param scenarioValidation validation object
* @return the same validation object as passed in, but augmented with the results
*/
public UserScenarioValidation getUserScenarioValidationResults(final UserScenarioValidation scenarioValidation) {
return invoke(USER_SCENARIO_VALIDATIONS, scenarioValidation.id, "results",
new RequestClosure<JsonObject>() {
@Override
public JsonObject call(Invocation.Builder request) {
return request.get(JsonObject.class);
}
},
new ResponseClosure<JsonObject, UserScenarioValidation>() {
@Override
public UserScenarioValidation call(JsonObject json) {
UserScenarioValidation sc = (UserScenarioValidation) ObjectUtils.copy(scenarioValidation);
JsonArray results = json.getJsonArray("results");
if (results != null) {
for (int k = 0; k < results.size(); ++k) {
sc.results.add(new UserScenarioValidation.Result(results.getJsonObject(k)));
}
}
return sc;
}
}
);
}
/**
* Retrieves a test (instance).
*
* @param id its id
* @return {@link com.loadimpact.resource.Test}
*/
public Test getTest(int id) {
return invoke(TESTS, id,
new RequestClosure<JsonObject>() {
@Override
public JsonObject call(Invocation.Builder request) {
return request.get(JsonObject.class);
}
},
new ResponseClosure<JsonObject, Test>() {
@Override
public Test call(JsonObject json) {
return new Test(json);
}
}
);
}
/**
* Retrieves all tests.
*
* @return list of {@link com.loadimpact.resource.Test}
*/
public List<Test> getTests() {
return invoke(TESTS,
new RequestClosure<JsonArray>() {
@Override
public JsonArray call(Invocation.Builder request) {
return request.get(JsonArray.class);
}
},
new ResponseClosure<JsonArray, List<Test>>() {
@Override
public List<Test> call(JsonArray json) {
List<Test> ds = new ArrayList<Test>(json.size());
for (int k = 0; k < json.size(); ++k) {
ds.add(new Test(json.getJsonObject(k)));
}
return ds;
}
}
);
}
/**
* Starts a test.
*
* @param testConfigId id of the test configuration
* @return test-instance ID for the just created load test
*/
public int startTest(int testConfigId) {
return invoke(TEST_CONFIGS, testConfigId, "start",
new RequestClosure<JsonObject>() {
@Override
public JsonObject call(Invocation.Builder request) {
return request.post(null, JsonObject.class);
}
},
new ResponseClosure<JsonObject, Integer>() {
@Override
public Integer call(JsonObject json) {
return json.getInt("id", -1);
}
}
);
}
/**
* Aborts a running test.
*
* @param testId its id
* @throws com.loadimpact.exception.ResponseParseException if unsuccessful
*/
public void abortTest(final int testId) {
invoke(TESTS, testId, ABORT,
new RequestClosure<JsonStructure>() {
@Override
public JsonStructure call(Invocation.Builder request) {
Response response = request.post(null);
if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) {
throw new ResponseParseException(TESTS, testId, ABORT, null);
}
return null;
}
},
null
);
}
/**
* Polls a running test for status updates using the given listener. This method blocks until the test has completed
* and periodically retrieves status info using {@link #getTest(int)}
*
* @param testId test ID
* @param pollInterval length of poll interval, in seconds
* @param listener a {@link RunningTestListener} object
* @return the last retrieved test-instance object, or null if something went wrong
* @see RunningTestListener
* @see com.loadimpact.exception.AbortTest
*/
public Test monitorTest(int testId, int pollInterval, RunningTestListener listener) {
try {
long nextDeadline = System.currentTimeMillis() + pollInterval * 1000;
Test test = getTest(testId);
while (test.status.isInProgress()) {
listener.onProgress(test, this);
Thread.sleep(Math.max(nextDeadline - System.currentTimeMillis(), 0));
nextDeadline = System.currentTimeMillis() + pollInterval * 1000;
test = getTest(testId);
}
if (test.status.isSuccessful()) {
listener.onSuccess(test);
} else {
listener.onFailure(test);
}
return test;
} catch (InterruptedException e) {
abortTest(testId);
listener.onAborted();
} catch (AbortTest e) {
abortTest(testId);
listener.onAborted();
} catch (ApiException e) {
abortTest(testId);
listener.onError(e);
} catch (Exception e) {
abortTest(testId);
throw new RuntimeException(e);
}
return null;
}
public List<UrlMetricResult> getUrlMetricResults(int testId, URL url, LoadZone zone, Integer scenarioId, Integer httpStatus, HttpMethods httpMethod, final OffsetRange range) {
if (url == null) throw new IllegalArgumentException("Missing url");
if (zone == null) zone = LoadZone.AGGREGATE_WORLD;
if (scenarioId == null) throw new IllegalArgumentException("Missing scenario_id");
if (httpStatus == null) httpStatus = 200;
if (httpMethod == null) httpMethod = HttpMethods.GET;
String ids = String.format("%s%s:%d:%d:%d:%s", UrlMetricResult.METRIC_ID_PREFIX, StringUtils.md5(url.toString()), zone.id, scenarioId, httpStatus, httpMethod);
return invokeForResults(testId, ids, range, UrlMetricResult.class);
}
public List<PageMetricResult> getPageMetricResults(int testId, String pageName, LoadZone zone, Integer scenarioId, OffsetRange range) {
if (pageName == null) throw new IllegalArgumentException("Missing pageName");
if (zone == null) zone = LoadZone.AGGREGATE_WORLD;
if (scenarioId == null) throw new IllegalArgumentException("Missing scenario_id");
String ids = String.format("%s%s:%d:%d", PageMetricResult.METRIC_ID_PREFIX, StringUtils.md5(pageName), zone.id, scenarioId);
return invokeForResults(testId, ids, range, PageMetricResult.class);
}
public List<CustomMetricResult> getCustomMetricResults(int testId, String metricName, LoadZone zone, Integer scenarioId, OffsetRange range) {
if (metricName == null) throw new IllegalArgumentException("Missing metricName");
if (zone == null) zone = LoadZone.AGGREGATE_WORLD;
if (scenarioId == null) throw new IllegalArgumentException("Missing scenario_id");
String ids = String.format("%s%s:%d:%d", CustomMetricResult.METRIC_ID_PREFIX, StringUtils.md5(metricName), zone.id, scenarioId);
return invokeForResults(testId, ids, range, CustomMetricResult.class);
}
public List<ServerMetricResult> getServerMetricResults(int testId, String agentName, String metricName, OffsetRange range) {
if (agentName == null) throw new IllegalArgumentException("Missing agentName");
if (metricName == null) throw new IllegalArgumentException("Missing metricName");
String ids = String.format("%s%s", ServerMetricResult.METRIC_ID_PREFIX, StringUtils.md5(agentName + metricName));
return invokeForResults(testId, ids, range, ServerMetricResult.class);
}
public List<? extends StandardMetricResult> getStandardMetricResults(int testId, StandardMetricResult.Metrics metric, LoadZone zone, OffsetRange range) {
if (metric == null) throw new IllegalArgumentException("Missing metric");
if (zone == null) zone = LoadZone.AGGREGATE_WORLD;
String ids = String.format("%s:%d", metric.id, zone.id);
return invokeForResults(testId, ids, range, metric);
}
}