diff --git a/src/main/java/org/pkwmtt/timetable/TimetableController.java b/src/main/java/org/pkwmtt/timetable/TimetableController.java index f7490d6..c5a1bfd 100644 --- a/src/main/java/org/pkwmtt/timetable/TimetableController.java +++ b/src/main/java/org/pkwmtt/timetable/TimetableController.java @@ -5,10 +5,12 @@ import org.pkwmtt.exceptions.SpecifiedGeneralGroupDoesntExistsException; import org.pkwmtt.exceptions.SpecifiedSubGroupDoesntExistsException; import org.pkwmtt.exceptions.WebPageContentNotAvailableException; +import org.pkwmtt.timetable.dto.CustomSubjectFilterDTO; import org.pkwmtt.timetable.dto.TimetableDTO; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.ArrayList; import java.util.List; import static java.util.Objects.isNull; @@ -29,13 +31,42 @@ public class TimetableController { * @throws WebPageContentNotAvailableException . */ @GetMapping("/{generalGroupName}") - public ResponseEntity getGeneralGroupSchedule (@PathVariable String generalGroupName, @RequestParam(required = false, name = "sub") List subgroups) + public ResponseEntity getGeneralGroupSchedule (@PathVariable String generalGroupName, + @RequestParam(required = false, name = "sub") List subgroups) throws WebPageContentNotAvailableException, SpecifiedGeneralGroupDoesntExistsException, SpecifiedSubGroupDoesntExistsException, JsonProcessingException { + var areSubgroupsProvided = !(isNull(subgroups) || subgroups.isEmpty()); - if (isNull(subgroups) || subgroups.isEmpty()) { - return ResponseEntity.ok(cachedService.getGeneralGroupSchedule(generalGroupName)); + return + areSubgroupsProvided ? + ResponseEntity.ok(service.getFilteredGeneralGroupSchedule( + generalGroupName, + subgroups, + new ArrayList<>() + )) + : ResponseEntity.ok(cachedService.getGeneralGroupSchedule(generalGroupName)); + } + + @PostMapping(value = "/{generalGroupName}", consumes = "application/json", produces = "application/json") + public ResponseEntity getGeneralGroupScheduleWithCustomSubjects (@PathVariable String generalGroupName, + @RequestParam(required = false, name = "sub") List subgroups, + @RequestBody(required = false) List customSubjects) + throws WebPageContentNotAvailableException, SpecifiedGeneralGroupDoesntExistsException, SpecifiedSubGroupDoesntExistsException, JsonProcessingException { + var areSubgroupsProvided = !(isNull(subgroups) || subgroups.isEmpty()); + var areCustomSubjectsProvided = !(isNull(customSubjects) || customSubjects.isEmpty()); + + if (areSubgroupsProvided) { + if (!areCustomSubjectsProvided) { + customSubjects = new ArrayList<>(); + } + + return ResponseEntity.ok(service.getFilteredGeneralGroupSchedule( + generalGroupName, + subgroups, + customSubjects + )); + } - return ResponseEntity.ok(service.getFilteredGeneralGroupSchedule(generalGroupName, subgroups)); + return ResponseEntity.ok(cachedService.getGeneralGroupSchedule(generalGroupName)); } /** diff --git a/src/main/java/org/pkwmtt/timetable/TimetableExceptionHandler.java b/src/main/java/org/pkwmtt/timetable/TimetableExceptionHandler.java index bb92055..08944b3 100644 --- a/src/main/java/org/pkwmtt/timetable/TimetableExceptionHandler.java +++ b/src/main/java/org/pkwmtt/timetable/TimetableExceptionHandler.java @@ -18,12 +18,10 @@ public class TimetableExceptionHandler { @ExceptionHandler(WebPageContentNotAvailableException.class) @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE) - public ResponseEntity handleWebPageContentNotAvailableException (WebPageContentNotAvailableException e) { + public ResponseEntity handleWebPageContentNotAvailableException ( + WebPageContentNotAvailableException e) { log.error("SERVICE_UNAVAILABLE # " + e.getMessage()); - return new ResponseEntity<>( - new ErrorResponseDTO(e.getMessage()), - HttpStatus.SERVICE_UNAVAILABLE - ); + return new ResponseEntity<>(new ErrorResponseDTO(e.getMessage()), HttpStatus.SERVICE_UNAVAILABLE); } @ExceptionHandler(JsonProcessingException.class) @@ -32,11 +30,11 @@ public ResponseEntity handleJsonProcessingException (JsonProce log.error("INTERNAL_SERVER_ERROR # " + e.getMessage()); return new ResponseEntity<>( new ErrorResponseDTO("Json Processing Failed"), - HttpStatus.INTERNAL_SERVER_ERROR + HttpStatus.INTERNAL_SERVER_ERROR ); } - @ExceptionHandler({SpecifiedGeneralGroupDoesntExistsException.class, SpecifiedSubGroupDoesntExistsException.class}) + @ExceptionHandler({SpecifiedGeneralGroupDoesntExistsException.class, SpecifiedSubGroupDoesntExistsException.class, IllegalArgumentException.class}) @ResponseStatus(HttpStatus.BAD_REQUEST) public ResponseEntity handleSpecifiedGeneralGroupDoesntExistsException (Exception e) { return new ResponseEntity<>(new ErrorResponseDTO(e.getMessage()), HttpStatus.BAD_REQUEST); @@ -46,9 +44,6 @@ public ResponseEntity handleSpecifiedGeneralGroupDoesntExistsE @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ResponseEntity handleIllegalAccessException (IllegalAccessException e) { log.error("INTERNAL_SERVER_ERROR # " + e.getMessage()); - return new ResponseEntity<>( - new ErrorResponseDTO(e.getMessage()), - HttpStatus.INTERNAL_SERVER_ERROR - ); + return new ResponseEntity<>(new ErrorResponseDTO(e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR); } } diff --git a/src/main/java/org/pkwmtt/timetable/TimetableService.java b/src/main/java/org/pkwmtt/timetable/TimetableService.java index 55d0ce5..1be7e72 100644 --- a/src/main/java/org/pkwmtt/timetable/TimetableService.java +++ b/src/main/java/org/pkwmtt/timetable/TimetableService.java @@ -6,12 +6,17 @@ import org.pkwmtt.exceptions.SpecifiedGeneralGroupDoesntExistsException; import org.pkwmtt.exceptions.SpecifiedSubGroupDoesntExistsException; import org.pkwmtt.exceptions.WebPageContentNotAvailableException; +import org.pkwmtt.timetable.dto.CustomSubjectFilterDTO; import org.pkwmtt.timetable.dto.DayOfWeekDTO; import org.pkwmtt.timetable.dto.SubjectDTO; import org.pkwmtt.timetable.dto.TimetableDTO; +import org.pkwmtt.timetable.enums.TypeOfWeek; +import org.pkwmtt.timetable.objects.CustomSubjectDetails; +import org.pkwmtt.timetable.parser.TimetableParserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -71,28 +76,94 @@ public List getAvailableSubGroups (String generalGroupName) * Retrieves timetable and filters entries based on subgroups parameters * * @param generalGroupName name of the general group - * @param sub subgroups list + * @param subgroup subgroups list * @return filtered timetable * @throws WebPageContentNotAvailableException if source data can't be retrieved */ - public TimetableDTO getFilteredGeneralGroupSchedule (String generalGroupName, List sub) + public TimetableDTO getFilteredGeneralGroupSchedule (String generalGroupName, + List subgroup, + List customSubjectFilters) throws WebPageContentNotAvailableException, SpecifiedGeneralGroupDoesntExistsException, JsonProcessingException { - + //Uppercase name to assure match generalGroupName = generalGroupName.toUpperCase(); - //Check if specified subgroup is available for this generalGroup - var subgroups = getAvailableSubGroups(generalGroupName); - for (var group : sub) { - if (!subgroups.contains(group)) { - throw new SpecifiedSubGroupDoesntExistsException(group); - } - } + //Check if specified subgroup is available for general group or else throw + checkSubGroupAvailability(generalGroupName, subgroup); + //Get user's schedule List schedule = cachedService.getGeneralGroupSchedule(generalGroupName).getData(); + //Get schedule to extract customSubject details + List customSubjects = + createListOfCustomSchedulesDetails(generalGroupName, customSubjectFilters, schedule); - for (var day : schedule) { - sub.forEach(day::filterByGroup); + return filterSchedule(schedule, subgroup, generalGroupName, customSubjects); + } + + private List createListOfCustomSchedulesDetails (String generalGroupName, + List customSubjectFilters, + List schedule) { + List customSubjectsDetails = new ArrayList<>(); + customSubjectFilters.forEach(customFilter -> { + + //Get schedule for specified filter + List customSubjectSchedule = customFilter + .getGeneralGroup() + .equals(generalGroupName) ? schedule : cachedService + .getGeneralGroupSchedule(customFilter.getGeneralGroup()) + .getData(); + + //Add detail like classroom and rowId + //Go by days: Monday, Tuesday etc... + for (int i = 0; i < customSubjectSchedule.size(); i++) { + //Find subjects matching filters + customSubjectsDetails.addAll( + searchDayOfWeekAndAddCustomSubjectsDetails( + customSubjectSchedule.get(i).getEven(), customFilter, i, + TypeOfWeek.EVEN + )); + + customSubjectsDetails.addAll( + searchDayOfWeekAndAddCustomSubjectsDetails( + customSubjectSchedule.get(i).getOdd(), customFilter, i, + TypeOfWeek.ODD + )); + } + }); + return customSubjectsDetails; + } + + private List searchDayOfWeekAndAddCustomSubjectsDetails (List day, + CustomSubjectFilterDTO customFilter, + int dayIndex, + TypeOfWeek typeOfWeek) { + var matches = day.stream() + //Filter by matching name and subgroup from customFilter + .filter(item -> (item.getName().contains(customFilter.getName()) && item + .getName() + .contains(customFilter.getSubGroup()))).toList(); + + if (!matches.isEmpty()) { + return matches + .stream() + .map((item) -> new CustomSubjectDetails(item, customFilter.getSubGroup(), dayIndex, typeOfWeek)) + .toList(); + } + return new ArrayList<>(); + } + + private TimetableDTO filterSchedule (List schedule, + List subgroups, + String generalGroupName, + List customSubjectsDetails) { + //Go through user's schedule day by day + for (int i = 0; i < schedule.size(); i++) { + var day = schedule.get(i); + + //delete subjects colliding with custom subjects by name + deleteSubjectsCollidingWithCustomFilters(customSubjectsDetails, day); + //Filter by user's subgroups + filterDayByUsersSubgroups(subgroups, customSubjectsDetails, day, i); } schedule.forEach(DayOfWeekDTO::deleteSubjectTypesFromNames); @@ -100,9 +171,71 @@ public TimetableDTO getFilteredGeneralGroupSchedule (String generalGroupName, Li return new TimetableDTO(generalGroupName, schedule); } - /** - * @return List of general group's names - */ + private void filterDayByUsersSubgroups (List subgroups, + List customSubjectsDetails, + DayOfWeekDTO day, int dayIndex) { + subgroups.forEach(subgroup -> { + if (customSubjectsDetails.isEmpty()) { + day.filterByGroup(subgroup); + return; + } + + var customSubjectsByDay = customSubjectsDetails.stream() + //Compare day of week and subgroup + .filter(subject -> subject.getSubGroup().charAt(0) == subgroup.charAt(0)) // match subgroup + .filter(subject -> subject.getDayOfWeekNumber() == dayIndex) // match day of week + .toList(); + + day.filterByGroup(subgroup, customSubjectsByDay); + }); + } + + private void deleteSubjectsCollidingWithCustomFilters (List customSubjectsDetails, + DayOfWeekDTO day) { + for (CustomSubjectDetails customSubjectDetail : customSubjectsDetails) { + customSubjectDetail.getSubject().deleteTypeAndUnnecessaryCharactersFromName(); + + day.setEven( + day + .getEven() + .stream() + .filter( + subject -> !(subject + .getName() + .contains(customSubjectDetail.getSubject().getName()) + && subjectsAreSameType(subject, customSubjectDetail)) + ).toList()); + + day.setOdd(day + .getOdd() + .stream() + .filter( + subject -> !(subject.getName().contains(customSubjectDetail.getSubject().getName()) + && subjectsAreSameType(subject, customSubjectDetail)) + ).toList()); + + } + } + + private boolean subjectsAreSameType (SubjectDTO subject, CustomSubjectDetails customSubjectDetails) { + var subjectType = TimetableParserService.extractSubjectTypeFromName(subject.getName()); + var customSubjectType = TimetableParserService.extractSubjectTypeFromName( + customSubjectDetails.getSubGroup()); + return subjectType.equals(customSubjectType); + + } + + private void checkSubGroupAvailability (String generalGroupName, List subgroup) + throws JsonProcessingException { + //Check if specified subgroup is available for this generalGroup + var subgroups = getAvailableSubGroups(generalGroupName); + for (var group : subgroup) { + if (!subgroups.contains(group)) { + throw new SpecifiedSubGroupDoesntExistsException(group); + } + } + } + public List getGeneralGroupList () throws WebPageContentNotAvailableException { return cachedService.getGeneralGroupsMap().keySet().stream().sorted().collect(Collectors.toList()); } diff --git a/src/main/java/org/pkwmtt/timetable/dto/CustomSubjectFilterDTO.java b/src/main/java/org/pkwmtt/timetable/dto/CustomSubjectFilterDTO.java new file mode 100644 index 0000000..e024dc7 --- /dev/null +++ b/src/main/java/org/pkwmtt/timetable/dto/CustomSubjectFilterDTO.java @@ -0,0 +1,14 @@ +package org.pkwmtt.timetable.dto; + +import lombok.Data; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Data +@RequiredArgsConstructor +@Getter +public class CustomSubjectFilterDTO { + private final String name; + private final String generalGroup; + private final String subGroup; +} diff --git a/src/main/java/org/pkwmtt/timetable/dto/DayOfWeekDTO.java b/src/main/java/org/pkwmtt/timetable/dto/DayOfWeekDTO.java index 53d581b..4770bdd 100644 --- a/src/main/java/org/pkwmtt/timetable/dto/DayOfWeekDTO.java +++ b/src/main/java/org/pkwmtt/timetable/dto/DayOfWeekDTO.java @@ -1,16 +1,26 @@ package org.pkwmtt.timetable.dto; import lombok.Data; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.pkwmtt.timetable.enums.TypeOfWeek; +import org.pkwmtt.timetable.objects.CustomSubjectDetails; +import org.springframework.data.util.Pair; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.regex.Pattern; +import java.util.stream.Collectors; +@Slf4j @Data public class DayOfWeekDTO { private final String name; + @Setter private List odd; + @Setter private List even; public DayOfWeekDTO (String name) { @@ -19,16 +29,19 @@ public DayOfWeekDTO (String name) { even = new ArrayList<>(); } - - public void add (SubjectDTO subjectDTO, boolean isNotOdd) { - if (isNotOdd) { - even.add(subjectDTO); - } else { - odd.add(subjectDTO); + /** + * Add subject by week Type + * + * @param subjectDTO - subject + * @param typeOfWeek - type of week + */ + public void add (SubjectDTO subjectDTO, TypeOfWeek typeOfWeek) { + switch (typeOfWeek) { + case EVEN -> this.even.add(subjectDTO); + case ODD -> this.odd.add(subjectDTO); } } - public void deleteSubjectTypesFromNames () { even.forEach(SubjectDTO::deleteTypeAndUnnecessaryCharactersFromName); odd.forEach(SubjectDTO::deleteTypeAndUnnecessaryCharactersFromName); @@ -44,21 +57,46 @@ public void deleteSubjectTypesFromNames () { * and the last character is the subgroup number */ public void filterByGroup (String group) { + var groupCharAndTargetNumber = getGroupCharAndTargetNumber(group); + + // Apply the filter to both odd- and even-week lists + odd = filter(odd, groupCharAndTargetNumber.getFirst(), groupCharAndTargetNumber.getSecond()); + even = filter(even, groupCharAndTargetNumber.getFirst(), groupCharAndTargetNumber.getSecond()); + + } + + public void filterByGroup (String group, List customSubjects) { + var groupCharAndTargetNumber = getGroupCharAndTargetNumber(group); + + // Apply the filter to both odd- and even-week lists + odd = filter( + odd, groupCharAndTargetNumber.getFirst(), groupCharAndTargetNumber.getSecond(), customSubjects + .stream() + .filter(customSubject -> customSubject.getTypeOfWeek().equals(TypeOfWeek.ODD)) + .toList() + ); + + even = filter( + even, groupCharAndTargetNumber.getFirst(), groupCharAndTargetNumber.getSecond(), customSubjects + .stream() + .filter(customSubject -> customSubject.getTypeOfWeek().equals(TypeOfWeek.EVEN)) + .toList() + ); + + } + + private Pair getGroupCharAndTargetNumber (String group) { // Delete first character if group starts 'G' if (group.charAt(0) == 'G' && group.length() > 3) { group = group.substring(1); } // Extract the group letter (e.g., "K" from "K03") - var groupName = String.valueOf(group.charAt(0)); + var groupChar = String.valueOf(group.charAt(0)); // Extract the subgroup digit (e.g., "3" from "K03") var targetNumber = String.valueOf(group.charAt(group.length() - 1)); - - // Apply the filter to both odd- and even-week lists - odd = filter(odd, groupName, targetNumber); - even = filter(even, groupName, targetNumber); - + return Pair.of(groupChar, targetNumber); } /** @@ -71,11 +109,34 @@ public void filterByGroup (String group) { * @return a filtered list of SubjectDTO */ private List filter (List list, String groupName, String targetNumber) { + return list.stream().filter(item -> hasOnlyTargetGroup(item.getName(), groupName, targetNumber)).toList(); + } + + /** + * Filter by subgroup char and number + * + * @param list list of subjects for specific day + * @param groupName - name of subgroup + * @param targetNumber - number fo subgroup + * @param customSubjects - custom subjects added by user + * @return modified list of subjects + */ + private List filter (List list, + String groupName, + String targetNumber, + List customSubjects) { + + + list = list + .stream() + .filter(item -> hasOnlyTargetGroup(item.getName(), groupName, targetNumber)) // K04 -> usun K != 4 + .collect(Collectors.toList()); + + for (var customSubject : customSubjects) { + list.add(customSubject.getSubject().setCustom(true)); + } - list = list.stream() - // Keep only items that have no other subgroup codes - .filter(item -> hasOnlyTargetGroup(item.getName(), groupName, targetNumber)) - .toList(); + list.sort(Comparator.comparingInt(SubjectDTO::getRowId)); return list; } @@ -89,20 +150,13 @@ private List filter (List list, String groupName, String * @return true if no non-target subgroup codes are present */ private boolean hasOnlyTargetGroup (String element, String groupName, String targetNumber) { - var pattern = Pattern.compile(String.format( - "\\bG?[%s]0[1-9]\\b", - Pattern.quote(groupName) - )); + var pattern = Pattern.compile(String.format("\\bG?[%s]0[1-9]\\b", Pattern.quote(groupName))); var matcher = pattern.matcher(element); if (!matcher.find()) { return true; } - pattern = Pattern.compile(String.format( - "%s0%s", - Pattern.quote(groupName), - Pattern.quote(targetNumber) - )); + pattern = Pattern.compile(String.format("%s0%s", Pattern.quote(groupName), Pattern.quote(targetNumber))); matcher = pattern.matcher(element); return matcher.find(); } diff --git a/src/main/java/org/pkwmtt/timetable/dto/SubjectDTO.java b/src/main/java/org/pkwmtt/timetable/dto/SubjectDTO.java index 141c80a..97f7504 100644 --- a/src/main/java/org/pkwmtt/timetable/dto/SubjectDTO.java +++ b/src/main/java/org/pkwmtt/timetable/dto/SubjectDTO.java @@ -13,7 +13,7 @@ public class SubjectDTO { private String classroom; private int rowId; private SubjectType type; - + private Boolean custom = false; public void deleteTypeAndUnnecessaryCharactersFromName () { if (name.contains(" ")) { diff --git a/src/main/java/org/pkwmtt/timetable/enums/TypeOfWeek.java b/src/main/java/org/pkwmtt/timetable/enums/TypeOfWeek.java new file mode 100644 index 0000000..e09ea53 --- /dev/null +++ b/src/main/java/org/pkwmtt/timetable/enums/TypeOfWeek.java @@ -0,0 +1,5 @@ +package org.pkwmtt.timetable.enums; + +public enum TypeOfWeek { + ODD, EVEN, BOTH +} diff --git a/src/main/java/org/pkwmtt/timetable/objects/CustomSubjectDetails.java b/src/main/java/org/pkwmtt/timetable/objects/CustomSubjectDetails.java new file mode 100644 index 0000000..8f5afef --- /dev/null +++ b/src/main/java/org/pkwmtt/timetable/objects/CustomSubjectDetails.java @@ -0,0 +1,16 @@ +package org.pkwmtt.timetable.objects; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.pkwmtt.timetable.dto.SubjectDTO; +import org.pkwmtt.timetable.enums.TypeOfWeek; + +@Getter +@AllArgsConstructor +public class CustomSubjectDetails { + SubjectDTO subject; + String subGroup; + int dayOfWeekNumber; + TypeOfWeek typeOfWeek; + +} diff --git a/src/main/java/org/pkwmtt/timetable/parser/TimetableParserService.java b/src/main/java/org/pkwmtt/timetable/parser/TimetableParserService.java index 501a9e8..ee060b1 100644 --- a/src/main/java/org/pkwmtt/timetable/parser/TimetableParserService.java +++ b/src/main/java/org/pkwmtt/timetable/parser/TimetableParserService.java @@ -8,6 +8,7 @@ import org.pkwmtt.timetable.dto.DayOfWeekDTO; import org.pkwmtt.timetable.dto.SubjectDTO; import org.pkwmtt.examCalendar.enums.SubjectType; +import org.pkwmtt.timetable.enums.TypeOfWeek; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -20,47 +21,48 @@ @Slf4j @Service public class TimetableParserService { - + /** * Alters html code for it to fit parsing process * * @param html webpage content * @return altered html code */ - private String clean(String html) { + private String clean (String html) { return html.replaceAll("
", " ") - .replaceAll(Pattern.quote("-(N"), "-(N") - .replaceAll(Pattern.quote("-(P"), "-(P") - .replaceAll(Pattern.quote("-(p"), "-(p") - .replaceAll(Pattern.quote("-(n"), "-(n") - .replaceAll(Pattern.quote(""), "") - .replaceAll(Pattern.quote(""), "") - .replaceAll(Pattern.quote("J ang"), "J_ang") - .replaceAll(Pattern.quote("J niemiecki"), "J_niemiecki") - .replaceAll(Pattern.quote("WF hala ("), "WF_hala_(") - .replaceAll(Pattern.quote(" "), ""); - + .replaceAll(Pattern.quote("-(N"), "-(N") + .replaceAll(Pattern.quote("-(P"), "-(P") + .replaceAll(Pattern.quote("-(p"), "-(p") + .replaceAll(Pattern.quote("-(n"), "-(n") + .replaceAll(Pattern.quote(""), "") + .replaceAll(Pattern.quote(""), "") + .replaceAll(Pattern.quote("J ang"), "J_ang") + .replaceAll(Pattern.quote("J niemiecki"), "J_niemiecki") + .replaceAll(Pattern.quote("WF hala ("), "WF_hala_(") + .replaceAll(Pattern.quote(" "), ""); + } - + /** * Extrude hours list from webpage * * @param html subpage of any general group * @return List of hours: List of Strings */ - public List parseHours(String html) { + public List parseHours (String html) { //Parse html code to Document object (allows to query elements) Document document = Jsoup.parse(clean(html)); - + List hours = new ArrayList<>(); - - for (Element item : document.select("td.g")) + + for (Element item : document.select("td.g")) { hours.add(item.text()); - + } + return hours; } - + /** * Parse webpage html to map of general groups containing name of a group and link * to subpage with its timetable @@ -68,58 +70,59 @@ public List parseHours(String html) { * @param html .../list containing list of general groups * @return map of general groups in format [GroupName: URL] */ - public Map parseGeneralGroups(String html) { + public Map parseGeneralGroups (String html) { Document document = Jsoup.parse(html); - + Map generalGroups = new HashMap<>(); - - for (Element item : document.select("#oddzialy .el a")) + + for (Element item : document.select("#oddzialy .el a")) { generalGroups.put(item.text(), item.attr("href")); - + } + return generalGroups; } - + /** * Parse html of specific General Group webpage to lists of subjects * * @param html of general group webpage * @return list of subjects sorted by day and odd or even type */ - public List parse(String html) { + public List parse (String html) { Document document = Jsoup.parse(clean(html)); Elements rows = extractRows(document); - + List days = parseHeaders(rows); - + //Remove header row rows.removeFirst(); - + //Go every row for (int rowId = 0; rowId < rows.size(); rowId++) { Element row = rows.get(rowId); Elements cell = row.select("td.l"); - + //Go every cell in a row for (int columnId = 0; columnId < cell.size(); columnId++) { Elements items = getValidItems(cell.get(columnId)); - + //Go every item in column for (int itemId = 0; itemId < items.size() - 1; itemId += 2) { boolean notOdd; String name = items.get(itemId).text(); String classroom = items.get(itemId + 1).text(); - + notOdd = isNameNotOdd(name); - + SubjectDTO subject = buildSubject(name, classroom, rowId); - - days.get(columnId).add(subject, notOdd); + + days.get(columnId).add(subject, notOdd ? TypeOfWeek.EVEN : TypeOfWeek.ODD); } } } return days; } - + /** * Cleans names from unnecessary and unwanted characters * @@ -128,78 +131,87 @@ public List parse(String html) { * @param rowId timetable row id * @return subject with cleaned data */ - private SubjectDTO buildSubject(String rawName, String rawClassroom, int rowId) { + private SubjectDTO buildSubject (String rawName, String rawClassroom, int rowId) { String name = cleanSubjectName(rawName); String classroom = cleanClassroomName(rawClassroom); SubjectType type = extractSubjectTypeFromName(name); - + return new SubjectDTO() - .setName(name) - .setClassroom(classroom) - .setRowId(rowId) - .setType(type); + .setName(name) + .setClassroom(classroom) + .setRowId(rowId) + .setType(type); } - + /** * Finds items containing data in cell * * @param cell from timetable * @return items from cell */ - private Elements getValidItems(Element cell) { + private Elements getValidItems (Element cell) { Elements items = cell.select("span"); items.removeIf(item -> item.text().contains("#") || item.text().length() == 2); return items; } - + /** * Extracts subject type from its name * * @param name subject name * @return subject type or empty string if there isn't any specified */ - private SubjectType extractSubjectTypeFromName(String name) { + public static SubjectType extractSubjectTypeFromName (String name) { name = name.trim(); - if (name.endsWith("W")) return SubjectType.LECTURE; - if (name.endsWith("S")) return SubjectType.SEMINAR; - if (name.endsWith("Ć")) return SubjectType.EXERCISES; - + if (name.endsWith("W")) { + return SubjectType.LECTURE; + } + if (name.endsWith("S")) { + return SubjectType.SEMINAR; + } + if (name.endsWith("Ć")) { + return SubjectType.EXERCISES; + } + Pattern laboratoryPattern = Pattern.compile("(? parseHeaders(Elements rows) { + private List parseHeaders (Elements rows) { List days = new ArrayList<>(); Elements headers = rows.getFirst().select("th"); for (int i = 2; i < headers.size(); i++) { @@ -207,22 +219,24 @@ private List parseHeaders(Elements rows) { } return days; } - - private String cleanClassroomName(String text) { - if (text.contains("-p")) + + private String cleanClassroomName (String text) { + if (text.contains("-p")) { return text.replace("-p", ""); - if (text.contains("-n")) + } + if (text.contains("-n")) { return text.replace("-n", ""); + } return text; } - + /** * Deletes all unnecessary characters in subject name * * @param text subject name * @return cleaned name */ - private String cleanSubjectName(String text) { + private String cleanSubjectName (String text) { text = text.replaceAll("-", ""); text = deleteEvenMark(text); text = deleteOddMark(text); @@ -230,39 +244,43 @@ private String cleanSubjectName(String text) { text = text.replaceAll(Pattern.quote("."), ""); return text; } - + /** * Deletes marks of odd day * * @param text subject name * @return altered text */ - private String deleteEvenMark(String text) { - if (text.contains("(P")) + private String deleteEvenMark (String text) { + if (text.contains("(P")) { return text.replace("(P", ""); - if (text.contains("(p")) + } + if (text.contains("(p")) { return text.replace("(p", ""); - + } + return text; } - + /** * Deletes marks of even day * * @param text subject name * @return altered text */ - private String deleteOddMark(String text) { - + private String deleteOddMark (String text) { + text = text.replaceAll("-", ""); - - if (text.contains("(N")) + + if (text.contains("(N")) { return text.replace("(N", ""); - if (text.contains("(n")) + } + if (text.contains("(n")) { return text.replace("(n", ""); + } return text; } - + /** * Checks if subjects name isn't odd * @@ -270,7 +288,7 @@ private String deleteOddMark(String text) { * @return true if subject isn't odd and * false if subject is odd */ - private boolean isNameNotOdd(String name) { + private boolean isNameNotOdd (String name) { return !name.contains("(N") && !name.contains("-(n"); } } \ No newline at end of file diff --git a/src/test/java/org/pkwmtt/timetable/TimetableCacheServiceTest.java b/src/test/java/org/pkwmtt/timetable/TimetableCacheServiceTest.java index c4c80a3..598ea30 100644 --- a/src/test/java/org/pkwmtt/timetable/TimetableCacheServiceTest.java +++ b/src/test/java/org/pkwmtt/timetable/TimetableCacheServiceTest.java @@ -40,7 +40,7 @@ public void initWireMock () { } @Test - @Disabled("hard coded values") + @Disabled("Values for hours are hard coded in endpoint for now") public void shouldHourListBePresentInCache () { //given var key = "hourList"; diff --git a/src/test/java/org/pkwmtt/timetable/TimetableControllerTest.java b/src/test/java/org/pkwmtt/timetable/TimetableControllerTest.java index 2636a70..1c4d195 100644 --- a/src/test/java/org/pkwmtt/timetable/TimetableControllerTest.java +++ b/src/test/java/org/pkwmtt/timetable/TimetableControllerTest.java @@ -4,15 +4,16 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.pkwmtt.ValuesForTest; +import org.pkwmtt.examCalendar.enums.SubjectType; import org.pkwmtt.exceptions.dto.ErrorResponseDTO; +import org.pkwmtt.timetable.dto.CustomSubjectFilterDTO; +import org.pkwmtt.timetable.dto.SubjectDTO; import org.pkwmtt.timetable.dto.TimetableDTO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; import test.TestConfig; import java.util.Arrays; @@ -22,7 +23,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.*; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.*; @Slf4j @@ -33,27 +33,28 @@ class TimetableControllerTest extends TestConfig { @Autowired private TestRestTemplate restTemplate; - + @BeforeEach - public void initWireMock() { + public void initWireMock () { EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/plany/o25.html")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "text/*") - .withBody(ValuesForTest.timetableHTML))); - + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/*") + .withBody(ValuesForTest.timetableHTML))); + EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/lista.html")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "text/*") - .withBody(ValuesForTest.listHTML))); + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/*") + .withBody(ValuesForTest.listHTML))); } @Test public void testGetGeneralGroupScheduleFiltered_withOptionalParams () { //given - var url = String.format("http://localhost:%s/pkwmtt/api/v1/timetables/12K1?sub=K01&sub=L01&sub=P01", - port + var url = String.format( + "http://localhost:%s/pkwmtt/api/v1/timetables/12K1?sub=K01&sub=L01&sub=P01", + port ); //when @@ -61,18 +62,75 @@ public void testGetGeneralGroupScheduleFiltered_withOptionalParams () { //then assertAll( - () -> assertEquals(HttpStatus.OK, response.getStatusCode()), - () -> { - var responseBody = response.getBody(); - assertNotNull(responseBody); - }, - () -> { - assertNotNull(response.getBody()); - var responseData = response.getBody().getData(); - assertEquals(5, responseData.size()); - assertEquals(12, responseData.getFirst().getOdd().size()); - assertEquals(6, responseData.getFirst().getEven().size()); - } + () -> assertEquals(HttpStatus.OK, response.getStatusCode()), + () -> { + var responseBody = response.getBody(); + assertNotNull(responseBody); + }, + () -> { + assertNotNull(response.getBody()); + var responseData = response.getBody().getData(); + assertEquals(5, responseData.size()); + assertEquals(12, responseData.getFirst().getOdd().size()); + assertEquals(6, responseData.getFirst().getEven().size()); + } + ); + } + + @Test + public void testGetGeneralGroupScheduleFiltered_withOptionalParamsAndCustomSubjectsForSameGeneralGroup () { + //given + var url = String.format( + "http://localhost:%s/pkwmtt/api/v1/timetables/12K1?sub=K01&sub=L01&sub=P01", + port + ); + List payload = List.of(new CustomSubjectFilterDTO("PKM", "12K1", "K04")); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + var expectedObject = new SubjectDTO() + .setName("PKM") + .setType(SubjectType.COMPUTER_LABORATORY) + .setClassroom("A227") + .setRowId(8) + .setCustom(true); + + //when + + HttpEntity> request = new HttpEntity<>(payload, headers); + ResponseEntity response = restTemplate.postForEntity( + url, + request, + TimetableDTO.class + ); + //then + assertAll( + () -> assertEquals(HttpStatus.OK, response.getStatusCode()), + () -> { + var responseBody = response.getBody(); + assertNotNull(responseBody); + }, + () -> { + assertNotNull(response.getBody()); + var responseData = response.getBody().getData(); + var subject_Monday_Nr10_Odd_Row8 = responseData + .getFirst() + .getOdd() + .stream() + .filter(item -> item.getRowId() == 8).toList().getFirst(); + var subject_Monday_Nr11_Odd_Row9 = responseData + .getFirst() + .getOdd() + .stream() + .filter(item -> item.getRowId() == 9).toList().getFirst(); + assertEquals(subject_Monday_Nr10_Odd_Row8, expectedObject); + assertEquals(subject_Monday_Nr11_Odd_Row9, expectedObject.setRowId(9)); + var subject_Thursday_Nr3_Odd_Row2List = responseData + .get(3) + .getOdd() + .stream() + .filter(item -> item.getRowId() == 2).toList(); + assertEquals(0, subject_Thursday_Nr3_Odd_Row2List.size()); + } ); } @@ -181,7 +239,11 @@ public void shouldReturn_ListOfHours () { Pattern pattern = Pattern.compile(regex); Arrays.stream(response.getBody()).toList().forEach(item -> { Matcher matcher = pattern.matcher(item); - if(!matcher.find()) fail("Wrong hour format"); + if (!matcher.find()) { + fail("Wrong hour format"); + } }); } + + } \ No newline at end of file diff --git a/src/test/java/org/pkwmtt/timetable/TimetableServiceTest.java b/src/test/java/org/pkwmtt/timetable/TimetableServiceTest.java index 357ccf2..09b53d7 100644 --- a/src/test/java/org/pkwmtt/timetable/TimetableServiceTest.java +++ b/src/test/java/org/pkwmtt/timetable/TimetableServiceTest.java @@ -1,11 +1,5 @@ package org.pkwmtt.timetable; -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; - import com.fasterxml.jackson.core.JsonProcessingException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -14,39 +8,45 @@ import org.pkwmtt.exceptions.SpecifiedSubGroupDoesntExistsException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import test.TestConfig; +import java.util.ArrayList; import java.util.List; -import java.util.regex.Pattern; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; @SpringBootTest class TimetableServiceTest extends TestConfig { @Autowired private TimetableService service; - + @BeforeEach - public void initWireMock() { - EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/plany/o25.html")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "text/*") - .withBody(ValuesForTest.timetableHTML))); - - EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/lista.html")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "text/*") - .withBody(ValuesForTest.listHTML))); + public void initWireMock () { + EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/plany/o25.html")).willReturn(aResponse() + .withStatus(200) + .withHeader( + "Content-Type", + "text/*" + ) + .withBody( + ValuesForTest.timetableHTML))); + + EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/lista.html")).willReturn(aResponse() + .withStatus(200) + .withHeader( + "Content-Type", + "text/*" + ) + .withBody(ValuesForTest.listHTML))); } @Test public void shouldReturnAvailableSubGroups () throws JsonProcessingException { //given var generalGroupName = "12K1"; - var regex = "^[A-Z]\\d{2}$"; - var pattern = Pattern.compile(regex); var expectedResult = List.of("K01", "K04", "L01", "L02", "L04", "P01", "P04"); //when @@ -54,14 +54,7 @@ public void shouldReturnAvailableSubGroups () throws JsonProcessingException { //then assertThat(result).isEqualTo(expectedResult); - - //I don't know why it is in test. I think it is for debug? -// result.forEach(item -> { -// Matcher matcher = pattern.matcher(item); -// if (!matcher.find()) { -// fail("Wrong subgroup format"); -// } -// }); + } @@ -74,8 +67,8 @@ public void shouldThrow_SpecifiedGeneralGroupDoesntExistsException () { //then assertThrows( - SpecifiedGeneralGroupDoesntExistsException.class, - () -> service.getFilteredGeneralGroupSchedule(generalGroupName, subgroups) + SpecifiedGeneralGroupDoesntExistsException.class, + () -> service.getFilteredGeneralGroupSchedule(generalGroupName, subgroups, new ArrayList<>()) ); } @@ -88,29 +81,25 @@ public void shouldThrow_SpecifiedSubGroupDoesntExistsException () { //then assertThrows( - SpecifiedSubGroupDoesntExistsException.class, - () -> service.getFilteredGeneralGroupSchedule(generalGroupName, subgroups) + SpecifiedSubGroupDoesntExistsException.class, + () -> service.getFilteredGeneralGroupSchedule(generalGroupName, subgroups, new ArrayList<>()) ); } @Test public void shouldReturnSortedGeneralGroupList () { //given - var expectedResult = List.of( - "11A1", - "11K2", - "12K1", - "12K2", - "12K3" - ); + var expectedResult = List.of("11A1", "11K2", "12K1", "12K2", "12K3"); //when var result = service.getGeneralGroupList(); - + //then assertAll( - () -> assertNotNull(result), - () -> assertFalse(result.isEmpty()), - () -> assertEquals(expectedResult, result) + () -> assertNotNull(result), + () -> assertFalse(result.isEmpty()), + () -> assertEquals(expectedResult, result) ); } + + } \ No newline at end of file