diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index ec40f34..c38cb1f 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -14,6 +14,12 @@ jobs:
distribution: 'temurin'
java-version: '17'
+ - name: Setup env.properties
+ run: |
+ echo "DB_URL=${{ secrets.DB_URL }}" >> src/main/resources/env.properties
+ echo "DB_USERNAME=${{ secrets.DB_USERNAME }}" >> src/main/resources/env.properties
+ echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> src/main/resources/env.properties
+
- name: Cache Maven packages
uses: actions/cache@v4
with:
diff --git a/.gitignore b/.gitignore
index 5eac309..54f2b0f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,4 +30,7 @@ build/
!**/src/test/**/build/
### VS Code ###
-.vscode/
\ No newline at end of file
+.vscode/
+
+### Enviroment ###
+env.properties
diff --git a/pom.xml b/pom.xml
index 14ce122..80015cb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -34,17 +34,27 @@
org.springframework.boot
spring-boot-starter-web
-
+
com.mysql
mysql-connector-j
- runtime
+ 9.1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
org.projectlombok
lombok
true
+
+
+ org.modelmapper
+ modelmapper
+ 3.2.1
+
org.springframework.boot
spring-boot-starter-test
diff --git a/src/main/java/com/carpi/carpibackend/Application.java b/src/main/java/com/carpi/carpibackend/Application.java
index 21f18c2..3fa06d1 100644
--- a/src/main/java/com/carpi/carpibackend/Application.java
+++ b/src/main/java/com/carpi/carpibackend/Application.java
@@ -9,5 +9,4 @@ public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
-
}
diff --git a/src/main/java/com/carpi/carpibackend/ApplicationConfig.java b/src/main/java/com/carpi/carpibackend/ApplicationConfig.java
new file mode 100644
index 0000000..0d2198e
--- /dev/null
+++ b/src/main/java/com/carpi/carpibackend/ApplicationConfig.java
@@ -0,0 +1,35 @@
+package com.carpi.carpibackend;
+
+import org.modelmapper.Converter;
+import org.modelmapper.ModelMapper;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import com.carpi.carpibackend.dto.CourseDto;
+import com.carpi.carpibackend.entity.CourseSearchResult;
+
+@Configuration
+public class ApplicationConfig {
+
+ private static final String[] EMPTY_LIST = new String[0];
+
+ @Bean
+ public ModelMapper modelMapper() {
+ ModelMapper modelMapper = new ModelMapper();
+ Converter split =
+ ctx -> ctx.getSource() == null ? EMPTY_LIST : ctx.getSource().split(",");
+ modelMapper.typeMap(CourseSearchResult.class, CourseDto.class).addMappings(
+ mapper -> {
+ mapper.using(split).map(
+ CourseSearchResult::getSemesterList,
+ CourseDto::setSemesterList
+ );
+ mapper.using(split).map(
+ CourseSearchResult::getAttributeList,
+ CourseDto::setAttributeList
+ );
+ }
+ );
+ return modelMapper;
+ }
+}
diff --git a/src/main/java/com/carpi/carpibackend/controller/CourseController.java b/src/main/java/com/carpi/carpibackend/controller/CourseController.java
new file mode 100644
index 0000000..ed541ec
--- /dev/null
+++ b/src/main/java/com/carpi/carpibackend/controller/CourseController.java
@@ -0,0 +1,54 @@
+package com.carpi.carpibackend.controller;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.modelmapper.ModelMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.carpi.carpibackend.dto.CourseDto;
+import com.carpi.carpibackend.entity.CourseSearchResult;
+import com.carpi.carpibackend.service.CourseSearchService;
+
+@CrossOrigin
+@RestController
+@RequestMapping("/api/v1/course")
+public class CourseController {
+
+ @Autowired
+ private CourseSearchService courseSearchService;
+
+ @Autowired
+ private ModelMapper modelMapper;
+
+ @GetMapping("/all")
+ public ResponseEntity> getAll() {
+ return searchCourses(null, null, null, null);
+ }
+
+ @GetMapping("/search")
+ public ResponseEntity> searchCourses(
+ @RequestParam(required = false) String searchPrompt,
+ @RequestParam(required = false) String[] deptFilters,
+ @RequestParam(required = false) String[] attrFilters,
+ @RequestParam(required = false) String[] semFilters
+ ) {
+ List searchResults = courseSearchService.searchCourses(
+ searchPrompt,
+ deptFilters,
+ attrFilters,
+ semFilters
+ );
+ List courseDtos = searchResults.stream().map(
+ result -> modelMapper.map(result, CourseDto.class)
+ ).collect(Collectors.toList());
+ return ResponseEntity.ok(courseDtos);
+ }
+}
diff --git a/src/main/java/com/carpi/carpibackend/dto/CourseDto.java b/src/main/java/com/carpi/carpibackend/dto/CourseDto.java
new file mode 100644
index 0000000..db391e7
--- /dev/null
+++ b/src/main/java/com/carpi/carpibackend/dto/CourseDto.java
@@ -0,0 +1,26 @@
+package com.carpi.carpibackend.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+
+public class CourseDto {
+
+ private String department;
+
+ private int code;
+
+ private String title;
+
+ private String description;
+
+ private short creditMin;
+
+ private short creditMax;
+
+ private String[] semesterList;
+
+ private String[] attributeList;
+}
diff --git a/src/main/java/com/carpi/carpibackend/entity/Course.java b/src/main/java/com/carpi/carpibackend/entity/Course.java
new file mode 100644
index 0000000..f662b97
--- /dev/null
+++ b/src/main/java/com/carpi/carpibackend/entity/Course.java
@@ -0,0 +1,39 @@
+package com.carpi.carpibackend.entity;
+
+import com.carpi.carpibackend.keys.CourseKey;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.EmbeddedId;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+
+@Entity
+@Table(name = "course")
+public class Course {
+
+ @EmbeddedId
+ private CourseKey pkCourses;
+
+ @Column(name = "dept", nullable = false)
+ private String department;
+
+ @Column(name = "code_num", nullable = false)
+ private int code;
+
+ @Column(name = "title", nullable = false)
+ private String title;
+
+ @Column(name = "desc_text", nullable = false)
+ private String description;
+
+ @Column(name = "credit_min", nullable = false)
+ private short creditMin;
+
+ @Column(name = "credit_max", nullable = false)
+ private short creditMax;
+}
diff --git a/src/main/java/com/carpi/carpibackend/entity/CourseSearchResult.java b/src/main/java/com/carpi/carpibackend/entity/CourseSearchResult.java
new file mode 100644
index 0000000..41c33be
--- /dev/null
+++ b/src/main/java/com/carpi/carpibackend/entity/CourseSearchResult.java
@@ -0,0 +1,61 @@
+package com.carpi.carpibackend.entity;
+
+import com.carpi.carpibackend.keys.CourseKey;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.EmbeddedId;
+import jakarta.persistence.Entity;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+
+@Entity
+public class CourseSearchResult {
+
+ @EmbeddedId
+ private CourseKey pkCourses;
+
+ @Column(name = "dept", nullable = false)
+ private String department;
+
+ @Column(name = "code_num", nullable = false)
+ private int code;
+
+ @Column(name = "title", nullable = false)
+ private String title;
+
+ @Column(name = "desc_text", nullable = false)
+ private String description;
+
+ @Column(name = "credit_min", nullable = false)
+ private short creditMin;
+
+ @Column(name = "credit_max", nullable = false)
+ private short creditMax;
+
+ @Column(name = "sem_list", nullable = false)
+ private String semesterList;
+
+ @Column(name = "attr_list", nullable = true)
+ private String attributeList;
+
+ @Column(name = "code_match", nullable = false)
+ private boolean codeMatch;
+
+ @Column(name = "title_exact_match", nullable = false)
+ private boolean titleExactMatch;
+
+ @Column(name = "title_start_match", nullable = false)
+ private boolean titleStartMatch;
+
+ @Column(name = "title_match", nullable = false)
+ private boolean titleMatch;
+
+ @Column(name = "title_acronym", nullable = false)
+ private boolean titleAcronym;
+
+ @Column(name = "title_abbrev", nullable = false)
+ private boolean titleAbbrev;
+}
diff --git a/src/main/java/com/carpi/carpibackend/keys/CourseKey.java b/src/main/java/com/carpi/carpibackend/keys/CourseKey.java
new file mode 100644
index 0000000..18704b8
--- /dev/null
+++ b/src/main/java/com/carpi/carpibackend/keys/CourseKey.java
@@ -0,0 +1,40 @@
+package com.carpi.carpibackend.keys;
+
+import java.io.Serializable;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor
+@AllArgsConstructor
+
+@Embeddable
+public class CourseKey implements Serializable {
+
+ @Column(name = "dept", insertable = false, updatable = false, nullable = false)
+ private String department;
+
+ @Column(name = "code_num", insertable = false, updatable = false, nullable = false)
+ private int code;
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof CourseKey) {
+ CourseKey other = (CourseKey) obj;
+ return department.equals(other.department) && code == other.code;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = department.hashCode();
+ result = 31 * result + Integer.hashCode(code);
+ return result;
+ }
+}
diff --git a/src/main/java/com/carpi/carpibackend/repository/CourseRepository.java b/src/main/java/com/carpi/carpibackend/repository/CourseRepository.java
new file mode 100644
index 0000000..fd63c14
--- /dev/null
+++ b/src/main/java/com/carpi/carpibackend/repository/CourseRepository.java
@@ -0,0 +1,12 @@
+package com.carpi.carpibackend.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import com.carpi.carpibackend.entity.Course;
+import com.carpi.carpibackend.keys.CourseKey;
+
+@Repository
+public interface CourseRepository extends JpaRepository {
+
+}
diff --git a/src/main/java/com/carpi/carpibackend/repository/CourseSearchResultRepository.java b/src/main/java/com/carpi/carpibackend/repository/CourseSearchResultRepository.java
new file mode 100644
index 0000000..b37bd30
--- /dev/null
+++ b/src/main/java/com/carpi/carpibackend/repository/CourseSearchResultRepository.java
@@ -0,0 +1,86 @@
+package com.carpi.carpibackend.repository;
+
+import java.util.List;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+
+import com.carpi.carpibackend.entity.CourseSearchResult;
+import com.carpi.carpibackend.keys.CourseKey;
+
+@Repository
+public interface CourseSearchResultRepository extends JpaRepository {
+
+ @Query(
+ value = """
+ SELECT
+ course.dept AS dept,
+ course.code_num AS code_num,
+ course.title AS title,
+ course.desc_text AS desc_text,
+ course.credit_min AS credit_min,
+ course.credit_max AS credit_max,
+ GROUP_CONCAT(DISTINCT CONCAT(course_seats.semester, ' ', course_seats.sem_year)) AS sem_list,
+ GROUP_CONCAT(DISTINCT course_attribute.attr ORDER BY course_attribute.attr ASC) AS attr_list,
+ REGEXP_LIKE(CONCAT(course.dept, ' ', course.code_num), ?1, 'i') AS code_match,
+ REGEXP_LIKE(course.title, ?2, 'i') AS title_exact_match,
+ REGEXP_LIKE(course.title, ?3, 'i') AS title_start_match,
+ REGEXP_LIKE(course.title, ?4, 'i') AS title_match,
+ REGEXP_LIKE(course.title, ?5, 'i') AS title_acronym,
+ REGEXP_LIKE(course.title, ?6, 'i') AS title_abbrev
+ FROM
+ course
+ INNER JOIN course_seats USING(dept, code_num)
+ LEFT JOIN course_attribute USING(dept, code_num)
+ WHERE
+ REGEXP_LIKE(dept, ?7, 'i') > 0
+ GROUP BY
+ dept,
+ code_num,
+ title,
+ desc_text,
+ credit_min,
+ credit_max,
+ code_match,
+ title_exact_match,
+ title_start_match,
+ title_match,
+ title_acronym,
+ title_abbrev
+ HAVING
+ (
+ code_match > 0
+ OR title_exact_match > 0
+ OR title_start_match > 0
+ OR title_match > 0
+ OR title_acronym > 0
+ OR title_abbrev > 0
+ )
+ AND REGEXP_LIKE(IFNULL(attr_list, ''), ?8, 'i') > 0
+ AND REGEXP_LIKE(sem_list, ?9, 'i') > 0
+ ORDER BY
+ code_match DESC,
+ title_exact_match DESC,
+ title_start_match DESC,
+ title_match DESC,
+ title_acronym DESC,
+ title_abbrev DESC,
+ code_num ASC,
+ dept ASC
+ ;
+ """,
+ nativeQuery = true
+ )
+ public List searchCourses(
+ String searchCodeRegex,
+ String searchFullRegex,
+ String searchStartRegex,
+ String searchAnyRegex,
+ String searchAcronymRegex,
+ String searchAbbrevRegex,
+ String deptFilterRegex,
+ String attrFilterRegex,
+ String semFilterRegex
+ );
+}
diff --git a/src/main/java/com/carpi/carpibackend/service/CourseSearchService.java b/src/main/java/com/carpi/carpibackend/service/CourseSearchService.java
new file mode 100644
index 0000000..b29d772
--- /dev/null
+++ b/src/main/java/com/carpi/carpibackend/service/CourseSearchService.java
@@ -0,0 +1,106 @@
+package com.carpi.carpibackend.service;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.carpi.carpibackend.entity.CourseSearchResult;
+import com.carpi.carpibackend.repository.CourseSearchResultRepository;
+
+@Service
+public class CourseSearchService {
+
+ @Autowired
+ private CourseSearchResultRepository courseSearchResultRepository;
+
+ /**
+ * @author Jack Zgombic
+ * @author Raymond Chen
+ * @param searchPrompt Prompt used to search for relevant courses. May be null.
+ * @param deptFilters Department filters (e.g. "CSCI", "MATH"). May be null.
+ * @param attrFilters Attribute filters (e.g. "HASS Inquiry"). May be null.
+ * @param semFilters Semester filters (e.g. "Fall", "Spring"). May be null.
+ * @return A list containing the most relevant courses according to the given
+ * search prompt and filters, or a list of all courses if all arguments
+ * are null.
+ */
+ public List searchCourses(
+ String searchPrompt,
+ String[] deptFilters,
+ String[] attrFilters,
+ String[] semFilters
+ ) {
+ String[] deptFiltersCopy = null,
+ attrFiltersCopy = null,
+ semFiltersCopy = null;
+ String deptFilterRegex = ".*",
+ attrFilterRegex = ".*",
+ semFilterRegex = ".*";
+ if (deptFilters != null && deptFilters.length > 0) {
+ deptFiltersCopy = Arrays.copyOf(deptFilters, deptFilters.length);
+ Arrays.sort(deptFiltersCopy);
+ deptFilterRegex = String.join("|", deptFiltersCopy);
+ }
+ if (attrFilters != null && attrFilters.length > 0) {
+ attrFiltersCopy = Arrays.copyOf(attrFilters, attrFilters.length);
+ Arrays.sort(attrFiltersCopy);
+ attrFilterRegex = String.join(".*", attrFiltersCopy);
+ }
+ if (semFilters != null && semFilters.length > 0) {
+ semFiltersCopy = Arrays.copyOf(semFilters, semFilters.length);
+ Arrays.sort(semFiltersCopy);
+ semFilterRegex = String.join(".*", semFiltersCopy);
+ }
+ if (searchPrompt == null) {
+ return courseSearchResultRepository.searchCourses(
+ ".*",
+ ".*",
+ ".*",
+ ".*",
+ ".*",
+ ".*",
+ deptFilterRegex,
+ attrFilterRegex,
+ semFilterRegex
+ );
+ }
+ final String regStartOrSpace = "(^|.* )";
+ String searchCodeRegex = "^" + searchPrompt + "$",
+ searchFullRegex = "^" + searchPrompt + "$",
+ searchStartRegex = "^" + searchPrompt,
+ searchAnyRegex = searchPrompt,
+ searchAcronymRegex = regStartOrSpace;
+ for (int i = 0; i < searchPrompt.length(); ++i) {
+ char ch = searchPrompt.charAt(i);
+ if (ch != ' ') {
+ searchAcronymRegex += ch + ".* ";
+ }
+ }
+ searchAcronymRegex = searchAcronymRegex.substring(0, searchAcronymRegex.length() - 3);
+ String searchAbbrevRegex = "";
+ String[] tokens = searchPrompt.split(" ");
+ if (tokens.length > 1) {
+ searchAbbrevRegex += regStartOrSpace;
+ for (int i = 0; i < tokens.length; ++i) {
+ searchAbbrevRegex += tokens[i] + ".* ";
+ }
+ searchAbbrevRegex = searchAbbrevRegex.substring(0, searchAbbrevRegex.length() - 3);
+ }
+ else {
+ searchAbbrevRegex = "a^";
+ }
+ return courseSearchResultRepository.searchCourses(
+ searchCodeRegex,
+ searchFullRegex,
+ searchStartRegex,
+ searchAnyRegex,
+ searchAcronymRegex,
+ searchAbbrevRegex,
+ deptFilterRegex,
+ attrFilterRegex,
+ semFilterRegex
+ );
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index ddfa32a..8d87d89 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1 +1,7 @@
spring.application.name=carpi-backend
+
+spring.config.import=file:src/main/resources/env.properties
+
+spring.datasource.url=${DB_URL}
+spring.datasource.username=${DB_USERNAME}
+spring.datasource.password=${DB_PASSWORD}
diff --git a/src/main/resources/env.properties.example b/src/main/resources/env.properties.example
new file mode 100644
index 0000000..b721dd4
--- /dev/null
+++ b/src/main/resources/env.properties.example
@@ -0,0 +1,3 @@
+DB_URL=
+DB_USERNAME=
+DB_PASSWORD=