dimanche 25 octobre 2020

What type of design pattern is this and is a good one (approach)?

I have a Spring-based application where I have to support two types of credentials at config time which are determined at runtime and need to be passed to two types of requst builders (SOAP with JAX-WS (+ JAXB models) and plain HTTP via Apache HttpClient with a custom RPC-style XML-based approach) which will provide the rest for a client to complete the request. Most is already in place, but the credentials are tricky:

  • I don't want the builders to see the actual implementing classes of the credentials. Ideally, interfaces only.
  • I don't want to leak implemenation details of the request into the credentials too because they are fundamentally different.

What I have for the moment is (simplified) the following: SOAP requests have a builder hierarchy:

public abstract class RequestBuilder<T> {

  protected static enum AuthType {
    PLAIN, SSO;
  }

  /* Builder properties */
  protected AuthType authType;
  protected String username;

  // Plain auth
  protected String password;

  // SSO auth
  protected String ssoToken;
  protected URL ssoLoginUrl;
  protected String ssoAppId;

  ...

  public RequestBuilder<T> plainAuth(String username, String password) {
    check();
    this.authType = AuthType.PLAIN;
    this.username = validateAndReturnString("username", username);
    this.password = validateAndReturnString("password", password);

    return this;
  }

  public RequestBuilder<T> ssoAuth(String username, String ssoToken, URL ssoLoginUrl,
      String ssoAppId) {
    check();
    this.authType = AuthType.SSO;
    this.username = validateAndReturnString("username", username);
    this.ssoToken = validateAndReturnString("ssoToken", ssoToken);
    this.ssoLoginUrl = validateAndReturnObject("ssoLoginUrl", ssoLoginUrl);
    this.ssoAppId = validateAndReturnString("ssoAppId", ssoAppId);

    return this;
  }

  protected void validateAuth() {
    validateObjectState("authType", authType);

    switch (authType) {
    case PLAIN:
      validateStringState("username", username);
      validateStringState("password", password);
      break;
    case SSO:
      validateObjectState("ssoLoginUrl", ssoLoginUrl);
      validateStringState("ssoAppId", ssoAppId);
      validateStringState("username", username);
      validateStringState("ssoToken", ssoToken);
      break;
    default:
      // Cannot happen
      break;
    }
  }

  public abstract T build() throws RequestBuildException;

}

and a concrete implementation:

public class ImportRequestBuilder extends RequestBuilder<ImportRequest> {

  // Builder properties
  private Input input;

   public ImportRequestBuilder input(Input input) {
    check();
    this.input = validateAndReturnObject("input", input);

    return this;
  }

  public ImportRequest build() throws RequestBuildException {
    ImportModelObjectFactory tcOf = new ImportModelObjectFactory();

    validateAuth();
    validateObjectState("input", input);

    ImportRequest request = tcOf.createImportRequest();

    switch (authType) {
    case PLAIN:
      request.setUsername(tcOf.createUsername(username));
      request.setPassword(tcOf.createPassword(password));
      break;
    case SSO:
      request.setSsoUrl(tcOf.createSsoUrl(ssoLoginUrl.toExternalForm()));
      request.setSsoAppId(tcOf.createSsoAppId(ssoAppId));
      request.setUsername(tcOf.createUsername(username));
      request.setSsoToken(tcOf.createSsoToken(ssoToken));
      break;
    default:
      // Cannot happen
      break;
    }

    ...

    request.setInput(tcOf.createInput(sw.toString()));

    return request;
  }

}

The plain requests do not have builders at all, they basically load a template for the XML content, interpolate properties in that template and pass on to HttpClient:

HttpPost login = new HttpPost(LOGIN_PATH);

Map<String, String> vars = new HashMap<>();
vars.put("username", username);
vars.put("password", (password));

HttpEntity entity = requestUtils.mergeAndCreate(loginBody, vars);
login.setEntity(entity);
...

For SSO accordingly different along with a different loginBody.

The credentials shall be provided through an abstract factory (singleton):

public abstract CredentialsFactory {

  Credentials createCredentials();

}

along with two implementations PlainCredentialsFactory and SsoCredentialsFactory, plus

interface Credentials {

  AuthType getAuthType();

  String getUsername();

  void populate(RequestBuilder bulder);

}

Everything else is implementation private to the PlainCredentials and SsoCredentials. Now, as mentioned earlier, I don't want RequestBuilders to be aware of these two classes, but the interface only neither do I want to the opposite.

My initial design is to apply some kind of director pattern (you have to judge now) via the populate method, but this will only satisfy the SOAP builder, not the plain HTTP one. I am not happy with this design because I have to please two builder types, ugly and the Credentials have to be aware of the builder interface.

Is the above a director pattern at all or just bad coupling? An alternative approach is to use an adapter/transporter pattern with a CredentialsReceiver which could be implemented by builders and reside in the same module as the Credentials class:

interface CredentailsReceiver {

    void plainAuth(String username, String password);
 
    void ssoAuth(String username, String ssoToken, ...);

}

with that the Credentials interface would not depend on the builder interface, but any object implementing the receiver which is an adapter/transformer.

To make it a bit clearer, the plain credentials will reside in a simple properties file while the SSO credentials will require certificate-based authentication which will in turn trigger some SAML action finally issuing the username based on the client cert and along with a time-constraint token. But should not matter to the request builders at all.

WDYT?

Aucun commentaire:

Enregistrer un commentaire