Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Implement a Custom Alexa Skill Using Spring Cloud Microservices (Part 3)

DZone's Guide to

Implement a Custom Alexa Skill Using Spring Cloud Microservices (Part 3)

Dive into this series teaching you how to create and implement your own custom skills for Alex with the help of Spring Cloud and microservices.

· IoT Zone
Free Resource

Build an open IoT platform with Red Hat—keep it flexible with open source software.

Moving on from where we left off, in this section, we'll go over the technical details of the medical record management service we'll need for our custom skill. In particular, we will review important parts of the code. The complete code base can be downloaded from here.

Review of the Medical Record Management Service

The medical record management service is implemented using Spring Cloud. We will follow a pattern described in Microservices with Spring. The service will consist of three modules, registration, patient, and web.

Registration Module

The registration module functions as a service registry where services can register themselves and service clients can lookup services. It utilizes a Eureka discovery server. The only Java class in this module is com.demo.alexa.microservice.registration.RegistrationServer. Its main method starts and configures a Eureka server. The default host name and port of the server are defined in the configuration file, registration-server.yml, by the eureka.instance.hostname and server.port parameters, respectively. However, at startup user can override those by supplying <host name> and <port> arguments. 

@SpringBootApplication
@EnableEurekaServer
public class RegistrationServer {

    public static void main(String[] args) {
        if(args.length > 0 && args.length != 2){
            errorMessage();
            return;
        }else if(args.length == 2){
            System.setProperty("eureka.instance.hostname", args[0]);
            System.setProperty("server.port", args[1]);
        }

        System.setProperty("spring.config.name", "registration-server");
        SpringApplication.run(RegistrationServer.class, args);

    }

    protected static void errorMessage() {
        System.out.println("Usage: java -jar [jar file name] <host name> <port> OR");
        System.out.println("Usage: java -jar [jar file name]");
    }
}

Registration Module Configuration

The configuration file registration-server.yml is given below. Because registration service is not itself a client, it will not register with the Eureka server, hence eureka.client.registerWithEureka: false.

# Discovery server configuration
eureka:
  instance:
    hostname: localhost
  client: 
    registerWithEureka: false
    fetchRegistry: false

server:
  port: 1111   # HTTP (Tomcat) port

Patient Module

The role of the patient module is to encapsulate access to a patient database. It provides the core business functionality to query and update patient vitals. The individual instances of the patient module register themselves with the registration server. The patient module exposes its services as REST calls. The classes in that module all belong to com.demo.alexa.microservice.patient package. 

Patient Class

The Patient class encapsulates the patient table in database and represents a unique patient. It has id (unique database identifier), number (patient's medical record number), name (patient's name), pulse (patient's latest pulse measurement), temperature (patient's latest temperature measurement), sysp (patient's latest systolic pressure measurement) and diasp (patient's latest diastolic pressure measurement) columns.

@Entity
@Table(name = "PATIENT")
public class Patient implements Serializable {

    private static final long serialVersionUID = 1L;

    public static Long nextId = 0L;

    @Id
    protected Long id;

    @Column(name = "name")
    protected String name;

    @Column(name = "number")
    protected Integer number;

    @Column(name = "pulse")
    protected Integer pulse;

    @Column(name = "temperature")
    protected Integer temperature;

    @Column(name = "sysp")
    protected Integer sysp;

    @Column(name = "diasp")
    protected Integer diasp;

    ... // ignore the getter/setter methods

PatientConfiguration Class

PatientConfiguration class is responsible for Java-based Spring configuration for the patient module. For demonstration purposes, the sample application in this article uses an H2 database in embedded mode. PatientConfiguration creates a DataSource from H2 database via dataSource() method. Also notice the @EnableJpaRepositories annotation. This will trigger implementation of the repository interface to be discussed next. The schema.sql defines the database schema whereas data.sql is a script to populate the database with sample data. 

@Configuration
@ComponentScan
@EntityScan("com.demo.alexa.microservice.patient")
@EnableJpaRepositories("com.demo.alexa.microservice.patient")
@PropertySource("classpath:db-config.properties")
public class PatientConfiguration {
    protected Logger logger;

    public PatientConfiguration() {
        logger = Logger.getLogger(getClass().getName());
    }

    @Bean
    public DataSource dataSource() {
        logger.info("dataSource() invoked");

        DataSource dataSource = 
          (new EmbeddedDatabaseBuilder()).addScript("classpath:db/schema.sql")
                .addScript("classpath:db/data.sql").build();

        logger.info("dataSource = " + dataSource);

        // Log data
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        List<Map<String, Object>> patients = 
          jdbcTemplate.queryForList("SELECT name FROM PATIENT");
        logger.info("System has " + patients.size() + " patients");

        return dataSource;
    }
}

PatientRepository Class

PatientRepository is the Repository interface with various query and update methods. The concrete implementation will be provided by Spring, triggered by PatientConfiguration as mentioned above. 

public interface PatientRepository extends Repository<Patient, Long>{

    // Return a patient based on its medical record number
    public Patient findByNumber(Integer patientNumber);

    // Return a list of patients based on name matching
    public List<Patient> findByNameContainingIgnoreCase(String partialName);

    // Update a particular patient's vital measurements; patient is identified 
    // by its medical record number
    @Transactional
    @Modifying
    @Query("update Patient p set p.diasp = ?2, p.sysp = ?3, p.pulse = ?4, p.temperature = ?5 where p.number = ?1")
    public int setPatientVitals(Integer patientNumber, Integer diasp, Integer sysp, Integer pulse, Integer temperature);

    // Return #patients in database
    @Query("SELECT count(*) from Patient")
    public int countPatients();

    // Return the list of patients who have at least one of their vitals is in
    // abnormal range
    @Query("SELECT p from Patient p where DIASP > 80 or SYSP > 120 or PULSE < 60 or PULSE > 100 or TEMPERATURE < 96 or TEMPERATURE > 100")
    public List<Patient> getAbnormalVitals();
}

PatientController Class

The PatientController class provides a mapping between the service methods and the REST calls into the Patient service. In our sample application, all the service methods are provided by the Spring's repository implementation. (Note that those REST calls are used by the web module below, not the Patient Monitor Lambda function.)

@RestController
public class PatientController {
    protected Logger logger = Logger.getLogger(PatientController.class
            .getName());
    protected PatientRepository patientRepository;


    @Autowired
    public PatientController(PatientRepository patientRepository) {
        this.patientRepository = patientRepository;

        logger.info("PatientRepository says system has "
                + patientRepository.countPatients() + " patients");
    }

    /**
     * Return a patient from database identified by its medical record number.
     *
     * @param patientNumber: medical record number
     * @return Patient
     */
    @RequestMapping(value="/patient/bynumber/{patientNumber}", 
                    produces={"application/json"})
    public Patient byPatientNumber(
      @PathVariable("patientNumber") Integer patientNumber) {

        logger.info("patient-service byPatientID() invoked: " + patientNumber);
        Patient patient = patientRepository.findByNumber(patientNumber);
        logger.info("patient-service byPatientID() found: " + patient);

        if (patient == null){
            return Patient.noPatient();
        }
        else {
            return patient;
        }
    }

    /**
     * Record vital measurements for a patient identified by its medical 
     * record number.
     * Return the updated patient.
     *
     * @param patientNumber: medical record number
     * @param diasp: diastolic pressure to record
     * @param sysp: systolic pressure to record
     * @param pulse: pulse to record
     * @param temperature: temperature to record
     * @return Patient
     */
    @RequestMapping(value="/patient/setVitals/{patientNumber}/{diasp}/{sysp}/{pulse}/{temperature}", produces={"application/json"})
    public Patient setVitals(@PathVariable("patientNumber") Integer patientNumber,
        @PathVariable("diasp") String diasp,
        @PathVariable("sysp") String sysp,
        @PathVariable("pulse") String pulse,
        @PathVariable("temperature") String temperature){
        logger.info("patient-service setVitals() invoked: "
                + patientRepository.getClass().getName() + " for patientNumber = "
                + patientNumber + " diasp = " + diasp + " sysp = " + sysp + " pulse = " + pulse
                + " temperature = " + temperature);

        if(patientRepository.setPatientVitals(patientNumber, Integer.decode(diasp), Integer.decode(sysp),
                Integer.decode(pulse), Integer.decode(temperature)) == 1){
            return patientRepository.findByNumber(patientNumber);
        }
        else{
            return Patient.noPatient();
        }
    }

    /**
     * Return a list of patients based on partial name matching.
     *
     * @param partialName
     * @return List<Patient>
     */
    @RequestMapping(value="/patient/byname/{name}", produces={"application/json"})
    public List<Patient> byName(@PathVariable("name") String partialName) {
        logger.info("patient-service byName() invoked: "
                + patientRepository.getClass().getName() + " for "
                + partialName);

        List<Patient> patients = patientRepository.findByNameContainingIgnoreCase(partialName);

        logger.info("patient-service byName() found: " + patients);

        if (patients == null || patients.size() == 0)
            return Patient.noPatients();
        else {
            return patients;
        }
    }


    /**
     * Return the list of patients who have at least one of their vitals is in
     * abnormal range.
     *
     * @return List<Patient>
     */
    @RequestMapping(value="/patient/abnormal", produces={"application/json"})
    public List<Patient> getAbnormal() {
        logger.info("patient-service getAbnormal() invoked:"
                + patientRepository.getClass().getName());

        List<Patient> patients = patientRepository.getAbnormalVitals();

        logger.info("patient-service getAbnormal() found: " + patients);

        if (patients == null || patients.size() == 0)
            return Patient.noPatients();
        else {
            return patients;
        }
    }
}

PatientServer Class

PatientServer is the bootstrap class to start up the Patient module. The startup argument <registration service endpoint> must be the endpoint corresponding to the registration server. For example, if the registration server was started as:

java -jar registration-0.0.1-SNAPSHOT.jar 127.94.0.2 1112

Then the <registration service endpoint> must be http://127.94.0.2:1112/eureka/.

The <port> argument is the port the Tomcat server for this particular instance of the patient module will use upon startup. The <registration service endpoint> and <port> arguments correspond to the client.serviceUrl.defaultZone and server.port parameters, respectively, in the configuration file. If those arguments are not defined, the default values will take effect.

@EnableAutoConfiguration
@EnableDiscoveryClient
@Import(PatientConfiguration.class)
public class PatientServer {

    @Autowired
    protected PatientRepository patientRepository;

    protected Logger logger = Logger.getLogger(PatientServer.class.getName());

    public static void main(String[] args) {
        if(args.length > 0 && args.length != 2){
            errorMessage();
            return;
        }else if(args.length == 2){
            System.setProperty("eureka.client.serviceUrl.defaultZone", args[0]);
            System.setProperty("server.port", args[1]);
        }
        System.setProperty("spring.config.name", "patient-server");
        SpringApplication.run(PatientServer.class, args);
    }

    protected static void errorMessage() {
        System.out.println("Usage: java -jar [jar file name] <registration service endpoint> <port> OR");
        System.out.println("Usage: java -jar [jar file name]");
    }
}

Patient Module Configuration

The configuration file patient-server.yml is given below. Following the approach in Microservices with Spring, we set the client heartbeat frequency to 5 seconds, for development and unit testing purposes only. This is not recommended in a production environment because it may break the self-preservation mode of Eureka.

spring:
  application:
     name: patient-service  # Service registers under this name

# HTTP Server
server:
  port: 2222   # HTTP (Tomcat) port

# Discovery server endpoint
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:1111/eureka/
  instance:
    leaseRenewalIntervalInSeconds: 5   # demo and unit testing purposes only

Web Module

The web module encapsulates access to the patient service. In particular, it is used by the Patient Monitor Lambda function.

At startup, it obtains a list of patient service instances from the Eureka registry. Then, while accessing the patient service it uses a load-balanced HTTP client to choose an instance from that list. The web module exposes its services as REST calls. The classes in that module all belong to com.demo.alexa.microservice.web package.

Patient Class

Patient class is a simple POJO and represents a unique patient. It is reminiscent of the Patient entity class in patient module discussed previously. It has id (unique database identifier), number (patient's medical record number), name (patient's name), pulse (patient's latest pulse measurement), temperature (patient's latest temperature measurement), sysp (patient's latest systolic pressure measurement) and diasp (patient's latest diastolic pressure measurement) columns.

public class Patient implements Serializable {
    private static final long serialVersionUID = 1L;
    protected Long id;
    protected String name;
    protected Integer number;
    protected Integer pulse;
    protected Integer temperature;
    protected Integer sysp;
    protected Integer diasp;

    ... // getter and setters methods ignored

WebPatientService Class

WebPatientService class provides the core business service for the web module. It uses a Spring REST template with client side load balancing to access patient service. The underlying technology used by Spring while implementing the load-balanced client is Netflix Ribbon. (Also see Spring RestTemplate as a Load Balancer Client.) The serviceURL parameter in the constructor must be the URL under which the patient service has registered itself with Eureka registry. The business methods are implemented by calling the corresponding business method in the patient service. In that regard, the WebPatientService class simply encapsulates the access to the patient service.

The constructor below will be called by the WebServer class (to be reviewed soon). The WebServer class will also create the RestTemplate object that is injected by the Spring framework into the WebPatientService instance.

@Service
public class WebPatientService {
    @Autowired
    @LoadBalanced
    protected RestTemplate restTemplate;

    protected String serviceUrl;

    protected Logger logger = Logger.getLogger(WebPatientService.class
            .getName());


    /**
     * The serviceURL parameter corresponds to the patient service.
     * @param String serviceURL
     */
    public WebPatientService(String serviceURL) {
        this.serviceUrl = serviceURL.startsWith("http") ? serviceURL
                : "http://" + serviceURL;
    }

    /**
     * Return a Patient object corresponding to the medical record number
     * 
     * @param String patientNumber: Patient's medical record number
     * @return Patient
     */
    public Patient findByNumber(String patientNumber) {
        logger.info("findByNumber() invoked: for " + patientNumber);
        return restTemplate.getForObject(serviceUrl + 
                                        "/patient/bynumber/{patientNumber}",
                Patient.class, patientNumber);
    }

    /**
     * Return a list of patients based on partial name matching.
     * 
     * @param String name
     * @return List<Patient>
     */
    public List<Patient> findByNameContains(String name) {
        logger.info("findByNameContains() invoked:  for " + name);
        Patient[] patients = null;

        try {
            patients = restTemplate.getForObject(serviceUrl
                    + "/patient/byname/{name}", Patient[].class, name);
        } catch (HttpClientErrorException e) {
        }

        if (patients == null || patients.length == 0)
            return null;
        else
            return Arrays.asList(patients);
    }

    /**
     * Return the list of patients who have at least one of their vitals is 
     * in abnormal range.
     *
     * @return List<Patient>
     */
    public List<Patient> getAbnormal() {
        logger.info("getAbnormal() invoked");
        Patient[] patients = null;
        try {
            patients = restTemplate.getForObject(serviceUrl
                    + "/patient/abnormal", Patient[].class);
        } catch (HttpClientErrorException e) {
        }

        if (patients == null || patients.length == 0)
            return null;
        else
            return Arrays.asList(patients);
    }
    /**
     * Record vital measurements for a patient identified by its medical 
     * record number.
     * Return updated patient.
     *
     * @param patientNumber: medical record number
     * @param diasp: diastolic pressure to record
     * @param sysp: systolic pressure to record
     * @param pulse: pulse to record
     * @param temperature: temperature to record
     *
     * @return Patient
     */
    public Patient setVitals(Integer patientNumber, String diasp,
            String sysp, String pulse, String temperature){
        logger.info("setVitals() invoked");

        return restTemplate.getForObject(serviceUrl
                    + "/patient/setVitals/" + patientNumber + "/"
                    + diasp + "/"
                    + sysp + "/"
                    + pulse + "/"
                    + temperature, Patient.class);
    }
}

WebPatientController Class

The WebPatientController class provides a mapping between the service methods and the REST calls into the web service. The return value from each method is converted into a JSON formatted string. 

@RestController
public class WebPatientController {

    @Autowired
    protected WebPatientService patientService;

    protected Logger logger = Logger.getLogger(WebPatientController.class
            .getName());

    public WebPatientController(WebPatientService patientService) {
        this.patientService = patientService;
    }

    /**
     * Return a Patient object corresponding to the medical record number
     * 
     * @param String patientNumber: Patient's medical record number
     * @return Patient
     */
    @RequestMapping(value="/patient/bynumber/{patientNumber}", produces={"application/json"})
    public Patient byPNumber(@PathVariable("patientNumber") String patientNumber) {
        logger.info("web-service byPNumber() called by: " + patientNumber);
        Patient patient = patientService.findByNumber(patientNumber);
        logger.info("web-service byPNumber() found: " + patient);
        return patient;
    }

    /**
     * Return a list of patients based on partial name matching.
     * 
     * @param String partialName
     * @return List<Patient>
     */
    @RequestMapping(value="/patient/byname/{name}", 
                    produces={"application/json"})
    public List<Patient> byName(@PathVariable("name") String partialName) {
        logger.info("web-service byName() called by: " + partialName);
        List<Patient> patients = patientService.findByNameContains(partialName);
        logger.info("web-service byName() found: " + patients);
        return patients;

    }

    /**
     * Return the list of patients who have at least one of their vitals is in
     * abnormal range.
     *
     * @return List<Patient>
     */
    @RequestMapping(value="/patient/abnormal", produces={"application/json"})
    public List<Patient> abnormalVitals(){
        logger.info("web-service abnormalVitals() called");
        List<Patient> patients = patientService.getAbnormal();
        logger.info("web-service abnormalVitals() found: " + 
                    patients.size() + " patients");
        return patients;
    }

    /**
     * Record vital measurements for a patient identified by its medical 
     * record number.
     * Return updated patient.
     *
     * @param patientNumber: medical record number
     * @param diasp: diastolic pressure to record
     * @param sysp: systolic pressure to record
     * @param pulse: pulse to record
     * @param temperature: temperature to record
     *
     * @return Patient
     */
    @RequestMapping(value="/patient/setVitals/{patientNumber}/{diasp}/{sysp}/{pulse}/{temperature}", produces={"application/json"})
    public Patient setVitals(@PathVariable("patientNumber") Integer patientNumber,
            @PathVariable("diasp") String diasp,
            @PathVariable("sysp") String sysp,
            @PathVariable("pulse") String pulse,
            @PathVariable("temperature") String temperature){
        logger.info("web-service setVitals() called");
        return patientService.setVitals(patientNumber, diasp, sysp, pulse, 
                                        temperature);
    }
}

WebServer Class

WebServer is the bootstrap class to start up web module. The startup argument <registration service endpoint> must be the endpoint corresponding to registration server. For example, if the registration server was started as:

java -jar registration-0.0.1-SNAPSHOT.jar 127.94.0.2 1112

Then the <registration service endpoint> must be http://127.94.0.2:1112/eureka/.

The <port> argument is the port the Tomcat server for this particular instance of the web module will use upon startup. The <registration service endpoint> and <port> arguments correspond to the client.serviceUrl.defaultZone and server.port parameters, respectively, in the configuration file. If those arguments are not defined, default values will take effect.

The PATIENT_SERVICE_URL is the constant that corresponds to the name patient service used when registering itself. WebServer will use that name to create a new WebPatientService instance. It will also create the load balanced REST template that will be injected into WebPatientService.

@SpringBootApplication
@EnableDiscoveryClient
@ComponentScan(useDefaultFilters = false)
public class WebServer {

    public static final String PATIENT_SERVICE_URL = "http://patient-service";

    public static void main(String[] args) {
        if(args.length > 0 && args.length != 2){
            errorMessage();
            return;
        }else if(args.length == 2){
            System.setProperty("eureka.client.serviceUrl.defaultZone", args[0]);
            System.setProperty("server.port", args[1]);
        }
        System.setProperty("spring.config.name", "web-server");
        SpringApplication.run(WebServer.class, args);
    }

    @LoadBalanced
    @Bean
    RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @Bean
    public WebPatientService patientService() {
        return new WebPatientService(PATIENT_SERVICE_URL);
    }

    @Bean
    public WebPatientController patientController() {
        return new WebPatientController(patientService());
    }

    protected static void errorMessage() {
        System.out.println("Usage: java -jar [jar file name] <registration service endpoint> <port> OR");
        System.out.println("Usage: java -jar [jar file name]");
    }
}

Web Module Configuration

The configuration file is named web-server.yml and is listed below. The web module registers itself with the registry server under web-service name. The ribbon.eureka.enabled:  true tells Netflix Ribbon to query Eureka registry to obtain service endpoint addresses. The web-service.ribbon.NFLoadBalancer: com.netflix.loadbalancer.BaseLoadBalancer directs Ribbon to use a BaseLoadBalancer instance during client-side load balancing. 

spring:
  application:
    name: web-service  # service registers under this name

eureka:
  instance:
     leaseRenewalIntervalInSeconds: 5  # demo and unit testing purposes only
  client:
    serviceUrl:
      defaultZone: http://localhost:1111/eureka/

ribbon:
 eureka:
  enabled: true

web-service:
 ribbon:
  NFLoadBalancer: com.netflix.loadbalancer.BaseLoadBalancer

# HTTP Server
server:
  port: 3333   # HTTP (Tomcat) port

Conclusions

In this series, we discussed how to implement a custom skill for the Amazon Alexa virtual assistant. The custom skill provides a new, voice-driven user interface for an existing medical record management service.

A custom skill consists of an interaction model and its functional implementation. Amazon provides a web-based GUI to configure the interaction model where the user can supply the intents and associated sample utterances. Functional implementation of a custom skill can be done using a web service or an AWS Lambda function. In this article, we used a Java-based AWS Lambda function for implementing the custom skill. 

The Lambda function uses REST endpoints to access the medical record management service. Implementation of the medical record management service is done using Spring Cloud microservices.

Download Red Hat’s blueprint for building an open IoT platform—open source from cloud to gateways to devices.

Topics:
amazon echo ,iot ,spring cloud ,microservices

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}