lunedì 5 giugno 2017

Ping Applicativo con REST

Oggi voglio scrivere su alcune funzionalità presenti in Java 8: la Duration per indicare un intervallo di tempo e un nuovo client standard per chiamare un web service di tipo REST.

Si tratta di un servizio di Ping applicativo, cioè il client chiama il server un certo numero di volte e ne misura il tempo di risposta, poi fa una media del tempo di tutte le chiamate.
Il client si chiama a riga di comando indicando come parametri il numero di ping, l'indirizzo e volendo vedere la risposta completa, si può specificare il comando -showResponse

Per esempio un client potrebbe avere i seguenti parametri dopo il nome del file .jar: -c4 http://localhost:8080/MiaApp/rs/ping/ -showResponse

Veniamo ora ai dettagli applicativi.
Per provare ho usato come server wildfly in versione 10.1.0

Il codice più rilevante del server è questa classe, che risponde alle chiamate del client:

@Path("/ping")
public class ApplicativePingService {
@Inject
Logger logger;

private static final String VERSION = "0.0.1ALPHA";

@GET
@Produces("application/json")
public Response ping() {
  final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
  String hostName = null;
  try {
    hostName = InetAddress.getLocalHost().getHostName();
    final Pong pong = new Pong(VERSION, hostName,
    now.format(DateTimeFormatter.ISO_ZONED_DATE_TIME), true); // TODO chiama il db
    return Response.ok(pong, MediaType.APPLICATION_JSON).build();
  }
  catch (UnknownHostException ex) {
    logger.log(Level.SEVERE, "I cannot find the host name in ApplicativePingService.ping()", ex);
    return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
  }
}


Questa è la classe "Pong" che contiene i dati inviati dal server al client:

public class Pong {
  private String version;
  private String hostName;
  private String dateTime;
  private boolean databaseReady;

  public Pong() {
  }

  public Pong(final String version, final String hostName, final String dateTime, final boolean databaseReady) {

    this.version = version;
    this.hostName = hostName;
    this.dateTime = dateTime;
    this.databaseReady = databaseReady;
  }

  public void setVersion(String version) {
    this.version = version;
  }

  public void setHostName(String hostName) {
    this.hostName = hostName;
  }

  public void setDateTime(String dateTime) {
   this.dateTime = dateTime;
  }

  public String getVersion() {
    return version;
  }

  public String getHostName() {
    return hostName;
  }

  public String getDateTime() {
    return dateTime;
  }

  public boolean isDatabaseReady() {
    return databaseReady;
  }
}


Per quanto riguarda il client, voglio usare la nuova interfaccia standard per i client REST di Java Enterprise sfruttando le librerie fornite già da Wildfly, nel pom.xml le dipendenze da chiamare sono:


<dependency>
  <groupid>javax</groupid>
  <artifactid>javaee-api</artifactid>
  <version>6.0</version>
  <type>jar</type>
  <scope>provided</scope>
</dependency>
<dependency>
  <groupid>javax</groupid>
  <artifactid>javaee-web-api</artifactid>
  <version>7.0</version>
  <scope>provided</scope>
</dependency>
<dependency>
  <groupid>javax.enterprise</groupid>
  <artifactid>cdi-api</artifactid>
  <version>1.2</version>
  <scope>provided</scope>
</dependency>


Ora veniamo al codice del client più importante: un metodo che fa n chiamate a seconda del parametro passato:
Per mappare i dati JSon che arrivano dal server, usiamo la libreria jackson

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;



public List ping() {
  final List result = new ArrayList<>(this.pingCount);
  final ObjectMapper objectMapper = new ObjectMapper();
  Client client = ClientBuilder.newClient();
  try {
    for (int i = 0; i < pingCount; i++) {

      final LocalDateTime before = LocalDateTime.now(); 
      boolean ok = true; 
      Pong pong = null;
      String message = null;
      boolean databaseReady = false; 
      objectMapper.registerModule(new JavaTimeModule());
      WebTarget resource = client.target(address); 
      Response response = resource.request(MediaType.APPLICATION_JSON).get();
      ok = (response.getStatus() == Response.Status.OK.getStatusCode()); 
      if (ok) {
        message = response.readEntity(String.class); 
        ObjectMapper mapper = new ObjectMapper();
        try { 
          pong = mapper.readValue(message, new TypeReference() { });
          ok = (pong != null); 

          if (ok)
            databaseReady = pong.isDatabaseReady();
        } catch (JsonParseException | 
JsonMappingException | IOException e)  {
          ok = false
          e.printStackTrace();
        }
      }

      final LocalDateTime after = LocalDateTime.now();
      final Duration deltaTime = Duration.between(before, after);
      result.add(new PingResult(ok, deltaTime, pong, message, databaseReady));
    }
  } finally {
    if (client != null)
      client.close();
  }
    return result;
}


Questo codice chiama il metodo sopra per chiamare il server n volte, poi formatta la durata in minuti, secondi e millisecondi (forse solo il Java 9 vedremo dei metodi comodi per estrarre le parti da una Duration, perché i metodi attuali effettuano una conversione in una specifica unità di misura, così è veramente scomoda). Inoltre calcola la durata media di una chiamata.

Per mappare i dati JSon che arrivano dal server, usiamo la libreria jackson

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;



enum CommandLineState { UNDETERMINATE, PING_NUM, ADDRESS };

public static void main(String[] args) {
  int pingRepetition=4;
  String serverAddress = null;
  boolean showResponse = false;
  CommandLineState actualState = CommandLineState.UNDETERMINATE;

  for (String argument : args) {
    switch (actualState) {
    case UNDETERMINATE:
      if (argument.toLowerCase().startsWith("-c")) {
        if (argument.length() > 2)
          pingRepetition = Integer.parseInt(argument.substring(2));
        else
          actualState = CommandLineState.PING_NUM;
      }
      else {
        if ("-showResponse".equals(argument)) {
          showResponse = true;
        else
          serverAddress = argument;


        actualState = CommandLineState.UNDETERMINATE;
        }

        break;
      case PING_NUM:
        pingRepetition = Integer.parseInt(argument.substring(2));
        actualState = CommandLineState.UNDETERMINATE;
        break;
      case ADDRESS:
        serverAddress = argument;
        actualState = CommandLineState.UNDETERMINATE;
        break;
      }
  }

  if ((serverAddress == null) || serverAddress.isEmpty()) {
    System.err.println("Indirizzo del server mancante");
    System.exit(-1);
  }
  if (pingRepetition < 1) {

    System.err.println("Il numero di ripetizioni deve essere maggiore di 0");           System.exit(-2);
  }

  final AppPing pinger = new AppPing(serverAddress, pingRepetition); List result =   pinger.ping();
  if (result != null) {
    // Le prossime 3 righe possono provenire da un file di risorse
    final String outputStr = "%s versione %s ha risposto %s database %s in tempo: %s";
    final String outputAvg= "La durata media è : %s";
    final String rawOutputMsg = "La risposta completa del server è: ";
    final String OK = "OK";
    final String NOT_OK = "NON OK";
    Duration totalDuration = Duration.ZERO;
    for (PingResult pingResult : result) {
      System.out.println();

      final Pong pong = pingResult.getPong();

      String pingStatus = pingResult.isOk() ? OK : NOT_OK;
      String dataBaseStatus = pingResult.isDatabaseReady() ? OK : NOT_OK;

      Duration duration = pingResult.getDuration();
      totalDuration = totalDuration.plus(duration);
      String durationStr = LocalTime.MIDNIGHT.plus(duration).format(DateTimeFormatter.ofPattern("mm:ss:SSS"));

      System.out.format(outputStr, pong.getHostName(), pong.getVersion(),      pingStatus, dataBaseStatus, durationStr);

      if (showResponse) {
        System.out.println();
        System.out.println(rawOutputMsg);
        System.out.println(pingResult.getMessage());
      }
    }

    System.out.println();
    final Duration averageDuration = totalDuration.dividedBy(pingRepetition);
    final String avgResponse =     LocalTime.MIDNIGHT.plus(averageDuration).format(DateTimeFormatter.ofPattern("mm:ss:SSS"));
    System.out.format(outputAvg, avgResponse);
    System.out.println();
  }
}