Classes Background

All concepts in Classes FRQs can be used with POJOs in PBL. The 2019 FRQ #2 describes a problem of a Step Tracker. The task is to write a StepTracker class. Included is the constructor and any required instance variables and methods.

  • Constructor:StepTracker tr = new StepTracker(10000);- Get Methods with algorithms: tr.activeDays(); tr.averageSteps();
  • Set Methods: tr.addDailySteps(9000);

Assumptions to PBL this Project

This is a fun problem and could be expanded upon with PBL. The StepTracker could be a POJO in Java. As a PBL concept, StepTracking is something that is done by an individual in relation to their personal fitness. What designs or objects would you need for StepTracking...

  • Backend Assumptions:- This problem has one-to-many relationship between "Person" object to day "steps" attributes. - Each day "steps" attribute would need to be stored.

    • Active days implies a "Person" object needs a "goal_steps" attribute to be stored to compare against the "steps" attribute for a day.
    • Person Class. Contact and biographical information, like age/height/weight could be important to this application. Such information could be used to help calculate goals for "steps".
    • Day statistics in Class could go beyond "steps". Though "steps" is key element. Additional attributes could include walking or running time, calorie intake, sleep hours, mood ratio, or other factors that could help in determining goal versus accomplishments.
    • Other relationships. A daily collection of "stats" implies calculations for Month or Year. Investigation should be made to determine best Java Modeling and Algorithms available.
  • Frontend Assumptions:

    • A person's steps need to be recorded by a person in the Frontend
    • A person would need to view or perform analytics on self, perhaps a daily step chart.
    • A person might want to know about how they met or missed goals versus food calorie intake, hours slept, mood ratio.

Starting the Project

To study Classes, which is the intent of FRQ2, A PBL project step tracking seems to begin with a "Person" Class. This may deviate from FRQ 2 simplicity, but it is a higher-level way to approach this problem.

Person POJO

This POJO is setup as an @Entity and use Lambok extensively to reduce code required by Developer(s). This POJO can be used for many Projects!!!

package com.nighthawk.spring_portfolio.mvc.person;

import java.time.LocalDate;
import java.time.Period;
import java.time.ZoneId;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;

import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.springframework.format.annotation.DateTimeFormat;

import com.vladmihalcea.hibernate.type.json.JsonType;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;

/*
Person is a POJO, Plain Old Java Object.
First set of annotations add functionality to POJO
--- @Setter @Getter @ToString @NoArgsConstructor @RequiredArgsConstructor
The last annotation connect to database
--- @Entity
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@TypeDef(name="json", typeClass = JsonType.class)
public class Person {
    
    // automatic unique identifier for Person record
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    // email, password, roles are key attributes to login and authentication
    @NotEmpty
    @Size(min=5)
    @Column(unique=true)
    @Email
    private String email;

    @NotEmpty
    private String password;

    // @NonNull, etc placed in params of constructor: "@NonNull @Size(min = 2, max = 30, message = "Name (2 to 30 chars)") String name"
    @NonNull
    @Size(min = 2, max = 30, message = "Name (2 to 30 chars)")
    private String name;

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dob;
    

    /* HashMap is used to store JSON for daily "stats"
    "stats": {
        "2022-11-13": {
            "calories": 2200,
            "steps": 8000
        }
    }
    */
    @Type(type="json")
    @Column(columnDefinition = "jsonb")
    private Map<String,Map<String, Object>> stats = new HashMap<>(); 
    

    // Constructor used when building object from an API
    public Person(String email, String password, String name, Date dob) {
        this.email = email;
        this.password = password;
        this.name = name;
        this.dob = dob;
    }

    // A custom getter to return age from dob attribute
    public int getAge() {
        if (this.dob != null) {
            LocalDate birthDay = this.dob.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
            return Period.between(birthDay, LocalDate.now()).getYears(); }
        return -1;
    }

}

Next Steps

Building the Spring Java Persistent API (JPA) and the RestController API will get this or any other project off of the ground. The JPA works with the database and RestController enables access by the Frontend developers.

Spring Data JPA

The appeal of Java for me and the Computer Science industry is the ease of database management. Being able to define a POJO and get all the database management under the hood is very cool!

  • BTW, there is a task to change to AWS in the future, this can be done by changing application properties and and POM dependencies, with little or no change to code.
  • Review code, there are Long interfaces and native SQL options in extending interfaces into the JPA Repository.
package com.nighthawk.spring_portfolio.mvc.person;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

/*
Extends the JpaRepository interface from Spring Data JPA.
-- Java Persistent API (JPA) - Hibernate: map, store, update and retrieve database
-- JpaRepository defines standard CRUD methods
-- Via JPA the developer can retrieve database from relational databases to Java objects and vice versa.
 */
public interface PersonJpaRepository extends JpaRepository<Person, Long> {
    Person findByEmail(String email);

    List<Person> findAllByOrderByNameAsc();

    // JPA query, findBy does JPA magic with "Name", "Containing", "Or", "Email", "IgnoreCase"
    List<Person> findByNameContainingIgnoreCaseOrEmailContainingIgnoreCase(String name, String email);
    /* Custom JPA query articles, there are articles that show custom SQL as well
       https://springframework.guru/spring-data-jpa-query/
       https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods
     */

    // Custom JPA query
    @Query(
            value = "SELECT * FROM Person p WHERE p.name LIKE ?1 or p.email LIKE ?1",
            nativeQuery = true)
    List<Person> findByLikeTermNative(String term);
    /*
        https://www.baeldung.com/spring-data-jpa-query
     */
}

Sprint RestController

This is the interface between Backend implementation and the world. Get, Post, Delete mappings are shown. In this code, you will see many interactions with the POJO as well as interactions with the JPA.

package com.nighthawk.spring_portfolio.mvc.person;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.*;
import java.text.SimpleDateFormat;

@RestController
@RequestMapping("/api/person")
public class PersonApiController {
    /*
    #### RESTful API ####
    Resource: https://spring.io/guides/gs/rest-service/
    */

    // Autowired enables Control to connect POJO Object through JPA
    @Autowired
    private PersonJpaRepository repository;

    /*
    GET List of People
     */
    @GetMapping("/")
    public ResponseEntity<List<Person>> getPeople() {
        return new ResponseEntity<>( repository.findAllByOrderByNameAsc(), HttpStatus.OK);
    }

    /*
    GET individual Person using ID
     */
    @GetMapping("/{id}")
    public ResponseEntity<Person> getPerson(@PathVariable long id) {
        Optional<Person> optional = repository.findById(id);
        if (optional.isPresent()) {  // Good ID
            Person person = optional.get();  // value from findByID
            return new ResponseEntity<>(person, HttpStatus.OK);  // OK HTTP response: status code, headers, and body
        }
        // Bad ID
        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);       
    }

    /*
    DELETE individual Person using ID
     */
    @DeleteMapping("/delete/{id}")
    public ResponseEntity<Person> deletePerson(@PathVariable long id) {
        Optional<Person> optional = repository.findById(id);
        if (optional.isPresent()) {  // Good ID
            Person person = optional.get();  // value from findByID
            repository.deleteById(id);  // value from findByID
            return new ResponseEntity<>(person, HttpStatus.OK);  // OK HTTP response: status code, headers, and body
        }
        // Bad ID
        return new ResponseEntity<>(HttpStatus.BAD_REQUEST); 
    }

    /*
    POST Aa record by Requesting Parameters from URI
     */
    @PostMapping( "/post")
    public ResponseEntity<Object> postPerson(@RequestParam("email") String email,
                                             @RequestParam("password") String password,
                                             @RequestParam("name") String name,
                                             @RequestParam("dob") String dobString) {
        Date dob;
        try {
            dob = new SimpleDateFormat("MM-dd-yyyy").parse(dobString);
        } catch (Exception e) {
            return new ResponseEntity<>(dobString +" error; try MM-dd-yyyy", HttpStatus.BAD_REQUEST);
        }
        // A person object WITHOUT ID will create a new record with default roles as student
        Person person = new Person(email, password, name, dob);
        repository.save(person);
        return new ResponseEntity<>(email +" is created successfully", HttpStatus.CREATED);
    }

    /*
    The personSearch API looks across database for partial match to term (k,v) passed by RequestEntity body
     */
    @PostMapping(value = "/search", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Object> personSearch(@RequestBody final Map<String,String> map) {
        // extract term from RequestEntity
        String term = (String) map.get("term");

        // JPA query to filter on term
        List<Person> list = repository.findByNameContainingIgnoreCaseOrEmailContainingIgnoreCase(term, term);

        // return resulting list and status, error checking should be added
        return new ResponseEntity<>(list, HttpStatus.OK);
    }

    /*
    The personStats API adds stats by Date to Person table 
    */
    @PostMapping(value = "/setStats", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Person> personStats(@RequestBody final Map<String,Object> stat_map) {
        // find ID
        long id=Long.parseLong((String)stat_map.get("id"));  
        Optional<Person> optional = repository.findById((id));
        if (optional.isPresent()) {  // Good ID
            Person person = optional.get();  // value from findByID

            // Extract Attributes from JSON
            Map<String, Object> attributeMap = new HashMap<>();
            for (Map.Entry<String,Object> entry : stat_map.entrySet())  {
                // Add all attribute other thaN "date" to the "attribute_map"
                if (!entry.getKey().equals("date") && !entry.getKey().equals("id"))
                    attributeMap.put(entry.getKey(), entry.getValue());
            }

            // Set Date and Attributes to SQL HashMap
            Map<String, Map<String, Object>> date_map = new HashMap<>();
            date_map.put( (String) stat_map.get("date"), attributeMap );
            person.setStats(date_map);  // BUG, needs to be customized to replace if existing or append if new
            repository.save(person);  // conclude by writing the stats updates

            // return Person with update Stats
            return new ResponseEntity<>(person, HttpStatus.OK);
        }
        // return Bad ID
        return new ResponseEntity<>(HttpStatus.BAD_REQUEST); 
        
    }

}

Test RESTController

Recommend testing RESTController with Curl or Postman.

  • Curl is command line oriented (not shown) but can be used for automation. For instance, you could use it with Bash to load test data.
  • PostMan is more visual or "browser like". Test outputs will show Postman visuals and discuss highlight different access methods. These methods are implementations available in RestController.

Adding record using Params and POST. add

Listing records using GET. list

Searching records using Body raw and JSON using POST. This is preferred method to secure transmission search

Delete record using DELETE. delete

Setting stats using Body raw and JSON using POST. This again is preferred method to secure transmission stats

Collection, List, ArrayList, HashMap

Using Collection, List, and ArrayList enables a POJO to reference other POJOs. Often in Java you will see these different terms, here is a brief rundown...

  • A Collection is an interface that defines the highest-level of shared collection behavior, and extends Iterable which allows usage of ForEach loop.
  • A List is an interface that defines the highest-level of shared List behavior. The type List has in its contract that all elements have an order, that the order will not change except through intended manipulation. Key interfaces:A List allows get() the n'th element; A List defines add() to put new elements at the end. - ArrayList is an implementation of List. ArrayList should not be used in an API, only for implementation details.

HashMap

HashMap may be more suited to having days in the same POJO.

  • HashMap requires keys in key/value relationship to be unique. Data by date is formed in unique Key/Value pairs.

Hacks

Start with Person POJO. See that HashMap is built in.

  • Build tester method (public static void main) for Person Pojo. Test zero argument and all argument constructor. build toString method to display attributes of the class.
  • Build other attributes into POJO that where mentioned in project dialog at the beginning of this blog. Add 1 or more APIs. Use Issues to plan and track work.
  • Correct the BUG in setStats! Currently, it won't let you do more than one date.
"stats":{        "2022-11-13": {
                "calories": 2200,
                "steps": 8000
        }

SQL (Extras)

Start thinking about building other data into POJOs. Use your own example or alter it for a different application.

  • PBL/SQL Relationships. In POJOs you can define a relationship in SQL, the POJO Person and a POJO Day can be built to contain relationships (alternate to HashMap style). List is the interface and ArrayList is the implementation. It is possible to nest Lists in Lists for 2D. Read this article for Background.
/*
"1 to Many" and "Many to 1"
- 1 Person to Many Day(s) Relationship (Forward)
- Many Days to 1 Person Relationship (Reverse)
- symbol "-|" means the Day to Person must contain a result
- symbol "O<-" means there are zero to many Day(s) were data is collected
-----------              -----------
| Person  | -|-----O<- | Day     |  
-----------              -----------
*/

// Relationship shown, data is incomplete
@Entity
public class Person {
    // Person data

    // relationship
    @OneToMany(
        cascade = CascadeType.ALL,
        orphanRemoval = true
    )
    private List<Day> days = new ArrayList<>();
}

// Relationship shown, data is incomplete
@Entity
public class Day {
    // attributes, how would you track days.  perhaps capture date and have many method to calculate this data
    private int year;
    private int day;
    private int steps;
 
    // relationship
    @ManyToOne(fetch = FetchType.LAZY)
    private Person person;
}