# List of contents 1. [[#REST API with Spring]] 2. [[#Flask API in Python]] 3. [[#DOCKER]] 4. [[#gRPC]] 5.
# REST API in Java with Spring ## Making REST API Requests ### With ***WebClient*** object: (e.g Spring WebFlux) #### GET ```java public void getAllPersons(CustomerSortingOptions sortingOptions, Consumer> customerConsumer) { webClient.get() .uri(uriBuilder -> uriBuilder .path("customers") .queryParam("sortField", sortingOptions.getSortField()) .queryParam("sortingOrder", sortingOptions.getSortingOrder()) .build()) .retrieve() .bodyToMono(new ParameterizedTypeReference>() { }) .onErrorStop() .subscribe(newPersons -> { customers.clear(); customers.addAll(newPersons); customerConsumer.accept(customers); }); } ``` #### POST ```java public void addPerson(Customer customer, Consumer> 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 ```java public void updatePerson(Customer customer, Consumer> 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 ```java public void deletePerson(Customer customer, Consumer> 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 ```java 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: ```java @GetMapping("persons") public ResponseEntity> 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 persons = personService.getAllPersons(sortingOptions); return ResponseEntity.ok(persons); } ``` Request: ```java public void getAllPersons(PersonSortingOptions sortingOptions, Consumer> 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>() { }) .onErrorStop() .subscribe(persons -> { this.persons = persons; personsConsumer.accept(this.persons); }); } ``` ### With *RestTemplate*: ```java GetMapping("/getTimeLine/{id}") public List getTimeLine(@PathVariable("id") int userID) { headers.setContentType(MediaType.APPLICATION_JSON); ResponseEntity> response = restTemplate.exchange( getActivityURL + userID, // Set the uri HttpMethod.GET, null, // requestEntity = null as there is no body, new ParameterizedTypeReference>(){} // response body type ); return response.getBody(); } ``` ## REST API Endpoints Example Java class: ```java @RestController @RequestMapping("/activity") public class ActivityController { private Map> userActivityMap = new HashMap<>(); private Map> userFollowedMap = new HashMap<>(); @GetMapping("/getActivity/{id}") public List getActivity(@PathVariable("id") int userID) { List 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 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 followedUsers = userFollowedMap.get(follower.getUserID()); if(followedUsers == null) return; followedUsers.remove(followed); } @GetMapping("/getFollowedList/{id}") public List getFollowedList(@PathVariable("id") int userID) { // TODO: this method is also not complete, so you have to implement it accordingly :) List followed = userFollowedMap.get(userID); if(followed == null) return new ArrayList<>(); return followed; } } ```
# Flask API in Python ## *Server side* endpoints #### GET ```python @api.route("/", 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 ```python @api.route("/", 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 ```python @api.route("/", 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 ```python @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 ```python # 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 ```python 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 ```python 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 ```python 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 ```python 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 ``` # 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/ ```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 ```bash docker build -t ``` ### Docker processes ```bash docker ps ``` #### Exercises - [L09P01 - Registry Office Service Deployment](file:///home/nikola/Documents/EIST/eist26l09p01-go38coz) - [L09P02 - Garching Best Software](file:///home/nikola/Documents/EIST/eist26l09p02-go38coz) - [L08PE04 - Spaceship](file:///home/nikola/Documents/EIST/eist26l0pe04-go38coz)
# 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 ```repeated``` field ```protobuf 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: ```protobuf 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 ```protobuf 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 1. Gradle tab in IDE and press "Tasks > other > generateProto") 2. 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 ```java public void getCityWeatherSingleDay(LocationDate request, StreamObserver 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: ```Java public void getCityWeatherMultipleDays(LocationDatePeriod request, StreamObserver 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: ```java public void getCityWeatherMultipleDays(LocationDatePeriod request, StreamObserver 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???) ```java public void getAlbumMetadata(EntitiesRequest request, StreamObserver 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(); } ``` ## Client Side #### Setup The **blocking stub** is a placeholder on which the functions are called. It needs to be received from the gRPC object in some way. ```java public WeatherClient(Channel channel) { // Get blocking stub from the gRPC object blockingStub = WeatherReporterGrpc.newBlockingStub(channel); } ``` or ```java 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 ```java 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*** ```java 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 for the stream of CityWeatherData Iterator 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* ```java 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(); } ```
## 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)**