24 KiB
24 KiB
List of contents
- #REST API with Spring
- #Flask API in Python
- #DOCKER
- #gRPC
REST API in Java with Spring
Making REST API Requests
With WebClient object:
(e.g Spring WebFlux)
GET
public void getAllPersons(CustomerSortingOptions sortingOptions, Consumer<List<Customer>> customerConsumer) {
webClient.get()
.uri(uriBuilder -> uriBuilder
.path("customers")
.queryParam("sortField", sortingOptions.getSortField())
.queryParam("sortingOrder", sortingOptions.getSortingOrder())
.build())
.retrieve()
.bodyToMono(new ParameterizedTypeReference<List<Customer>>() {
})
.onErrorStop()
.subscribe(newPersons -> {
customers.clear();
customers.addAll(newPersons);
customerConsumer.accept(customers);
});
}
POST
public void addPerson(Customer customer, Consumer<List<Customer>> customerConsumer) {
// TODO Part 2: Complete the http post request to the server
webClient.post()
.uri("customers")
.bodyValue(customer)
.retrieve()
.bodyToMono(Customer.class)
.onErrorStop()
.subscribe(newCustomer -> {
customers.add(newCustomer);
customerConsumer.accept(customers);
});
}
PUT
public void updatePerson(Customer customer, Consumer<List<Customer>> customerConsumer) {
// TODO Part 2: Complete the http put request to the server
webClient.put()
.uri("customers/" + customer.getId())
.bodyValue(customer)
.retrieve()
.bodyToMono(Customer.class)
.onErrorStop()
.subscribe(newCustomer -> {
customers.replaceAll(x -> x.getId().equals(customer.getId()) ? newCustomer : x);
customerConsumer.accept(customers);
});
}
DELETE
public void deletePerson(Customer customer, Consumer<List<Customer>> customerConsumer) {
// TODO Part 2: Make an http delete request to the server
webClient.delete()
.uri("customers/" + customer.getId())
.retrieve()
.toBodilessEntity()
.onErrorStop()
.subscribe(deletedCustomer -> {
customers.remove(customer);
customerConsumer.accept(customers);
});
}
Just in case: webClient Constructor
public CustomerController() {
this.webClient = WebClient.builder()
.baseUrl("http://localhost:8080/")
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
this.customers = new ArrayList<>();
}
With Request Parameters:
Endpoint:
@GetMapping("persons")
public ResponseEntity<List<Person>> getAllPersons(PersonSortingOptions sortingOptions) { // Don't use @RequestParams unless object is simple(for example String), for more complex objects Spring parses the object by itself
List<Person> persons = personService.getAllPersons(sortingOptions);
return ResponseEntity.ok(persons);
}
Request:
public void getAllPersons(PersonSortingOptions sortingOptions, Consumer<List<Person>> personsConsumer) {
this.webClient.get()
.uri(uriBuilder -> uriBuilder // Use uriBuilder to build the path
.path("persons")
.queryParam("sortingOrder",sortingOptions.getSortingOrder()) // The fields are passed separately
.queryParam("sortField",sortingOptions.getSortField()) // In the format: ("Name",value)
.build()
)
.retrieve()
.bodyToMono(new ParameterizedTypeReference<List<Person>>() {
})
.onErrorStop()
.subscribe(persons -> {
this.persons = persons;
personsConsumer.accept(this.persons);
});
}
With RestTemplate:
GetMapping("/getTimeLine/{id}")
public List<Tweet> getTimeLine(@PathVariable("id") int userID) {
headers.setContentType(MediaType.APPLICATION_JSON);
ResponseEntity<List<Tweet>> response = restTemplate.exchange(
getActivityURL + userID, // Set the uri HttpMethod.GET,
null, // requestEntity = null as there is no body,
new ParameterizedTypeReference<List<Tweet>>(){} // response body type );
return response.getBody();
}
REST API Endpoints
Example Java class:
@RestController
@RequestMapping("/activity")
public class ActivityController {
private Map<Integer, List<Tweet>> userActivityMap = new HashMap<>();
private Map<Integer, List<User>> userFollowedMap = new HashMap<>();
@GetMapping("/getActivity/{id}")
public List<Tweet> getActivity(@PathVariable("id") int userID) {
List<Tweet> userTweets = userActivityMap.get(userID);
if(userTweets == null) return new ArrayList<>();
return userTweets;
}
@PostMapping("/addActivity")
public void addActivity(@RequestBody Tweet tweet) {
userActivityMap.computeIfAbsent(tweet.getUser().getUserID(), x -> new LinkedList<>());
userActivityMap.get(tweet.getUser().getUserID()).add(tweet);
}
@PostMapping("/addFollower")
public void addFollower(@RequestBody FollowRequest followRequest) {
User follower = followRequest.getFollower();
User followed = followRequest.getFollowed();
userFollowedMap.computeIfAbsent(follower.getUserID(),x -> new LinkedList<>())
.add(followed);
}
@DeleteMapping("/deleteActivity")
public void deleteActivity(@RequestBody Tweet tweet) {
List<Tweet> userTweets = userActivityMap.get(tweet.getUser().getUserID());
if(userTweets == null) return;
userTweets.remove(tweet);
}
@DeleteMapping("/deleteFollower")
public void deleteFollower(@RequestBody FollowRequest followRequest) {
User follower = followRequest.getFollower();
User followed = followRequest.getFollowed();
List<User> followedUsers = userFollowedMap.get(follower.getUserID());
if(followedUsers == null) return;
followedUsers.remove(followed);
}
@GetMapping("/getFollowedList/{id}")
public List<User> getFollowedList(@PathVariable("id") int userID) {
// TODO: this method is also not complete, so you have to implement it accordingly :)
List<User> followed = userFollowedMap.get(userID);
if(followed == null) return new ArrayList<>();
return followed;
}
}
Excersises:
- L02PE01 TUM Social App
- L02PE02 REST Architectural Style Part 2
- L09PE02 Wizardry Character Creation
GET
@api.route("/<key>", methods=["GET"])
def get(key: str) -> Response:
"""
Retrieve the file-content associated with the given key owned by the user.
If there is no user-id `uid` given in the request body or the user-id does not match, return error message and status.
Otherwise, return the file-content and success status.
"""
request_body: dict = request.json
ff: FileFolder = current_app.ff # type: ignore
user_id = request_body.get("uid")
if user_id is None:
return Response(NO_UID_IN_REQUEST_BODY,BAD_REQUEST)
file = ff.get(key)
if file is None:
return Response(response=KEY_DOES_NOT_EXIST % key, status=NOT_FOUND)
if file.uid != user_id:
return Response(response=NO_ACCESS_TO_ENTRY % key, status=BAD_REQUEST)
return Response(response=file.content,status=OK)
pass
PUT
@api.route("/<key>", methods=["PUT"])
def put(key: str) -> Response:
"""
Upsert a file-content associated with the given key owned by the user.
If there is no user-id `uid` or file-content `content` given in the request body, return error message and status.
Otherwise, return the success message and success status
"""
request_body: dict = request.json
ff: FileFolder = current_app.ff
if "uid" not in request_body:
return Response(NO_UID_IN_REQUEST_BODY,BAD_REQUEST)
if "content" not in request_body:
return Response(NO_CONTENT_IN_REQUEST_BODY,BAD_REQUEST)
uid = request_body["uid"]
file_content = request_body["content"]
old_entry = ff.get(key)
if old_entry is None:
entry = Entry(uid,file_content)
success = ff.put(key,entry)
if success:
return Response(PUT_SUCCESS,OK)
else:
return Response(NO_SPACE_IN_FILE_FOLDER,BAD_REQUEST)
if uid != old_entry.uid:
return Response(NO_ACCESS_TO_ENTRY % key,status=BAD_REQUEST)
entry = Entry(uid,file_content)
ff.put(key,entry)
return Response(PUT_SUCCESS,OK)
pass
DELETE
@api.route("/<key>", methods=["DELETE"])
def remove(key: str) -> Response:
"""
Remove the file-content associated with the given key owned by the user.
If there is no user-id `uid` given in the request body or the user-id does not match, return error message and status.
Otherwise, return the removed file-content and success status
"""
request_body: dict = request.json
ff: FileFolder = current_app.ff
if "uid" not in request_body:
return Response(NO_UID_IN_REQUEST_BODY,BAD_REQUEST)
uid = request_body["uid"]
entry = ff.get(key)
if entry is None:
return Response(KEY_DOES_NOT_EXIST % key,NOT_FOUND)
if entry.uid != uid:
return Response(NO_ACCESS_TO_ENTRY % key,BAD_REQUEST)
ff.remove(key)
return Response(entry.content,OK)
pass
GET - all
@api.route("/", methods=["GET"])
def list() -> Response:
"""
List all (key, file-content) tuples owned by the user.
If there is no user-id `uid` given in the request body, return error message and status.
Otherwise, return the list of file-content and success status.
"""
request_body: dict = request.json
ff: FileFolder = current_app.ff
if "uid" not in request_body:
return Response(NO_UID_IN_REQUEST_BODY,BAD_REQUEST)
uid = request_body["uid"]
items = ff.items()
output = []
for key, entry in items:
output.append(f"{key}:{entry.content}")
output_string = ".".join(output)
return Response(output_string,OK)
pass
Making requests on the client side
Helper method
# make json request to server
def json_request(self, method: str, path: str, body: dict):
self.conn.request(method, path, json.dumps(body), headers={"Content-Type": "application/json"})
flask_response = self.conn.getresponse()
return flask_response.read().decode()
PUT
def put(self, key, content):
"""
Upsert (key, content) tuple into the file folder.
Return the server response
"""
request_body = {"uid": self.user_id, "content": content}
return self.json_request("PUT", f"/{key}",request_body)
pass
GET
def read(self, key):
"""
Retrieve the file-content associated with the given key owned by the user.
Return the server response
"""
request_body = {"uid": self.user_id}
return self.json_request("GET",f"/{key}",request_body)
pass
DELETE
def remove(self, key):
"""
Remove the file-content associated with the given key owned by the user.
Return the server response
"""
request_body = {"uid": self.user_id}
return self.json_request("DELETE",f"/{key}",request_body)
pass
GET - ALL
def list(self):
"""
List all file-content owned by the user.
Return the server response
"""
request_body = {"uid": self.user_id}
return self.json_request("GET","/",request_body)
pass
Excersises:
- L03PE01 Online FileFolder
Python - general syntax and tips:
Dictonaries:
thisdict = {
"brand": "Ford",
"model": "Mustang",
"year": 1964
}
print(thisdict["brand"])
thisdict = dict(name = "John", age = 36, country = "Norway")
print(thisdict)
You can check if a key exists with the keyword in:
if "model" in thisdict:
print("Yes, 'model' is one of the keys in the thisdict dictionary")
You can loop over keys and values in dictionary:
for x in thisdict: # Loop over keys
print(x)
for x in thisdict.keys(): #Another way to loop over keys
print(x)
for x in thisdict: # Loop over values
print(thisdict[x])
for x in thisdict.values(): # Another way to loop over values
print(x)
for x, y in thisdict.items(): # Looping over key-value pairs as tuples
print(x, y)
Dictionary methods
| Method | Description |
|---|---|
| clear() | Removes all the elements from the dictionary |
| copy() | Returns a copy of the dictionary |
| fromkeys() | Returns a dictionary with the specified keys and value |
| get() | Returns the value of the specified key |
| items() | Returns a list containing a tuple for each key value pair |
| keys() | Returns a list containing the dictionary's keys |
| pop() | Removes the element with the specified key |
| popitem() | Removes the last inserted key-value pair |
| setdefault() | Returns the value of the specified key. If the key does not exist: insert the key, with the specified value |
| update() | Updates the dictionary with the specified key-value pairs |
| values() | Returns a list of all the values in the dictionary |
DOCKER
Docker reference: https://docs.docker.com/
Dockerfiles
https://docs.docker.com/get-started/docker-concepts/building-images/writing-a-dockerfile/ https://docs.docker.com/reference/dockerfile/
FROM python:3.13
WORKDIR /usr/local/app
# Install the application dependencies
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Copy in the source code
COPY src ./src
EXPOSE 8080
# Setup an app user so the container doesn't run as the root user
RUN useradd app
USER app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
Building docker
docker build -t <tag>
Docker processes
docker ps
Exercises
gRPC
gRPC reference: https://grpc.io/docs/ gRPC Java basics: https://grpc.io/docs/languages/java/basics/
Protobuf files
Defining classes:
Important tips:
- Array in protobuf means
repeatedfield
message Date {
int32 day = 1; // Number specifies order
int32 month = 2;
int32 year = 3;
}
message Location {
string city = 1;
string country = 2;
}
message LocationDate {
Location location = 1;
Date date = 2;
}
Defining services:
service WeatherReporter {
//Returning one object
rpc GetCityWeatherSingleDay(LocationDate) returns (CityWeatherData) {}
// Returning a stream of objects
rpc GetCityWeatherMultipleDays(LocationDatePeriod) returns (stream CityWeatherData) {}
}
Example of services and objects with repeated fields
This will be utilized by some of the client side examples underneath
service MetadataService {
rpc getTrackMetadata(EntitiesRequest) returns (Tracks) {}
rpc getAlbumMetadata(EntitiesRequest) returns (Albums) {}
}
message EntitiesRequest {
repeated uint32 id = 1;
}
message Track {
uint64 id = 1;
string name = 2;
string album = 3;
string artist = 4;
uint32 duration = 5;
}
message Tracks {
repeated Track track = 1;
}
message Album {
uint64 id = 1;
string name = 2;
string artist = 3;
repeated uint32 tracks = 4;
}
message Albums {
repeated Album album = 1;
}
Generating classes from .proto files
- Gradle tab in IDE and press "Tasks > other > generateProto")
- The generated classes from the proto file should be in build/generated/source/proto/main folder
Server side
In practice can be done with streams or with simple iteration
public void getCityWeatherSingleDay(LocationDate request, StreamObserver<CityWeatherData> responseObserver) {
var data = allWeatherData.stream().filter(cityWeatherData -> cityWeatherData.getLocationDate().equals(request)).findFirst().orElse(null);
if(data != null)responseObserver.onNext(data);
responseObserver.onCompleted();
}
For returning a stream of data just call onNext multiple times
With iteration:
public void getCityWeatherMultipleDays(LocationDatePeriod request, StreamObserver<CityWeatherData> responseObserver) {
for(CityWeatherData data : allWeatherData) {
if(data.getLocationDate().getLocation().equals(request.getLocation())) {
if(compareDate(data.getLocationDate().getDate(), request.getStartDate()) >= 0 && compareDate(data.getLocationDate().getDate(), request.getEndDate()) <= 0)
{
responseObserver.onNext(data);
}
}
}
responseObserver.onCompleted();
}
With streams:
public void getCityWeatherMultipleDays(LocationDatePeriod request, StreamObserver<CityWeatherData> responseObserver) {
var startDate = request.getStartDate();
var endDate = request.getEndDate();
allWeatherData.stream()
.filter(cityWeatherData -> cityWeatherData.getLocationDate().getLocation().equals(request.getLocation()))
.filter(cityWeatherData -> compareDate(cityWeatherData.getLocationDate().getDate(),startDate) >= 0)
.filter(cityWeatherData -> compareDate(cityWeatherData.getLocationDate().getDate(),endDate) <= 0)
.forEach(responseObserver::onNext);
responseObserver.onCompleted();
}
For returning an object which contains array of other objects (stupid???)
public void getAlbumMetadata(EntitiesRequest request, StreamObserver<Albums> responseObserver) {
Albums.Builder albumsBuilder = Albums.newBuilder();
for(int i = 0; i < request.getIdCount();i++) {
// Find the album by its id
Album album = database.findAlbumById(request.getId(i));
if(album != null) {
albumsBuilder.addAlbum(album);
}
}
// In case that an album isn't found builder builds empty list
Albums albums = albumsBuilder.build();
responseObserver.onNext(albums);
responseObserver.onCompleted();
}
If you want to return an error
public void getTrafficData(RouteRequest request, StreamObserver<TrafficResponse> responseObserver) {
var trafficModel = db.getTrafficModel(request);
var response = trafficModel.query(request);
if(response.isPresent()) {
responseObserver.onNext(response.get());
responseObserver.onCompleted();
} else {
// Just pass an exception with the onError method
responseObserver.onError(new Exception("No data available."));
}
}
Client Side
Setup
The blocking stub is a placeholder on which functions are called. It needs to be received from the gRPC object in some way.
public WeatherClient(Channel channel) {
// Get blocking stub from the gRPC object
blockingStub = WeatherReporterGrpc.newBlockingStub(channel);
}
or
public SpotifyClient(Channel channel) {
// spotifyConnection is a wrapper defined in the excercise
// this is just an example that there can be other ways to get the stub
this.spotifyConnection = new SpotifyConnection(channel);
this.blockingStub = spotifyConnection.getStub();
}
Calling method with single object return value
public void getCityWeatherSingleDay(int day, int month, int year, String city, String country) {
// Need to use builders to create the objects for the request
Date date = Date.newBuilder().setDay(day).setMonth(month).setYear(year).build();
Location location = Location.newBuilder().setCity(city).setCountry(country).build();
// Building the locationDate which will be passed as a parameter
LocationDate locationDate = LocationDate.newBuilder().setDate(date).setLocation(location).build();
// Getting the returned object from calling the method on the stub
CityWeatherData cityWeatherData = blockingStub.getCityWeatherSingleDay(locationDate);
System.out.println(cityWeatherData.toString());
}
Calling method which returns stream
public void getCityWeatherMultipleDays(int startDay, int startMonth, int startYear, int endDay, int endMonth, int endYear, String city, String country) {
// Building objects for the request
Date startDate = Date.newBuilder()
.setDay(startDay)
.setMonth(startMonth)
.setYear(startYear)
.build();
Date endDate = Date.newBuilder()
.setDay(endDay)
.setMonth(endMonth)
.setYear(endYear)
.build();
Location location = Location.newBuilder().setCity(city).setCountry(country).build();
// Building LocationDatePeriod object to be passed as parameter
LocationDatePeriod locationDatePeriod = LocationDatePeriod.newBuilder()
.setStartDate(startDate)
.setEndDate(endDate)
.setLocation(location)
.build();
// Method call returns Iterator<CityWeatherData> for the stream of CityWeatherData
Iterator<CityWeatherData> iterator = blockingStub.getCityWeatherMultipleDays(locationDatePeriod);
// Iterating over the objects
while (iterator.hasNext()) {
CityWeatherData cityWeatherData = iterator.next();
System.out.println(cityWeatherData.toString());
}
}
Calling method which has parameter with repeated field
public String displayPlaylist(int[] playlist) throws SpotifyClientException {
StringBuilder stringBuilder = new StringBuilder();
EntitiesRequest.Builder builder = EntitiesRequest.newBuilder();
// IMPORTANT for REPEATED fields
// Add all id's to request
for(int id : playlist) {
builder.addId(id);
}
EntitiesRequest request = builder.build();
// Unlike in earlier examplese returns OBJECT which contains multiple Tracks
Tracks tracks;
tracks = blockingStub.getTrackMetadata(request);
if(tracks.getTrackCount() != playlist.length) throw new SpotifyClientException("Failed to get track metadata");
for(int i = 0; i < tracks.getTrackCount();i++) {
Track track = tracks.getTrack(i);
int durationInSeconds = track.getDuration();
int durationInMinutes = durationInSeconds / 60;
durationInSeconds = durationInSeconds % 60;
stringBuilder.append(String.format("%d. %s - %s (%d:%02d)%n", i+1,track.getName(), track.getArtist(), durationInMinutes, durationInSeconds));
}
return stringBuilder.toString();
}
Excersises:
- L02PB01 Weather Reporter
- L02P01 Spotify gRPC
- L03PE03 Gobblemaps
ALL RESOURCE LINKS:
- Lecture slides (PDF)
- Your own homework solutions (cloned)
- Cheat-sheets/self-written notes (printed/on paper/PDF)
- C reference: https://en.cppreference.com/w/c/language
- Python reference: https://docs.python.org/3/reference/index.html
- Java reference: https://www.baeldung.com/get-started-with-java-series
- Java reference: https://docs.oracle.com/en/java/javase/22/
- gRPC reference: https://grpc.io/docs/
- Spotbugs: https://spotbugs.readthedocs.io
- AddressSanitizer: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html
- Spring Boot: https://www.baeldung.com/spring-boot
- Spring Boot: https://spring.io/projects/spring-boot
- Docker: https://docs.docker.com/
- JavaFX: https://openjfx.io/openjfx-docs
- Gradle: https://docs.gradle.org/current/userguide/userguide.html
- Git: https://git-scm.com/docs
- JUnit: https://junit.org/junit5/docs/current/user-guide/
- Mockito: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html
- EasyMock: https://easymock.org/user-guide.html
- man pages (command line, not online)