package {{invokerPackage}}; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Type; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonParseException; import com.google.gson.reflect.TypeToken; import {{invokerPackage}}.auth.ApiKeyAuth; import {{invokerPackage}}.auth.HttpBasicAuth; import com.perforce.hwsclient.models.HWSStatus; import com.perforce.hwsclient.models.LoginResponse; import com.squareup.okhttp.Call; import com.squareup.okhttp.Interceptor; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; import retrofit.ErrorHandler; import retrofit.RestAdapter; import retrofit.RetrofitError; import retrofit.client.OkClient; import retrofit.converter.ConversionException; import retrofit.converter.Converter; import retrofit.converter.GsonConverter; import retrofit.mime.TypedByteArray; import retrofit.mime.TypedInput; import retrofit.mime.TypedOutput; /** * The Java client which fronts the HelixWebServices REST api. * Note that this is generated from a mustache template and * should not be edited directly. */ public class ApiClient { /** Map of authorized accessors. */ private Map<String, Interceptor> apiAuthorizations; /** Http request processor. */ private OkHttpClient okClient; /** Rest adapter factory. */ private RestAdapter.Builder adapterBuilder; /** Static /api path for requests. */ private static final String API_PATH = "/api"; /** Constant current version. */ private static final String SUPPORTED_VERSION = "v16.1"; /** Top level path for requests. */ private String basePath; /** * Apply configuration header overrides to our requests. * <p> * This method will provide the typical prefix to each configuration option, * so keys in this map should just be the configuration value name. For * example, <code>P4PORT</code>. * * @param overrides * A map of configuration option to configuration value. */ public void addOverrides(final Map<String, String> overrides) { if (overrides != null && !overrides.isEmpty()) { adapterBuilder.setRequestInterceptor(request -> { overrides.entrySet().forEach(e -> { request.addHeader( "X-Perforce-Helix-Web-Services-" + e.getKey(), e.getValue()); }); }); } } /** * The base URL to your HWS instance. * * @return The root to your HWS server, e.g., * https://perforce.mycompany.com/hws */ public String getBasePath() { return basePath; } /** * Change the base URL to the HWS instance. * * Changes will not be reflected until createDefaultAdapter is called again. * * @param basePath * The HWS root URL */ public void setBasePath(final String basePath) { this.basePath = basePath; } /** * A special construction option that allows your client to trust all * certificates. * * By default, all HWS instances start with a self-signed certificate. You * may need to construct using this variation to validate your code is * working in a non-production system. * * @param trustAllSsl * When true, we do not validate SSL. * @param basePath * The HWS root URL */ public ApiClient(final boolean trustAllSsl, final String basePath) { this.basePath = basePath; apiAuthorizations = new LinkedHashMap<>(); createDefaultAdapter(trustAllSsl); } /** * Construct an ApiClient with the indicated basePath. * * If you are testing out a new installation, this will likely not work due * to the use of a self-signed certificate on the server. * * @param basePath * The HWS root URL */ public ApiClient(final String basePath) { this(false, basePath); } /** * Construct an ApiClient, establishing a login session with the "project" * login server. * * This will generate an ApiClient instance, and, establish the security * header by calling the <code>POST /projects/v1/login</code> method. * * @param basePath * The HWS root URL * @param username * The Perforce user to connect as * @param password * The Perforce password to authenticate with (discarded after * ticket is acquired) * @return The ApiClient handle ready to make authenticated requests. */ public static ApiClient createWithTicket( final String basePath, final String username, final String password) { return createWithTicket(false, basePath, username, password); } /** * Construct an ApiClient, establishing a login session with the "project" * login server. * * This will generate an ApiClient instance, and, establish the security * header by calling the <code>POST /projects/v1/login</code> method. * * @param trustAllSsl * When true, we do not validate SSL. * @param basePath * The HWS root URL * @param username * The Perforce user to connect as * @param password * The Perforce password to authenticate with (discarded after * ticket is acquired) * @return The ApiClient handle ready to make authenticated requests. */ public static ApiClient createWithTicket( final boolean trustAllSsl, final String basePath, final String username, final String password) { ApiClient apiClient = new ApiClient(trustAllSsl, basePath); apiClient.setCredentials(username, password); DefaultApi api = apiClient.createDefaultService(); LoginResponse loginResponse = api.login(); apiClient.setApiKey(loginResponse.getTicket()); return apiClient; } /** * Checks status of the API against the currently configured server. * * @return true if the server is not currently reporting any problems. */ public boolean isOK() { try { HWSStatus status = getStatus(); return status != null && "OK".equals(status.getStatus()); } catch (Exception e) { return false; } } /** * Gets the status. * * @return the status */ public HWSStatus getStatus() { return createDefaultService().getStatus(); } /** * Will do a custom fetch of the URL at "/api", and will see if our * API_PATH string pops up in the mix. * * @return False if the server does not tell us we're a valid version. * (Might happen if you can't access the server too.) */ public boolean isSupported() { try { Request request = new Request.Builder().url(basePath + "/api/hws/version").method("GET", null) .addHeader("Accept", "application/json").build(); Call call = okClient.newCall(request); Response response = call.execute(); Gson gson = new Gson(); Type mapType = new TypeToken<Map<String, Object>>() { }.getType(); Map<String, Object> info = gson.fromJson(response.body().string(), mapType); @SuppressWarnings("unchecked") List<String> versions = (List<String>) info.get("supportedVersions"); return versions.stream().anyMatch(v -> SUPPORTED_VERSION.equals(v)); } catch (Exception e) { return false; } } /** * Initializes the adapterBuilder, typically called during construction. * * @param trustAllSsl * when true will configure the adapter to trust all connections. */ public void createDefaultAdapter(final boolean trustAllSsl) { Gson gson = new GsonBuilder() .setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").create(); okClient = new OkHttpClient(); if (trustAllSsl) { // Create a trust manager that does not validate certificate chains final TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { @Override public void checkClientTrusted( final java.security.cert.X509Certificate[] chain, final String authType) throws CertificateException { } @Override public void checkServerTrusted( final java.security.cert.X509Certificate[] chain, final String authType) throws CertificateException { } @Override public java.security.cert.X509Certificate[] getAcceptedIssuers() { return new java.security.cert.X509Certificate[] {}; } } }; // Install the all-trusting trust manager final SSLContext sslContext; try { sslContext = SSLContext.getInstance("SSL"); sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); } catch (KeyManagementException | NoSuchAlgorithmException e) { throw new RuntimeException(e); } // Create an ssl socket factory with our all-trusting manager final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); okClient.setSslSocketFactory(sslSocketFactory); okClient.setHostnameVerifier((hostname, session) -> true); } adapterBuilder = new RestAdapter.Builder().setEndpoint(basePath + API_PATH) .setClient(new OkClient(okClient)) .setErrorHandler(new ErrorHandler() { @Override public Throwable handleError(final RetrofitError arg0) { return new HwsRestException(arg0); } }) .setConverter(new GsonConverterWrapper(gson)); } /** * Obtain a handle to the service object to interact with HWS. * * @return The primary API object to interact with HWS. */ public DefaultApi createDefaultService() { return createService(DefaultApi.class); } /** * Create a service object to interact with the server. * * In general, you'll probably just want the DefaultApi here. * * @param <S> * The interface API (probably just DefaultApi.class) * @param serviceClass * The interface class indicating the API you want to use. * @return The interface object to interact with the remote server. */ public <S> S createService(final Class<S> serviceClass) { return adapterBuilder.build().create(serviceClass); } /** * Helper method to configure the username/password for basic auth or * password oauth. * * @param username * The Perforce login * @param ticket * The Perforce ticket (not the user's password) */ public void setCredentials(final String username, final String ticket) { HttpBasicAuth auth = new HttpBasicAuth(); auth.setUsername(username); auth.setPassword(ticket); updateAuthorization("ticket_auth", auth); } /** * Helper method to set the API key as the "ticket_auth" scheme. * * @param apiKey the token to set for authorization */ public void setApiKey(final String apiKey) { ApiKeyAuth auth = new ApiKeyAuth("header", "Authorization"); auth.setApiKey(apiKey); updateAuthorization("ticket_auth", auth); } /** * Add an authorization token. * @param authName the name associated with a token * @param authorization the token */ private void updateAuthorization(final String authName, final Interceptor authorization) { okClient.interceptors().clear(); okClient.interceptors().add(authorization); apiAuthorizations.put(authName, authorization); } /** * Get the adapter builder. * @return the current builder of adapters. */ public RestAdapter.Builder getAdapterBuilder() { return adapterBuilder; } /** * Set a new adapter builder. * @param adapterBuilder the new adapter builder to use */ public void setAdapterBuilder(final RestAdapter.Builder adapterBuilder) { this.adapterBuilder = adapterBuilder; } /** * Get the http client. * @return the current http client */ public OkHttpClient getOkClient() { return okClient; } /** * Set the current authorizations in the http client. * @param okClient the http client to be modified. */ public void addAuthsToOkClient(final OkHttpClient okClient) { for (Interceptor apiAuthorization : apiAuthorizations.values()) { okClient.interceptors().add(apiAuthorization); } } /** * Clones the okClient given in parameter, adds the auth interceptors and * uses it to configure the RestAdapter. * * @param okClient * The OKHttpClient to clone */ public void configureFromOkclient(final OkHttpClient okClient) { OkHttpClient clone = okClient.clone(); addAuthsToOkClient(clone); adapterBuilder.setClient(new OkClient(clone)); } /** * Wrapper class for the retrofit error to indicate that it was * handled by a perforce api. */ public class HwsRestException extends RuntimeException { /** Generated id, not expected to change. */ private static final long serialVersionUID = -8177702544598405834L; /** The original retrofit error. */ private final RetrofitError retrofitError; /** * Instantiate a new exception to wrap the retrofit error. * @param retrofitError the original rest error */ HwsRestException(final RetrofitError retrofitError) { super(retrofitError.getLocalizedMessage()); this.retrofitError = retrofitError; } /** * Get the original retrofit error, for further analysis. * @return RetrofitError the original error */ public RetrofitError getRetrofitError() { return retrofitError; } } } /** * This wrapper is to take care of this case: when the deserialization fails due * to JsonParseException and the expected type is String, then just return the * body string. */ class GsonConverterWrapper implements Converter { /** The actual converter instance. */ private GsonConverter converter; /** * Constructor with a converter instance. * @param gson the java <-> json converter */ GsonConverterWrapper(final Gson gson) { converter = new GsonConverter(gson); } @Override public Object fromBody(final TypedInput body, final Type type) throws ConversionException { byte[] bodyBytes = readInBytes(body); TypedByteArray newBody = new TypedByteArray(body.mimeType(), bodyBytes); try { return converter.fromBody(newBody, type); } catch (ConversionException e) { if (e.getCause() instanceof JsonParseException && type.equals(String.class)) { return new String(bodyBytes); } else { throw e; } } } @Override public TypedOutput toBody(final Object object) { return converter.toBody(object); } /** * Read a content body one byte at a time. * @param body the content * @return a byte array equivalent of the content * @throws ConversionException if it cannot be converted */ private byte[] readInBytes(final TypedInput body) throws ConversionException { InputStream in = null; try { in = body.in(); ByteArrayOutputStream os = new ByteArrayOutputStream(); byte[] buffer = new byte[0xFFFF]; for (int len; (len = in.read(buffer)) != -1;) { os.write(buffer, 0, len); } os.flush(); return os.toByteArray(); } catch (IOException e) { throw new ConversionException(e); } finally { if (in != null) { try { in.close(); } catch (IOException ignored) { } } } } }
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#17 | 19725 | swellard | Remove the need to pass LoginRequest to login call | ||
#16 | 19655 | drobins | Wrap the retrofit exception in a perforce client one | ||
#15 | 19642 | swellard | Swagger code gen change method names | ||
#14 | 19628 | swellard | Make ApiClient easier to read | ||
#13 | 19611 | swellard | Refactor REST path - make version namespace specific | ||
#12 | 19597 | swellard | Refactor REST path | ||
#11 | 19535 | drobins | Refactor package names to hws | ||
#10 | 19399 | tjuricek |
Upgrade to spark 2.5 which requires a different path for the version information. Spark 2.5's static file matching seems to conflict with just using the "/api" path since a "/publicsite/api" directory actually exists. You end up with a 500 error (NPE exception) if you attempt to map that path. Also, disable the failing RPM test for now. |
||
#9 | 19002 | tjuricek |
Improve API to interact with multiple p4ds. The configuration now requires an explicit setting of what P4Ds HWS can talk to via the 'P4D config dir', where there's a file indicating connection settings per p4d, and importantly, an ID. This is the "server ID" referenced everywhere. Most methods now require a server ID to indicate which p4d to manipulate. In the future, it's likely we will interact with *multiple* p4d instances on some services. This completely removes the ability to run HWS as a kind of an "open proxy" to whatever p4d you want. Given the nature of the change and the lack of priority, we've removed Helix Cloud testing and disabled several methods from their "Helix Cloud" implementation. These will be relatively easy to bring back, we'll just need a new method from Raymond that lists the "allowed server IDs" that map to the HWS configured server IDs for a particular user. Another notable aspect of this change is the use of JSON Web Token to create our authentication key. We associate this key with an in-memory "session" that contains the P4D tickets we use to authenticate users. The JWT token, by default, is assigned a timeout, which allows HWS to block further access to underlying servers without having to interact with multiple auth backends. If any backend fails with that session, the user will get a 403. If you disable the timeout, you'll need to ensure your clients clear out sessions. |
||
#8 | 18798 | tjuricek |
Report supported platform versions in the default request, if we accept application/json. Added a method to the Java client SDK to check if it's a supported version. |
||
#7 | 18737 | tjuricek | Ensure that checking if the system is OK does just returns false if the server isn't running, instead of throwing an exception. | ||
#6 | 18727 | tjuricek | Convert the status method to be part of the documented API, and convert the login-related models. | ||
#5 | 18679 | tjuricek |
Revising HWS paths to work primarily at product version 2016.1. The swagger definitions will primarily work at a major platform release number. We will generate new clients for each major release, and ensure backwards compatibility as time goes on by keeping the older clients around in the tree. Note: These are JUST URLs, and do not include other revisions we plan on making shortly. |
||
#4 | 18605 | tjuricek |
Document a simplified method for obtaining the Java client SDK handle. The client SDK is included as a part of the distribution. Javadoc is included in a subdirectory and hosted directly. A stupid simple HTML page was added by default to give people something to access right after installation. |
||
#3 | 18585 | tjuricek | Adding JavaDoc reference for client libraries to package distributions. | ||
#2 | 18524 | tjuricek |
Add a "trust everything" mode to the generated Java client for testing our installed packages. Also, check in the client's dependencies as well via our "vendor" declarations. It's unclear how we'll distribute the client SDK code, probably as a group of jar files. We'll see. |
||
#1 | 18515 | tjuricek |
Replacing java_client with Swagger-based clients/java project. - Switched implementations of the Swagger client to use okhttp with gson. - Added the version to the "status" method, and hey, added that method to the spec - Added templates to the java code generator to add some default methods, fixing some import issues in Gradle NOTE: We may want to break down the API a bit and restructure it. |