Skip to content

Commit d9dbff6

Browse files
authored
feat: adds lesson_23 homework and lesson_24 pre-work (#729)
Signed-off-by: Anthony D. Mays <anthony@morganlatimer.com>
1 parent 21024c5 commit d9dbff6

File tree

124 files changed

+14940
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

124 files changed

+14940
-1
lines changed

lesson_23/README.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,49 @@ Please review the following resources before lecture:
1919

2020
## Homework
2121

22-
- TODO(anthonydmays): Define requirements and stuff...
22+
- [ ] Complete the [Creating a Library API](#creating-a-library-api) assignment.
23+
- [ ] Do pre-work for [lesson 27](/lesson_27/).
24+
25+
### Creating a Library API
26+
27+
We are continuing to build atop the foundation of our library app. For this assignment, you will help implement the API that will be used by a yet-to-come front-end app.
28+
29+
* You will implement the [MediaItemsController][controller-file] to enable the following API:
30+
* `GET /items` - Retrieves a list of media items
31+
* `POST /items` - Creates a new media item
32+
* `GET /items/:id` - Retrieves a single media item with the given ID.
33+
* `DELETE /items/:id` - Deletes a single media item with the given ID.
34+
* Study the tests in [MediaItemsControllerTest][controller-test-file] to understand what you should accept and return in the API.
35+
* You should not need to make any code changes outside of the `com.codedifferently.lesson23.web` package.
36+
37+
#### Running the API
38+
39+
You can run the server using the usual `./gradlew run --console=plain` command from the `api/java` directory. If you want to test that the server is running correctly, you can use `curl` like so:
40+
41+
```bash
42+
curl http://localhost:3001/items | json_pp
43+
```
44+
45+
The project also includes an OpenAPI user interface (Swagger) for navigating the API. Just visit http://localhost:3001/swagger-ui.html to access it.
46+
47+
Alternatively, you can also test the API using the tool [Postman][postman-link]. I recommend installing this tool to make it easier to test things.
48+
49+
#### Debugging the API
50+
51+
Remember that you can debug the API by visiting the main function in [Lesson23.java][main-file] and clicking `Debug main`. You'll be able to set breakpoints in your code to see what's happening and fix issues.
52+
53+
![Debugging the API](./debug.png)
54+
55+
56+
#### TypeScript API
57+
58+
This project also includes a fully functioning TypeScript version of the Java project. You can visit `api/javascript/api_app` to execute it using `npm start` and view the OpenAPI documentation at http://localhost:3000/api (note that it runs on port 3000).
59+
60+
## Additional resources
61+
62+
* [gRPC vs REST: Comparing API Styles in Practice (Article)](https://dev.to/anthonydmays/grpc-vs-rest-comparing-api-styles-in-practice-4bl): This article explains why the stuff most people call REST isn't actually.
63+
64+
[controller-file]: ./api/java/api_app/src/main/java/com/codedifferently/lesson23/web/MediaItemsController.java
65+
[controller-test-file]: ./api/java/api_app/src/test/java/com/codedifferently/lesson23/web/MediaItemsControllerTest.java
66+
[postman-link]: https://www.postman.com/downloads/
67+
[main-file]: ./api/java/api_app/src/main/java/com/codedifferently/lesson23/Lesson23.java

lesson_23/api/java/.gitattributes

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#
2+
# https://help.github.com/articles/dealing-with-line-endings/
3+
#
4+
# Linux start script should use lf
5+
/gradlew text eol=lf
6+
7+
# These are Windows script files and should use crlf
8+
*.bat text eol=crlf
9+

lesson_23/api/java/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Ignore Gradle project-specific cache directory
2+
.gradle
3+
4+
# Ignore Gradle build output directory
5+
build
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
plugins {
2+
// Apply the application plugin to add support for building a CLI application in Java.
3+
application
4+
eclipse
5+
id("com.diffplug.spotless") version "6.25.0"
6+
id("org.springframework.boot") version "3.4.0"
7+
id("com.adarshr.test-logger") version "4.0.0"
8+
}
9+
10+
apply(plugin = "io.spring.dependency-management")
11+
12+
repositories {
13+
// Use Maven Central for resolving dependencies.
14+
mavenCentral()
15+
}
16+
17+
dependencies {
18+
// Use JUnit Jupiter for testing.
19+
testImplementation("com.codedifferently.instructional:instructional-lib")
20+
testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
21+
testImplementation("org.springframework.boot:spring-boot-starter-test")
22+
testImplementation("org.assertj:assertj-core:3.26.3")
23+
testImplementation("at.favre.lib:bcrypt:0.10.2")
24+
testImplementation("org.springframework.boot:spring-boot-starter-test")
25+
26+
// This dependency is used by the application.
27+
implementation("com.codedifferently.instructional:instructional-lib")
28+
implementation("com.google.guava:guava:33.3.1-jre")
29+
implementation("com.google.code.gson:gson:2.11.0")
30+
implementation("commons-cli:commons-cli:1.6.0")
31+
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0")
32+
implementation("org.springframework.boot:spring-boot-starter")
33+
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
34+
implementation("org.springframework.boot:spring-boot-starter-validation")
35+
implementation("org.springframework.boot:spring-boot-starter-web")
36+
compileOnly("org.springframework.boot:spring-boot-devtools")
37+
implementation("com.opencsv:opencsv:5.9")
38+
implementation("org.apache.commons:commons-csv:1.10.0")
39+
implementation("org.xerial:sqlite-jdbc:3.36.0")
40+
implementation("org.hibernate.orm:hibernate-community-dialects:6.2.7.Final")
41+
compileOnly("org.projectlombok:lombok:1.18.38")
42+
annotationProcessor("org.projectlombok:lombok:1.18.38")
43+
testCompileOnly("org.projectlombok:lombok:1.18.38")
44+
testAnnotationProcessor("org.projectlombok:lombok:1.18.38")
45+
}
46+
47+
application {
48+
// Define the main class for the application.
49+
mainClass.set("com.codedifferently.lesson23.Lesson23")
50+
}
51+
52+
tasks.named<JavaExec>("run") {
53+
standardInput = System.`in`
54+
}
55+
56+
tasks.named<Test>("test") {
57+
// Use JUnit Platform for unit tests.
58+
useJUnitPlatform()
59+
}
60+
61+
62+
configure<com.diffplug.gradle.spotless.SpotlessExtension> {
63+
64+
format("misc", {
65+
// define the files to apply `misc` to
66+
target("*.gradle", ".gitattributes", ".gitignore")
67+
68+
// define the steps to apply to those files
69+
trimTrailingWhitespace()
70+
indentWithTabs() // or spaces. Takes an integer argument if you don't like 4
71+
endWithNewline()
72+
})
73+
74+
java {
75+
// don't need to set target, it is inferred from java
76+
77+
// apply a specific flavor of google-java-format
78+
googleJavaFormat()
79+
// fix formatting of type annotations
80+
formatAnnotations()
81+
}
82+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# This file is generated by the 'io.freefair.lombok' Gradle plugin
2+
config.stopBubbling = true
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.codedifferently.lesson23;
2+
3+
import com.codedifferently.lesson23.cli.LibraryApp;
4+
import org.springframework.beans.factory.annotation.Autowired;
5+
import org.springframework.boot.CommandLineRunner;
6+
import org.springframework.boot.SpringApplication;
7+
import org.springframework.boot.autoconfigure.SpringBootApplication;
8+
import org.springframework.context.annotation.Configuration;
9+
10+
@Configuration
11+
@SpringBootApplication(scanBasePackages = "com.codedifferently")
12+
public class Lesson23 implements CommandLineRunner {
13+
14+
@Autowired private LibraryApp libraryApp;
15+
16+
public static void main(String[] args) {
17+
var application = new SpringApplication(Lesson23.class);
18+
application.run(args);
19+
}
20+
21+
@Override
22+
public void run(String... args) throws Exception {
23+
// Don't run as an app if we're running as a JUnit test.
24+
if (isJUnitTest()) {
25+
return;
26+
}
27+
28+
libraryApp.run(args);
29+
}
30+
31+
private static boolean isJUnitTest() {
32+
for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
33+
if (element.getClassName().startsWith("org.junit.")) {
34+
return true;
35+
}
36+
}
37+
return false;
38+
}
39+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package com.codedifferently.lesson23.cli;
2+
3+
import com.codedifferently.lesson23.factory.LibraryDataLoader;
4+
import com.codedifferently.lesson23.library.Book;
5+
import com.codedifferently.lesson23.library.Library;
6+
import com.codedifferently.lesson23.library.LibraryInfo;
7+
import com.codedifferently.lesson23.library.MediaItem;
8+
import com.codedifferently.lesson23.library.search.SearchCriteria;
9+
import java.util.Map;
10+
import java.util.Scanner;
11+
import java.util.Set;
12+
import java.util.UUID;
13+
import org.apache.commons.cli.CommandLine;
14+
import org.apache.commons.cli.CommandLineParser;
15+
import org.apache.commons.cli.DefaultParser;
16+
import org.apache.commons.cli.HelpFormatter;
17+
import org.apache.commons.cli.Option;
18+
import org.apache.commons.cli.Options;
19+
import org.apache.commons.cli.ParseException;
20+
import org.springframework.beans.factory.annotation.Autowired;
21+
import org.springframework.stereotype.Service;
22+
23+
@Service
24+
public final class LibraryApp {
25+
26+
@Autowired private Library library;
27+
28+
public void run(String[] args) throws Exception {
29+
// Show stats about the loaded library to the user.
30+
printLibraryInfo(library);
31+
32+
try (var scanner = new Scanner(System.in)) {
33+
LibraryCommand command;
34+
// Main application loop.
35+
while ((command = promptForCommand(scanner)) != LibraryCommand.EXIT) {
36+
switch (command) {
37+
case SEARCH -> doSearch(scanner, library);
38+
default -> System.out.println("\nNot ready yet, coming soon!");
39+
}
40+
}
41+
}
42+
}
43+
44+
private void printLibraryInfo(Library library) {
45+
LibraryInfo info = library.getInfo();
46+
Map<UUID, Set<MediaItem>> checkedOutItemsByGuest = info.getCheckedOutItemsByGuest();
47+
int numCheckedOutItems = checkedOutItemsByGuest.values().stream().mapToInt(Set::size).sum();
48+
System.out.println();
49+
System.out.println("========================================");
50+
System.out.println("Library id: " + library.getId());
51+
System.out.println("Number of items: " + info.getItems().size());
52+
System.out.println("Number of guests: " + info.getGuests().size());
53+
System.out.println("Number of checked out items: " + numCheckedOutItems);
54+
System.out.println("========================================");
55+
System.out.println();
56+
}
57+
58+
private static LibraryDataLoader getLoaderOrDefault(
59+
String[] args, LibraryDataLoader defaultLoader) throws Exception {
60+
String loaderType = getLoaderFromCommandLine(args);
61+
return loaderType == null
62+
? defaultLoader
63+
: Class.forName(loaderType)
64+
.asSubclass(LibraryDataLoader.class)
65+
.getDeclaredConstructor()
66+
.newInstance();
67+
}
68+
69+
private static String getLoaderFromCommandLine(String[] args) throws IllegalArgumentException {
70+
Options options = new Options();
71+
Option input = new Option("l", "loader", true, "data loader type");
72+
input.setRequired(false);
73+
options.addOption(input);
74+
CommandLineParser parser = new DefaultParser();
75+
HelpFormatter formatter = new HelpFormatter();
76+
try {
77+
CommandLine cmd = parser.parse(options, args);
78+
return cmd.getOptionValue("loader");
79+
} catch (ParseException e) {
80+
System.out.println();
81+
System.out.println(e.getMessage());
82+
formatter.printHelp("utility-name", options);
83+
84+
System.exit(1);
85+
}
86+
return null;
87+
}
88+
89+
private static LibraryCommand promptForCommand(Scanner scanner) {
90+
var command = LibraryCommand.UNKNOWN;
91+
while (command == LibraryCommand.UNKNOWN) {
92+
printMenu();
93+
var input = scanner.nextLine();
94+
try {
95+
command = LibraryCommand.fromValue(Integer.parseInt(input.trim()));
96+
} catch (IllegalArgumentException e) {
97+
System.out.println("Invalid command: " + input);
98+
}
99+
}
100+
return command;
101+
}
102+
103+
private static void printMenu() {
104+
System.out.println("\nEnter the number of the desired command:");
105+
System.out.println("1) << EXIT");
106+
System.out.println("2) SEARCH");
107+
System.out.println("3) CHECKOUT");
108+
System.out.println("4) RETURN");
109+
System.out.print("command> ");
110+
}
111+
112+
private void doSearch(Scanner scanner, Library library) {
113+
LibrarySearchCommand command = promptForSearchCommand(scanner);
114+
if (command == LibrarySearchCommand.RETURN) {
115+
return;
116+
}
117+
SearchCriteria criteria = getSearchCriteria(scanner, command);
118+
Set<MediaItem> results = library.search(criteria);
119+
printSearchResults(results);
120+
}
121+
122+
private LibrarySearchCommand promptForSearchCommand(Scanner scanner) {
123+
var command = LibrarySearchCommand.UNKNOWN;
124+
while (command == LibrarySearchCommand.UNKNOWN) {
125+
printSearchMenu();
126+
var input = scanner.nextLine();
127+
try {
128+
command = LibrarySearchCommand.fromValue(Integer.parseInt(input.trim()));
129+
} catch (IllegalArgumentException e) {
130+
System.out.println("Invalid command: " + input);
131+
}
132+
}
133+
return command;
134+
}
135+
136+
private void printSearchMenu() {
137+
System.out.println("\nEnter the number of the desired search criteria:");
138+
System.out.println("1) << RETURN");
139+
System.out.println("2) TITLE");
140+
System.out.println("3) AUTHOR");
141+
System.out.println("4) TYPE");
142+
System.out.print("search> ");
143+
}
144+
145+
private SearchCriteria getSearchCriteria(Scanner scanner, LibrarySearchCommand command) {
146+
System.out.println();
147+
switch (command) {
148+
case TITLE -> {
149+
System.out.println("Enter the title to search for: ");
150+
System.out.print("title> ");
151+
var title = scanner.nextLine();
152+
return SearchCriteria.builder().title(title).build();
153+
}
154+
case AUTHOR -> {
155+
System.out.println("Enter the author to search for: ");
156+
System.out.print("author> ");
157+
var author = scanner.nextLine();
158+
return SearchCriteria.builder().author(author).build();
159+
}
160+
case TYPE -> {
161+
System.out.println("Enter the type to search for: ");
162+
System.out.print("type> ");
163+
var type = scanner.nextLine();
164+
return SearchCriteria.builder().type(type).build();
165+
}
166+
default -> System.out.println("Invalid search command: " + command);
167+
}
168+
return null;
169+
}
170+
171+
private void printSearchResults(Set<MediaItem> results) {
172+
System.out.println();
173+
174+
if (results.isEmpty()) {
175+
System.out.println("No results found.");
176+
return;
177+
}
178+
179+
System.out.println("Search results:\n");
180+
for (MediaItem item : results) {
181+
System.out.println("ID: " + item.getId());
182+
System.out.println("TITLE: " + item.getTitle());
183+
if (item instanceof Book book) {
184+
System.out.println("AUTHOR(S): " + String.join(", ", book.getAuthors()));
185+
}
186+
System.out.println("TYPE: " + item.getType().toString().toUpperCase());
187+
System.out.println();
188+
}
189+
System.out.println("Found " + results.size() + " result(s).\n");
190+
}
191+
}

0 commit comments

Comments
 (0)