PingAccess

PingAccess Add-On SDK for Java Migration Guide

When upgrading PingAccess, review the changes made to the PingAccess add-on SDK for Java, analyze your addons, and make any necessary changes to ensure continued compatibility.

The following sections provide a detailed description of the changes, organized by package. Where relevant, code examples show you how to port existing code to account for the changes in the Software Development Kit (SDK) APIs.

Because the SDK for PingAccess 6.1 uses Java 8 features, plugins built against the Java add-on SDK for PingAccess 6.1 or later must be built with JDK 8.

Prevent modification to request in response chain

Starting in PingAccess 6.1, any modifications made to a request or its header fields during response processing will now result in a warning log message and the modification operation being ignored. Previously, PingAccess would log a warning message about the modification but still allow the modification operation to complete.

Retrieving key pair and trusted certificate group configuration data

In the previous version of the SDK, a SDK plugin accessed the configuration data of a key pair or trusted certificate group configured using the administrative application programming interface (API) by annotating a field in the plugin’s PluginConfiguration class with a JsonDeserialize annotation, specifying the appropriate custom deserializer from the SDK.

public class Configuration extends SimplePluginConfiguration
{
   @JsonDeserialize(using = PrivateKeyDeserializer.class)
    KeyStore.PrivateKeyEntry keyPair;

   @JsonDeserialize(using = TrustedCertificateGroupDeserializer.class)
    Collection<X509Certificate> certificateGroup;
}

In the current version of the SDK, this mechanism has changed to be less error-prone as well as to provide access to more properties of the key pairs and trusted certificate groups. The previous configuration class should be ported to the following:

public class Configuration extends SimplePluginConfiguration
{
   KeyPairModel keyPair;

   TrustedCertificateGroupModel certificateGroup;
}

The KeyPairModel#getPrivateKeyEntry method provides access to the KeyStore.PrivateKeyEntry object for the corresponding key pair in the administrative configuration. The TrustedCertificateGroupModel#getCertificates method provides access to the Collection of X509Certificate objects in the corresponding trusted certificate group in the administrative configuration. Refer to the JavaDoc for each of these classes for more information.

Related to this change, the provided implementations of ConfigurationModelAccessor, PrivateKeyAccessor and TrustedCertificateGroupAccessor, have been updated to use these new classes. Both classes have also been moved to new packages. PrivateKeyAccessor has also been renamed to KeyPairAccessor.

Before PingAccess 5.0
import com.pingidentity.pa.sdk.accessor.PrivateKeyAccessor;
import com.pingidentity.pa.sdk.accessor.TrustedCertificateGroupAccessor;

// … class definition omitted ...

private void invokePrivateKeyAccessorGet(
       PrivateKeyAccessor accessor,
       String id)
{
   KeyStore.PrivateKeyEntry keyPair = accessor.get(id);
}
private void invokeTrustedCertificateGroupAccessorGet(
       TrustedCertificateGroupAccessor accessor,
       String id)
{
   Collection<X509Certificate> certificates = accessor.get(id);
}
After PingAccess 5.0
import com.pingidentity.pa.sdk.accessor.certgroup.TrustedCertificateGroupModel;
import com.pingidentity.pa.sdk.accessor.keypair.KeyPairAccessor;

// … class definition omitted ...

private void invokePrivateKeyAccessorGet(
       KeyPairAccessor accessor,
       String id)
{
   KeyStore.PrivateKeyEntry keyPair = accessor.get(id)
                                              .map(KeyPairModel::getPrivateKeyEntry)
                                              .orElse(null);
}

private void invokeTrustedCertificateGroupAccessorGet(
       TrustedCertificateGroupAccessor accessor,
       String id)
{
   Collection<X509Certificate> certificates = accessor.get(id)
                   .map(TrustedCertificateGroupModel::getCertificates)
                   .orElse(null);
}

Changes to validation of PluginConfiguration instances

In the previous version of the SDK, the ConfigurablePlugin#configure method was invoked and passed a PluginConfiguration instance. The ConfigurablePlugin was expected to assign the specified PluginConfiguration instance to a field annotated with the javax.validation.Valid annotation. After the configure method returned, PingAccess passed the ConfigurablePlugin instance to a javax.validation.Validator for further validation.

If setup correctly, this logic allows javax.validation.Constraint annotations to declare the validation to be applied to fields in a PluginConfiguration implementation, ensuring the configuration is valid as well as providing validation error message to PingAccess to provide to administrators using the Administrative API or UI.

However, if the ConfigurablePlugin#configure method needed to post-process the specified PluginConfiguration instance, the method needed to duplicate all the validation declared on the fields of the PluginConfiguration.

To remove the need for this duplication of validation logic, PingAccess now validates the PluginConfiguration instance with a javax.validation.Validator prior to passing the instance to the ConfigurablePlugin#configure method.

Further, the ConfigurablePlugin no longer needs to annotate the field used to hold the PluginConfiguration instance. The field is still necessary to implement the ConfigurablePlugin#getConfiguration method.

The following example ConfigurablePlugin implementation demonstrates this change.

public class ValidationExample
       implements ConfigurablePlugin<ValidationExample.Configuration>
{
   // @Valid annotation no longer required
   private Configuration configuration;

   @Override
   public void configure(Configuration configuration) throws ValidationException
   {
       this.configuration = configuration;

       // With the previous version of the SDK, these assertions were not
       // guaranteed to be true, despite the javax.validation.Constraint
       // annotations enforcing these conditions.
       //
       // In the current version of the SDK, these assertions are guaranteed
       // to be true because they are enforced by the javax.validation.Constraint
       // annotations on the fields in the PluginConfiguration class, and the
       // PluginConfiguration validation is performed before invoking the
       // configure method.
       //
       // The end result is that plugins can remove duplicated validation
       // logic from the configure method if further post-processing of the
       // configuration needs to be performed.
       assert(configuration.getAttributeName() != null);
       assert(configuration.getAttributeName().length() > 0);
       assert(configuration.getAttributeName().length() <= 16);
       assert(configuration.getAttributeValue() != null);

   }

   @Override
   public Configuration getConfiguration()
   {
       return configuration;
   }

   static class Configuration extends SimplePluginConfiguration
   {
       @NotNull
       @Size(min = 1,
             max = 16,
             message = "Attribute name length must be between 1 and 16 characters")
       private String attributeName;

       @NotNull
       private String attributeValue;

       public String getAttributeName()
       {
           return attributeName;
       }

       public void setAttributeName(String attributeName)
       {
           this.attributeName = attributeName;
       }

       public String getAttributeValue()
       {
           return attributeValue;
       }

       public void setAttributeValue(String attributeValue)
       {
           this.attributeValue = attributeValue;
       }
   }
}

com.pingidentity.pa.sdk.http

The body interface has changed to require an explicit read of data before invoking methods to obtain that data. Previously, methods to obtain the data would result in an implicit read of the data. The following code examples illustrate this change in semantics.

com.pingidentity.pa.sdk.http.Body

As the updated Javadocs for the body interface indicates, plugins should avoid interrogating a body object unless absolutely necessary because reading a body object’s data into memory can impact the scalability of PingAccess. As the plugin code is updated, evaluate whether the body object needs to be used by the plugin.

Using the Body#read method

Before PingAccess 5.0:

private void invokeRead(Body body) throws IOException
{
    body.read();
}

After PingAccess 5.0:

private void invokeRead(Body body) throws AccessException
{
    try
    {
        body.read();
    }
    catch (IOException e)
    {
        throw new AccessException("Failed to read body content",
                                  HttpStatus.BAD_GATEWAY,
                                  e);
    }
}
Using the Body#getContent method

Before PingAccess 5.0:

private void invokeGetContent(Body body) throws IOException
{
    byte[] content = body.getContent();
}

After PingAccess 5.0:

private void invokeGetContent(Body body) throws AccessException
{
    invokeRead(body); // see the Body#read code example for this method
    byte[] content = body.getContent();
}
Using the Body#getBodyAsStream method

Before PingAccess 5.0:

private void invokeGetBodyAsStream(Body body) throws IOException
{
    InputStream stream = body.getBodyAsStream();
}

After PingAccess 5.0:

private void invokeGetBodyAsStream(Body body) throws AccessException
{
    invokeRead(body); // see the Body#read code example for this method
    InputStream stream = body.newInputStream();
}

The rename of the method from getBodyAsStream to newInputStream.

Using the Body#write method

Before PingAccess 5.0:

private void invokeWrite(Body body, BodyTransferrer bodyTransferrer) throws IOException
{
    body.write(bodyTransferrer);
}

After PingAccess 5.0:

This functionality is no longer supported. To obtain the content of the Body, read the content into memory using the Body#read method and then invoke Body#getContent or Body#newInputStream.

Using the Body#getLength method

Before PingAccess 5.0:

private void invokeGetLength(Body body) throws IOException
{
    int length = body.getLength();
}

After PingAccess 5.0:

private void invokeGetLength(Body body) throws AccessException
{
    invokeRead(body); // see the Body#read code example for this method
    int length = body.getLength();
}
Using the Body#getRaw method

Before PingAccess 5.0:

private void invokeGetRaw(Body body) throws IOException
{
    byte[] rawBody = body.getRaw();
}

After PingAccess 5.0:

This functionality is no longer supported. This method used to provide access to the content as it appeared on the wire, which required complicated handling if the body content used a chunked Transfer-Encoding. Use Body#getContent instead.

com.pingidentity.pa.sdk.http.BodyFactory

Using the BodyFactory#continuousBody method

Before PingAccess 5.0:

private void invokeContinuousBody(BodyFactory bodyFactory, byte[] content)
{
    Body body = bodyFactory.continuousBody(content);
}

After PingAccess 5.0:

private void invokeContinuousBody(BodyFactory bodyFactory, byte[] content)
{
    Body body = bodyFactory.createInMemoryBody(content);
}

Before PingAccess 5.0:

private void invokeContinuousBody(BodyFactory bodyFactory, InputStream in)
{
    Body body = bodyFactory.continuousBody(in);
}

After PingAccess 5.0:

A Body instance can no longer be created from an InputStream using the BodyFactory class. Instead, a plugin should read the contents of the InputStream into a byte array and provide the byte array to BodyFactory#createInMemoryBody.

com.pingidentity.pa.sdk.http.Constants

The constants available from this class have been removed from the SDK. Plugins using these constants should maintain their own constants with the needed values.

com.pingidentity.pa.sdk.http.Exchange

A handful of methods have been removed from the Exchange.

Further, the mechanism for storing data on the exchange through properties has been enhanced to make it easier to write type-safe code when working with Exchange properties.

Using the Exchange#getCreationTime method

Before PingAccess 5.0:

Calendar creationTime = exchange.getCreationTime();

After PingAccess 5.0:

Calendar creationTime = Calendar.getInstance();
creationTime.setTime(Date.from(exchange.getCreationTime()));

If a Calendar object is not required, consider using the Instant object returned from the getCreationTime method directly instead of converting it into a Calendar object.

Using the Exchange#getDestinations method

Before PingAccess 5.0:

List<String> destinations = exchange.getDestinations();

After PingAccess 5.0:

This functionality is no longer supported. Consider using the Exchange#getTargetHosts method to obtain similar information from the Exchange.

Using the Exchange#getOriginalHostHeader method

Before PingAccess 5.0:

String originalHostHeader = exchange.getOriginalHostHeader();

After PingAccess 5.0:

This functionality is no longer supported. Consider using the Exchange#getUserAgentHost method to obtain similar information from the Exchange. The getUserAgentHost method leverages the PingAccess HTTP requests configuration to determine the Host header value sent by the user agent.

Using the Exchange#getOriginalHostHeaderHost method

Before PingAccess 5.0:

String host = exchange.getOriginalHostHeaderHost();

After PingAccess 5.0:

This functionality is no longer supported. Consider using the Exchange#getUserAgentHost method to obtain similar information from the Exchange. The getUserAgentHost method leverages the PingAccess HTTP requests configuration to determine the Host header value sent by the user agent.

Using the Exchange#getOriginalHostHeaderPort method

Before PingAccess 5.0:

String port = exchange.getOriginalHostHeaderPort();

After PingAccess 5.0:

This functionality is no longer supported. Consider using the Exchange#getUserAgentHost method to obtain similar information from the Exchange. The getUserAgentHost method leverages the PingAccess HTTP requests configuration to determine the Host header value sent by the user agent.

Using the Exchange#getOriginalRequestBaseUri method

Before PingAccess 5.0:

String originalRequestBaseUri = exchange.getOriginalRequestBaseUri();

After PingAccess 5.0:

This functionality is no longer supported. A possible replacement is as follows:

String originalRequestBaseUri = exchange.getUserAgentProtocol() +
                                "://" +
                                exchange.getUserAgentHost();
Using the Exchange#getProperties method

Before PingAccess 5.0:

Map<String, String> properties = exchange.getProperties();

After PingAccess 5.0:

This functionality is no longer supported. Properties should be obtained individually from the Exchange.

Using the Exchange#getRequestBaseUri method

Before PingAccess 5.0:

String requestBaseUri = exchange.getRequestBaseUri();

After PingAccess 5.0:

This functionality is no longer supported. A possible replacement is as follows.

String requestBaseUri = exchange.getUserAgentProtocol() +
                        "://" +
                        exchange.getUserAgentHost();
Using the Exchange#getRequestScheme method

Before PingAccess 5.0:

String requestScheme = exchange.getRequestScheme();

After PingAccess 5.0:

This functionality is no longer supported. A possible replacement is as follows.

String requestScheme = exchange.getUserAgentProtocol() + "://";
Using the Exchange#getUser method

Before PingAccess 5.0:

private void invokeSetUser(Exchange exchange, User user)
{
    exchange.setUser(user);
}

After PingAccess 5.0:

This functionality is no longer supported. The identity associated with an Exchange cannot be replaced.

Using the Exchange#setUser method

Before PingAccess 5.0:

private void invokeSetUser(Exchange exchange, User user)
{
    exchange.setUser(user);
}

After PingAccess 5.0:

This functionality is no longer supported. The identity associated with an Exchange cannot be replaced.

Using the Exchange#setSourceIp method

Before PingAccess 5.0:

private void invokeSetSourceIp(Exchange exchange, String sourceIp)
{
    exchange.setSourceIp(sourceIp);
}

After PingAccess 5.0:

This functionality is no longer supported. This value cannot be changed.

Using the Exchange#setProperty method

Before PingAccess 5.0:

private void invokeSetProperty(Exchange exchange, String propertyKey, String value)
{
    exchange.setProperty(propertyKey, value);
}

After PingAccess 5.0:

private void invokeSetProperty(Exchange exchange,
                               ExchangeProperty<String> propertyKey,
                               String value)
{
    exchange.setProperty(propertyKey, value);
}

See the Javadocs for ExchangeProperty for instructions on creating an ExchangeProperty object.

Using the Exchange#getProperty method

Before PingAccess 5.0:

private void invokeGetProperty(Exchange exchange, String propertyKey)
{
    Object propertyValueObj = exchange.getProperty(propertyKey);
    if (propertyValueObj instanceof String)
    {
        String propertyValue = (String) propertyValueObj;
    }
}

After PingAccess 5.0:

private void invokeGetProperty(Exchange exchange, ExchangeProperty<String> propertyKey)
{
    String propertyValue = exchange.getProperty(propertyKey).orElse(null);
}

Exchange#getProperty now returns an Optional object instead of the Object directly.

com.pingidentity.pa.sdk.http.Header

This deprecated class has been replaced by the Headers interface. A Headers object can be created using a HeadersFactory obtained from the ServiceFactory#headersFactory method. The majority of methods on Header have counterparts on the Headers interface. See the Javadocs for the Headers interface for more information.

com.pingidentity.pa.sdk.http.HeaderField

This class is now final and cannot be extended.

Constructing a HeaderField

Before PingAccess 5.0:

private HeaderField createHeaderField(String line)
{
    return new HeaderField(line);
}

After PingAccess 5.0:

private HeaderField createHeaderField(String line)
{
    String name = line.substring(0, line.indexOf(':'));
    String value = (line.substring(line.indexOf(":") + 1)).trim();

    return new HeaderField(name, value);
}

Parsing an HTTP header field line can be error prone, consider if the plugin can avoid having to parse an HTTP header field line.

Using the HeaderField#setHeaderName method

Before PingAccess 5.0:

private void invokeSetHeaderName(HeaderField field)
{
    field.setHeaderName(new HeaderName("X-Custom"));
}

After PingAccess 5.0:

This functionality is no longer supported. A HeaderField’s name is set upon construction and cannot be changed.

Using the HeaderField#getApproximateSize method

Before PingAccess 5.0:

int approximateSize = field.getApproximateSize();

After PingAccess 5.0:

This method has been removed. The value returned by the method can still be computed:

int approximateSize = 2 * (4 +
                           field.getHeaderName().toString().length() +
                           field.getValue().length());

com.pingidentity.pa.sdk.http.Headers

A few methods on the Headers interface have been updated to use the instant class, instead of date.

Using the Headers#getDate method

Before PingAccess 5.0:

Date date = headers.getDate();

After PingAccess 5.0:

Date date = Date.from(headers.getDate());
Using the Headers#setDate method

Before PingAccess 5.0:

private void invokeSetDate(Headers headers, Date date)
{
    headers.setDate(date);
}

After PingAccess 5.0:

private void invokeSetDate(Headers headers, Date date)
{
    headers.setDate(date.toInstant());
}
Using the Headers#getLastModified method

Before PingAccess 5.0:

SimpleDateFormat format = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss z",
                                               Locale.ENGLISH);

String lastModified = headers.getLastModified();
if (lastModified != null)
{
    Date lastModifiedDate = format.parse(lastModified);
}

After PingAccess 5.0:

Date lastModifiedDate = Date.from(headers.getLastModified());
Using the Headers#setLastModified method

Before PingAccess 5.0:

private void invokeSetLastModified(Headers headers, Date date)
{
    SimpleDateFormat format = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss z",
                                                   Locale.ENGLISH);

    headers.setLastModified(format.format(date));
}

After PingAccess 5.0:

private void invokeSetLastModified(Headers headers, Date date)
{
    headers.setLastModified(date.toInstant());
}

com.pingidentity.pa.sdk.http.HeadersFactory

Using the HeadersFactory#createFromRawHeaderFields method

Before PingAccess 5.0:

private void invokeCreateFromRawHeaderFields(HeadersFactory factory,
                                             List<String> fields) throws ParseException
{
    Headers headers = factory.createFromRawHeaderFields(fields);
}

After PingAccess 5.0:

This functionality is no longer supported. Consider if the plugin can create HeaderFields directly and utilize the HeadersFactory#create method.

com.pingidentity.pa.sdk.http.HttpStatus

The HttpStatus enum was converted to a final class. Common HttpStatus instances are defined as constants on HttpStatus.

Using the HttpStatus#getLocalizationKey method

Before PingAccess 5.0:

String localizationKey = status.getLocalizationKey();

After PingAccess 5.0:

This functionality is no longer supported. Instead, a HttpStatus contains a LocalizedMessage instance that encapsulates the localization of the status message for use in error templates.

com.pingidentity.pa.sdk.http.MimeType

The constants available in this class are now available as constant MediaType instances in the class com.pingidentity.pa.sdk.http.CommonMediaTypes.

com.pingidentity.pa.sdk.http.MediaType

This class is now final and cannot be extended.

Constructing a MediaType

Before PingAccess 5.0:

private void createMediaType(String mediaTypeString)
{
    MediaType mediaType = new MediaType(mediaTypeString);
}

After PingAccess 5.0:

private void createMediaType(String mediaTypeString)
{
    MediaType mediaType = MediaType.parse(mediaTypeString);
}

com.pingidentity.pa.sdk.http.Message

A number of methods have been removed from the Message interface.

Using the Message#getBodyAsStream method

Before PingAccess 5.0:

InputStream bodyStream = message.getBodyAsStream();

After PingAccess 5.0:

This functionality is no longer supported. However, the following code snippet can be used to maintain semantics of the old method.

Body body = message.getBody();
try
{
    body.read();
}
catch (IOException | AccessException e)
{
    throw new RuntimeException("Could not get body as stream", e);
}

InputStream bodyStream = body.newInputStream();

While this snippet maintains semantics, enable a plugin to propagate errors as an AccessException instead of as a RuntimeException.

Using the Message#getCharset method

Before PingAccess 5.0:

Charset charset = message.getCharset();

After PingAccess 5.0:

This functionality is no longer supported. However, the following code snippet can be used to maintain semantics of the old method.

Charset charset = message.getHeaders().getCharset();
if (charset == null)
{
    charset = StandardCharsets.UTF_8;
}

While this snippet maintains semantics, a plugin should consider how to handle the case where a Charset is not specified by a Message’s header fields. Assuming a Charset of UTF-8 might lead to issues in some cases.

Using the Message#getHeader method

Before PingAccess 5.0:

Header header = message.getHeader();

After PingAccess 5.0:

This functionality is no longer supported. Instead, use Message#getHeaders and the Headers interface instead of Header.

Using the Message#setHeader method

Before PingAccess 5.0:

private void invokeSetHeader(Message message, Header header)
{
    message.setHeader(header);
}

After PingAccess 5.0:

This functionality is no longer supported. Instead, use Message#setHeaders and the Headers interface instead of Header.

Using the Message#isDeflate method

Before PingAccess 5.0:

boolean deflate = message.isDeflate();

After PingAccess 5.0:

This method has been removed. However, the value can still be computed with the following code snippet.

List<String> contentEncodingValues = message.getHeaders().getContentEncoding();
boolean deflate = contentEncodingValues.stream().anyMatch(v -> v.equalsIgnoreCase("deflate"))
                  && contentEncodingValues.size() == 1;
Using the Message#isGzip method

Before PingAccess 5.0:

boolean gzip = message.isGzip();

After PingAccess 5.0:

This method has been removed. However, the value can still be computed with the following code snippet.

List<String> contentEncodingValues = message.getHeaders().getContentEncoding();
boolean gzip = contentEncodingValues.stream().anyMatch(v -> v.equalsIgnoreCase("gzip"))
               && contentEncodingValues.size() == 1;
Using the Message#isHTTP10 method

Before PingAccess 5.0:

boolean http10 = message.isHTTP10();

After PingAccess 5.0:

This method has been removed. However, the value can still be computed with the following code snippet.

boolean http10 = message.getVersion().equals("1.0");
Using the Message#isHTTP11 method

Before PingAccess 5.0:

boolean http11 = message.isHTTP11();

After PingAccess 5.0:

The method has been removed. However, the value can still be computed with the following code snippet.

boolean http11 = message.getVersion().equals("1.1");
Using the Message#read method

Before PingAccess 5.0:

private void invokeRead(Message message,
                        InputStream inputStream,
                        boolean createBody) throws IOException
{
    message.read(inputStream, createBody);
}

After PingAccess 5.0:

This functionality is no longer supported. A request attached to an exchange can no longer be completely replaced, but individual components can be replaced, such as the method, Uniform Resource Identifier (URI), headers and body. A response attached to an exchange can be replaced by using Exchange#setResponse.

Using the Message#setVersion method

Before PingAccess 5.0:

private void invokeSetVersion(Message message, String version)
{
    message.setVersion(version);
}

After PingAccess 5.0:

This functionality is no longer supported. The version of a message cannot be changed.

Using the Message#write method

Before PingAccess 5.0:

private void invokeWrite(Message message,
                         OutputStream output) throws IOException
{
    message.write(output);
}

After PingAccess 5.0:

This functionality is no longer supported. However, the following code snippet can be used to perform the equivalent operation.

private void invokeWrite(Message message,
                         OutputStream output) throws IOException, AccessException
{
    Body body = message.getBody();
    body.read();

    output.write(message.getStartLine().getBytes(StandardCharsets.ISO_8859_1));
    output.write(message.getHeaders().toString().getBytes(StandardCharsets.ISO_8859_1));
    output.write("\r\n".getBytes(StandardCharsets.ISO_8859_1));
    output.write(body.getContent());
    output.flush();
}

com.pingidentity.pa.sdk.http.Method

The method interface has been converted to a final class. Additionally, the related methods enum has been merged into the method class. The method class provides common method instances as class-level constants.

Obtaining a common Method instance

Before PingAccess 5.0:

Method get = Methods.GET

After PingAccess 5.0:

Method get = Method.GET;
Using the Method#getMethodName method

Before PingAccess 5.0:

String methodName = method.getMethodName();

After PingAccess 5.0:

String methodName = method.getName();

com.pingidentity.pa.sdk.http.Request

A few methods have been removed from the request interface.

Using the Request#getPostParams method

Before PingAccess 5.0:

private void invokeGetPostParams(Request request) throws IOException
{
    Map<String, String[]> postParams = request.getPostParams();
}

After PingAccess 5.0:

private void invokeGetPostParams(Request request) throws AccessException
{
    Body body = request.getBody();
    try
    {
        body.read();
    }
    catch (IOException e)
    {
        throw new AccessException("Failed to read body content",
                                  HttpStatus.BAD_GATEWAY,
                                  e);
    }

    Map<String, String[]> postParams = body.parseFormParams();
}
Using the Request#isMultipartFormPost method

Before PingAccess 5.0:

boolean multipartFormPost = request.isMultipartFormPost();

After PingAccess 5.0:

This method has been removed from the Request interface. However, the value can still be calculated using the following code snippet.

Headers headers = request.getHeaders();

boolean multipartFormPost =
        request.getMethod() == Method.POST
        && headers.getContentType() != null
        && headers.getContentType().getBaseType().equals("multipart/form-data")
        && headers.getContentType().getParameter("boundary") != null;

com.pingidentity.pa.sdk.http.ResponseBuilder

A handful of methods were removed from ResponseBuilder. Additionally, a handful of methods have changed their semantics, particularly those that included an HTML message payload. See the updated Javadocs for ResponseBuilder for more info.

Using the ResponseBuilder#badRequestText method

Before PingAccess 5.0:

Response response = ResponseBuilder.badRequestText(message).build();

After PingAccess 5.0:

Response response = ResponseBuilder.newInstance(HttpStatus.BAD_REQUEST)
                                   .contentType(CommonMediaTypes.TEXT_PLAIN)
                                   .body(message)
                                   .build();

This approach does not localize the response body. Using a TemplateRenderer is recommended instead.

Using the ResponseBuilder#contentLength method

Before PingAccess 5.0:

Response response = ResponseBuilder.newInstance().contentLength(length).build();

After PingAccess 5.0:

This functionality is no longer supported. Consider using one of the ResponseBuilder#body methods instead of explicitly setting the content length. This ensures that the body content of the Response aligns with the Content-Length header field.

Using the ResponseBuilder#continue100 method

Before PingAccess 5.0:

Response response = ResponseBuilder.continue100().build();

After PingAccess 5.0:

Response response = ResponseBuilder.newInstance(HttpStatus.CONTINUE).build();
Using the ResponseBuilder#forbiddenText method

Before PingAccess 5.0:

Response response = ResponseBuilder.forbiddenText().build();

After PingAccess 5.0:

Response response = ResponseBuilder.newInstance(HttpStatus.FORBIDDEN)
                                   .contentType(CommonMediaTypes.TEXT_PLAIN)
                                   .body(HttpStatus.FORBIDDEN.getMessage())
                                   .build();

This approach does not localize the response body. Use a TemplateRenderer instead.

Using the ResponseBuilder#forbiddenWithoutBody method

Before PingAccess 5.0:

Response response = ResponseBuilder.forbiddenWithoutBody().build();

After PingAccess 5.0:

Response response = ResponseBuilder.newInstance(HttpStatus.FORBIDDEN).build();

Before PingAccess 5.0:

Response response = ResponseBuilder.forbiddenWithoutBody(message).build();

After PingAccess 5.0:

Response response = ResponseBuilder.newInstance(HttpStatus.FORBIDDEN).build();

In the original method, the string message parameter was not used.

Using the ResponseBuilder#htmlMessage method

Before PingAccess 5.0:

String message = ResponseBuilder.htmlMessage(caption, text);

After PingAccess 5.0:

This functionality is no longer supported. Plugins that used this method will need to construct the HTML message without this method. Consider using the TemplateRenderer utility class in place of this method.

Using the ResponseBuilder#internalServerError method

Before PingAccess 5.0:

Response response = ResponseBuilder.internalServerError(message).build();

After PingAccess 5.0:

Response response = ResponseBuilder.internalServerError().body(message).build();

This approach does not localize the response body. Use a TemplateRenderer instead.

Using the ResponseBuilder#internalServerErrorWithoutBody method

Before PingAccess 5.0:

Response response = ResponseBuilder.internalServerErrorWithoutBody().build();

After PingAccess 5.0:

Response response = ResponseBuilder.internalServerError().build();
Using the ResponseBuilder#newInstance method

The no-arg newInstance method has been removed. A HttpStatus is required to create an instance of ResponseBuilder, and the required HttpStatus object should be passed to the newInstance method that accepts a HttpStatus.Before PingAccess 5.0:

Response response = ResponseBuilder.newInstance().build()

After PingAccess 5.0:

Response response = ResponseBuilder.newInstance(HttpStatus.INTERNAL_SERVER_ERROR).build();
Using the ResponseBuilder#noContent method

Before PingAccess 5.0:

Response response = ResponseBuilder.noContent().build();

After PingAccess 5.0:

Response response = ResponseBuilder.newInstance(HttpStatus.NO_CONTENT).build();
Using the ResponseBuilder#notFoundWithoutBody method

Before PingAccess 5.0:

Response response = ResponseBuilder.notFoundWithoutBody().build();

After PingAccess 5.0:

Response response = ResponseBuilder.notFound().build();
Using the ResponseBuilder#serverUnavailable method

Before PingAccess 5.0:

Response response = ResponseBuilder.serverUnavailable(message).build();

After PingAccess 5.0:

Response response = ResponseBuilder.serviceUnavailable().body(message).build();

This approach does not localize the response body. Use a TemplateRenderer instead.

Using the ResponseBuilder#serviceUnavailableWithoutBody method

Before PingAccess 5.0:

Response response = ResponseBuilder.serverUnavailableWithoutBody().build();

After PingAccess 5.0:

Response response = ResponseBuilder.serviceUnavailable().build();
Using the ResponseBuilder#status method

The status methods have been removed. Instead the status should be specified to the newInstance method as it is now required.Before PingAccess 5.0:

Response response = ResponseBuilder.newInstance().status(HttpStatus.OK).build();

After PingAccess 5.0:

Response response = ResponseBuilder.newInstance(HttpStatus.OK).build();
Using the ResponseBuilder#unauthorizedWithoutBody method

Before PingAccess 5.0:

Response response = ResponseBuilder.unauthorizedWithoutBody().build();

After PingAccess 5.0:

Response response = ResponseBuilder.unauthorized().build();

com.pingidentity.pa.sdk.http.Response

A few methods were removed from the response interface.

Using the Response#isRedirect method

Before PingAccess 5.0:

boolean redirect = response.isRedirect();

After PingAccess 5.0:

boolean redirect = response.getStatusCode() >= 300
                   && response.getStatusCode() < 400;
Using the Response#setStatusCode method

Before PingAccess 5.0:

response.setStatusCode(HttpStatus.OK.getCode());

After PingAccess 5.0:

response.setStatus(HttpStatus.OK);
Using the Response#setStatusMessage method

Before PingAccess 5.0:

response.setStatusMessage(HttpStatus.OK.getMessage());

After PingAccess 5.0:

response.setStatus(HttpStatus.OK);

com.pingidentity.pa.sdk.identity

com.pingidentity.pa.sdk.identity.Identity

The getTokenExpiration method was updated to use an instant instead of date.

Using the Identity#getTokenExpiration method

Before PingAccess 5.0:

Date expiration = identity.getTokenExpiration();

After PingAccess 5.0:

Date expiration = Date.from(identity.getTokenExpiration());

com.pingidentity.pa.sdk.identity.OAuthTokenMetadata

The OAuthTokenMetadata methods now use an instant instead of a date.

Using the OAuthTokenMetadata#getExpiresAt method

Before PingAccess 5.0:

Date expiresAt = metadata.getExpiresAt();

After PingAccess 5.0:

Date expiresAt = Date.from(metadata.getExpiresAt());
Using the OAuthTokenMetadata#getRetrievedAt method

Before PingAccess 5.0:

Date retrievedAt = metadata.getRetrievedAt();

After PingAccess 5.0:

Date retrievedAt = Date.from(metadata.getRetrievedAt());

com.pingidentity.pa.sdk.identitymapping.header

ClientCertificateMapping has been removed from the SDK, as it was not required to create an IdentityMappingPlugin implementation.

Plugins utilizing this class should create their own version of this class.

com.pingidentity.pa.sdk.policy

com.pingidentity.pa.sdk.policy.AccessExceptionContext

The nested Builder class has been removed from AccessExceptionContext and instead AccessExceptionContext is a builder that can be initially created with the new AccessExceptionContext#create method.

The LocalizedMessage interface has been introduced to simplify the configuration of a localized message for use in an error template. A LocalizedMessage has three implementations provided in the SDK: FixedMessage, BasicLocalizedMessage and ParameterizedLocalizedMessage. See the following code examples for more information on using these new classes.

Constructing an AccessExceptionContext

Before PingAccess 5.0:

private AccessExceptionContext createAccessExceptionContext(HttpStatus httpStatus,
                                                            Throwable cause)

{
    return AccessExceptionContext.builder()
                                 .cause(cause)
                                 .httpStatus(httpStatus)
                                 .exceptionMessage(httpStatus.getMessage())
                                 .errorDescription(httpStatus.getLocalizationKey())
                                 .errorDescriptionIsKey(true)
                                 .errorDescriptionSubstitutions(new String[0])
                                 .build();
}

After PingAccess 5.0:

private AccessExceptionContext createAccessExceptionContext(HttpStatus httpStatus,
                                                            Throwable cause)
{
    return AccessExceptionContext.create(httpStatus)
                                 .cause(cause)
                                 .exceptionMessage(httpStatus.getMessage())
                                 .errorDescription(httpStatus.getLocalizedMessage());
}

Before PingAccess 5.0:

private AccessExceptionContext createAccessExceptionContext(HttpStatus httpStatus,
                                                            String localizationKey,
                                                            String[] substitutions)
{
    return AccessExceptionContext.builder()
                                 .httpStatus(httpStatus)
                                 .errorDescription(localizationKey)
                                 .errorDescriptionIsKey(true)
                                 .errorDescriptionSubstitutions(substitutions)
                                 .build();
}

After PingAccess 5.0:

private AccessExceptionContext createAccessExceptionContext(HttpStatus httpStatus,
                                                            String localizationKey,
                                                            String[] substitutions)
{
    LocalizedMessage localizedMessage =
            new ParameterizedLocalizedMessage(localizationKey, substitutions);

    return AccessExceptionContext.create(httpStatus)
                                 .errorDescription(localizedMessage);
}

Before PingAccess 5.0:

private AccessExceptionContext createAccessExceptionContext(HttpStatus httpStatus,
                                                            String localizationKey)
{
    return AccessExceptionContext.builder()
                                 .httpStatus(httpStatus)
                                 .errorDescription(localizationKey)
                                 .errorDescriptionIsKey(true)
                                 .build();
}

After PingAccess 5.0:

private AccessExceptionContext createAccessExceptionContext(HttpStatus httpStatus,
                                                            String localizationKey)
{
    LocalizedMessage localizedMessage = new BasicLocalizedMessage(localizationKey);
    return AccessExceptionContext.create(httpStatus)
                                 .errorDescription(localizedMessage);
}

Before PingAccess 5.0:

private AccessExceptionContext createAccessExceptionContext(HttpStatus httpStatus)
{
    return AccessExceptionContext.builder()
                                 .from(httpStatus)
                                 .httpStatusDescription(httpStatus.getLocalizationKey())
                                 .httpStatusDescriptionIsKey(true)
                                 .templateFile("template.html")
                                 .contentType("text/html");
}

After PingAccess 5.0:

private AccessExceptionContext createAccessExceptionContext(HttpStatus httpStatus)
{
    return AccessExceptionContext.create(httpStatus)
                                 .errorDescription(httpStatus.getLocalizedMessage());
}

This example demonstrates that it is no longer possible to set a template file and its associated content type on an AccessExceptionContext. To generate an error response from a template file, use the TemplateRenderer class. See the Javadocs for the TemplateRenderer class for more information.

com.pingidentity.pa.sdk.policy.AccessException

The changes to AccessExceptionContext apply to the creation of AccessException because the creation of an AccessException requires an AccessExceptionContext.

In addition to these changes, obtaining information from AccessException has also changed. See the code examples below for more information.

Finally, AccessException no longer derives from IOException and derives directly from Exception instead.

Constructing an AccessException

Before PingAccess 5.0:

private void throwAccessException(String errorDescription,
                                  Throwable throwable) throws AccessException
{
    throw new AccessException(errorDescription, throwable);
}

After PingAccess 5.0:

private void throwAccessException(String errorDescription,
                                  Throwable throwable) throws AccessException
{
    LocalizedMessage templateMessaage = new FixedMessage(errorDescription);
    throw new AccessException(AccessExceptionContext.create(HttpStatus.INTERNAL_SERVER_ERROR)
                                                    .exceptionMessage(errorDescription)
                                                    .cause(throwable)
                                                    .errorDescription(templateMessaage));
}

Before PingAccess 5.0:

private void throwAccessException(String errorDescription) throws AccessException
{
    throw new AccessException(errorDescription);
}

After PingAccess 5.0:

private void throwAccessException(String errorDescription) throws AccessException
{
    LocalizedMessage templateMessage = new FixedMessage(errorDescription);
    throw new AccessException(AccessExceptionContext.create(HttpStatus.INTERNAL_SERVER_ERROR)
                                                    .exceptionMessage(errorDescription)
                                                    .errorDescription(templateMessage));
}

Before PingAccess 5.0:

private void createAccessException(int statusCode,
                                   String statusMessage,
                                   String errorDescription) throws AccessException
{
    throw new AccessException(statusCode, statusMessage, errorDescription);
}

After PingAccess 5.0:

private void createAccessException(int statusCode,
                                   String statusMessage,
                                   String errorDescription) throws AccessException
{
    HttpStatus httpStatus = new HttpStatus(statusCode, statusMessage);
    LocalizedMessage templateMessage = new FixedMessage(errorDescription);
    throw new AccessException(AccessExceptionContext.create(httpStatus)
                                                    .exceptionMessage(errorDescription)
                                                    .errorDescription(templateMessage));
}

Before PingAccess 5.0:

private void throwAccessException(int statusCode,
                                  String statusMessage,
                                  String errorDescription,
                                  Throwable throwable) throws AccessException
{
    throw new AccessException(statusCode, statusMessage, errorDescription, throwable);
}

After PingAccess 5.0:

private void throwAccessException(int statusCode,
                                  String statusMessage,
                                  String errorDescription,
                                  Throwable throwable) throws AccessException
{
    HttpStatus httpStatus = new HttpStatus(statusCode, statusMessage);
    LocalizedMessage templateMessage = new FixedMessage(errorDescription);
    throw new AccessException(AccessExceptionContext.create(httpStatus)
                                                    .exceptionMessage(errorDescription)
                                                    .errorDescription(templateMessage)
                                                    .cause(throwable));
}

Before PingAccess 5.0:

private void throwAccessException() throws AccessException
{
    throw new AccessException(AccessExceptionContext.builder()
                                                    .httpStatusCode(403)
                                                    .httpStatusMessage("Forbidden")
                                                    .build());
}

After PingAccess 5.0:

private void throwAccessException() throws AccessException
{
    throw new AccessException(AccessExceptionContext.create(HttpStatus.FORBIDDEN));
}
Using the AccessException#getExceptionContext method

Before PingAccess 5.0:

AccessExceptionContext context = accessException.getExceptionContext();

After PingAccess 5.0:

This functionality is no longer supported. The information that used to be provided by the AccessExceptionContext is now provided directly by an AccessException.

Using the AccessException#getHttpStatusCode method

Before PingAccess 5.0:

int statusCode = accessException.getHttpStatusCode();

After PingAccess 5.0:

int statusCode = accessException.getErrorStatus().getCode();
Using the AccessException#getHttpStatusMessage method

Before PingAccess 5.0:

String statusMessage = accessException.getHttpStatusMessage();

After PingAccess 5.0:

String statusMessage = accessException.getErrorStatus().getMessage();
Using the AccessException#setHttpStatusCode method

Before PingAccess 5.0:

accessException.setHttpStatusCode(statusCode);

After PingAccess 5.0:

This functionality is no longer supported. The status code associated with an AccessException is fixed once it is constructed.

Using the AccessException#setHttpStatusMessage method

Before PingAccess 5.0:

accessException.setHttpStatusMessage(statusMessage);

After PingAccess 5.0:

This functionality is no longer supported. The status message associated with an AccessException is fixed once it is constructed.

com.pingidentity.pa.sdk.policy.RuleInterceptor

The handleRequest and handleResponse methods on a RuleInterceptor no longer throw an IOException. Instead, they throw an AccessException, which no longer derives from IOException.

Accounting for the RuleInterceptor#handleRequest method signature change

Before PingAccess 5.0:

@Override
public Outcome handleRequest(Exchange exchange) throws IOException
{
    Outcome outcome = applyPolicy(exchange);

    return outcome;
}

After PingAccess 5.0:

@Override
public Outcome handleRequest(Exchange exchange) throws AccessException
{
    Outcome outcome = applyPolicy(exchange);

    return outcome;
}
Account for the RuleInterceptor#handleResponse method signature change

Before PingAccess 5.0:

@Override
public void handleResponse(Exchange exchange) throws IOException
{
    applyPolicyToResponse(exchange.getResponse());
}

After PingAccess 5.0:

@Override
public void handleResponse(Exchange exchange) throws AccessException
{
    applyPolicyToResponse(exchange.getResponse());
}

com.pingidentity.pa.sdk.policy.error.InternalServerErrorCallback

This class has been removed. Use LocalizedInternalServerErrorCallback instead.

com.pingidentity.pa.sdk.services

com.pingidentity.pa.sdk.services.ServiceFactory

This class is now final and cannot be extended.

com.pingidentity.pa.sdk.siteauthenticator

com.pingidentity.pa.sdk.siteauthenticator.SiteAuthenticatorInterceptor

This interface is no longer a RequestInterceptor or ResponseInterceptor, but it still defines the handleRequest and handleResponse methods.

public interface SiteAuthenticatorInterceptor<T extends PluginConfiguration>
        extends DescribesUIConfigurable, ConfigurablePlugin<T>
{
    void handleRequest(Exchange exchange) throws AccessException;
    void handleResponse(Exchange exchange) throws AccessException;
}

Additionally, these methods now only throw an AccessException instead of an IOException or InterruptedException.

Accounting for the SiteAuthenticatorInterceptor#handleRequest method signature change

Before PingAccess 5.0:

@Override
public Outcome handleRequest(Exchange exc)
        throws RuntimeException, IOException, InterruptedException
{
    // Site authenticator implementation //

    return Outcome.CONTINUE;
}

After PingAccess 5.0:

@Override
public void handleRequest(Exchange exc) throws AccessException
{
    // Site authenticator implementation //
}
Accounting for the SiteAuthenticatorInterceptor#handleResponse method signature chang

Before PingAccess 5.0:

@Override
public void handleResponse(Exchange exc) throws IOException
{
    // Site authenticator response implementation //
}

After PingAccess 5.0:

@Override
public void handleResponse(Exchange exc) throws AccessException
{
    // Site authenticator response implementation //
}

com.pingidentity.pa.sdk.ui

com.pingidentity.pa.sdk.ui.ConfigurationType

The deprecated PRIVATEKEY enum value has been removed. Use a ConfigurationType of ConfigurationType#SELECT and specify the PrivateKeyAccessor.class instance to ConfigurationBuilder#dynamicOptions or UIElement#modelAccessor.

com.pingidentity.pa.sdk.user

com.pingidentity.pa.sdk.user.User

This class has been removed from the SDK. Use the identity interface instead. An instance of identity can be retrieved from the exchange, similar to the user interface.

com.pingidentity.pa.sdk.util

com.pingidentity.pa.sdk.util.TemplateRenderer

The semantics of the renderResponse method have changed so it produces a response and does not have any side-effects on the specified parameters.

Before PingAccess 5.0
private void invokeRenderResponse(TemplateRenderer templateRenderer,
                                  Map<String, String> context,
                                  String templateName,
                                  Exchange exchange,
                                  ResponseBuilder builder)
{
    templateRenderer.renderResponse(context, templateName, exchange, builder);
}
After PingAccess 5.0
private void invokeRenderResponse(TemplateRenderer templateRenderer,
                                  Map<String, String> context,
                                  String templateName,
                                  Exchange exchange,8
                                  ResponseBuilder builder)
{
    Response response = templateRenderer.renderResponse(exchange,
                                                        context,
                                                        templateName,
                                                        builder);
    exchange.setResponse(response);
}