Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,9 @@ public ResponseEntity<ErrorResponseDTO> handleConstraintViolationException(Const
.collect(Collectors.joining(", "));
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponseDTO(message));
}

@ExceptionHandler(ResourceAlreadyExistsException.class)
public ResponseEntity<ErrorResponseDTO> handleConflicts(RuntimeException e) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(new ErrorResponseDTO(e.getMessage()));
}
}
213 changes: 125 additions & 88 deletions src/main/java/org/pkwmtt/examCalendar/ExamService.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,25 +36,27 @@ public int addExam(ExamDto examDto) {

Set<StudentGroup> groups = verifyAndUpdateExamGroups(examDto);

// check if exam type exists
ExamType examType = examTypeRepository.findByName(examDto.getExamType())
.orElseThrow(() -> new ExamTypeNotExistsException(examDto.getExamType()));

// save exam in repository and return id of created exam
return examRepository.save(ExamDtoMapper.mapToNewExam(examDto, groups, examType)).getExamId();
Exam exam = ExamDtoMapper.mapToNewExam(examDto, groups, examType);
Set<Exam> existingExam = examRepository.findAllByTitle(exam.getTitle());

if (existingExam.contains(exam))
throw new ResourceAlreadyExistsException("Exam already exists");
return examRepository.save(exam).getExamId();
}

/**
* @param examDto new details of exam that overwrite old ones
* @param id of exam that need to be modified
*/
public void modifyExam(ExamDto examDto, int id) {
// check if exam which would be modified exists

examRepository.findById(id).orElseThrow(() -> new NoSuchElementWithProvidedIdException(id));

Set<StudentGroup> groups = verifyAndUpdateExamGroups(examDto);

// check if exam type exists
ExamType examType = examTypeRepository.findByName(examDto.getExamType())
.orElseThrow(() -> new ExamTypeNotExistsException(examDto.getExamType()));

Expand All @@ -77,32 +79,23 @@ public Exam getExamById(int id) {
return examRepository.findById(id).orElseThrow(() -> new NoSuchElementWithProvidedIdException(id));
}

/**
* @param generalGroups set of general groups from the same year of study
* e.g. 12K1, 12K2 are from 12K year of study,
* but 12A1 and 12B1 or 11A1 and 12A1 aren't from the same year
* @param subgroups subgroups that belong to provided general groups
* @return set of exams containing provided groups
*/
public Set<Exam> getExamByGroups(Set<String> generalGroups, Set<String> subgroups) {
// verify generalGroups identifiers

String superiorGroup = trimLastDigit(generalGroups);
verifyGeneralGroupsFormat(generalGroups);
// get exams for general groups
Set<Exam> exams = new HashSet<>(examRepository.findAllByGroups_NameIn(generalGroups));
exams = exams.stream()
.filter(exam -> exam.getGroups().stream()
.allMatch(group -> group.getName().matches("^\\d.*")))
.collect(Collectors.toSet());

// convert general group identifiers. e.g. 12K2 to 12K
Set<String> superiorGroups = generalGroups.stream().map(g -> {
if (Character.isDigit(g.charAt(g.length() - 1)))
return g.substring(0, g.length() - 1);
return g;
}).collect(Collectors.toSet());
// check if subgroups are provided
if (subgroups != null && !subgroups.isEmpty()) {
// verify subgroups identifiers
verifySubgroupsFormat(subgroups);
// check if superior group identifies the groups unambiguously
if (superiorGroups.size() != 1)
throw new InvalidGroupIdentifierException("ambiguous superior group identifier for subgroups");
exams.addAll(examRepository.findAllBySubgroupsOfGeneralGroup(superiorGroups.iterator().next(), subgroups));
}
return exams;
if(subgroups == null || subgroups.isEmpty())
return examRepository.findAllByGroups_NameIn(generalGroups);

verifySubgroupsFormat(subgroups);
return examRepository.findAllBySubgroupsOfSuperiorGroupAndGeneralGroup(superiorGroup, generalGroups, subgroups);
}

/**
Expand All @@ -112,87 +105,123 @@ public List<ExamType> getExamTypes() {
return examTypeRepository.findAll();
}


/**
* verify if groups exists in timetable if exist updates database.
* when timetable service is unavailable verifies groups using groupsRepository
*
* verify if groups exists and updates database when it exists, but repository doesn't contain it.
* When timetable service is unavailable verifies groups using groupsRepository
* @param examDto containing groups for verification
* @return single set of all kinds of provided groups as StudentGroup entities
* that are in database and could be safely attach to Exam entity
*/
private Set<StudentGroup> verifyAndUpdateExamGroups(ExamDto examDto) {
Set<String> generalGroupsFromRepository;
Set<String> generalGroups = examDto.getGeneralGroups();
Set<String> subgroups = examDto.getSubgroups();
// if timetable service is unavailable verify general groups using GroupRepository

if (generalGroups == null || generalGroups.isEmpty())
throw new InvalidGroupIdentifierException("general group is missing");

verifyGeneralGroups(generalGroups);

if (subgroups == null || subgroups.isEmpty())
return saveNewStudentGroups(generalGroups);

if (generalGroups.size() > 1)
throw new InvalidGroupIdentifierException("ambiguous general groups for subgroups");

String superiorGroup = generalGroups.iterator().next();
verifySubgroups(superiorGroup, subgroups);

subgroups.add(trimLastDigit(superiorGroup));
return saveNewStudentGroups(subgroups);
}

/**
* verifies provided generalGroups using timetable service or repository when service is unavailable
* @param generalGroups that would be verified
*/
private void verifyGeneralGroups(Set<String> generalGroups) {
try {
generalGroupsFromRepository = new HashSet<>(timetableService.getGeneralGroupList());
Set<String> existingGeneralGroups = new HashSet<>(timetableService.getGeneralGroupList());
if (!existingGeneralGroups.containsAll(generalGroups))
throw new InvalidGroupIdentifierException(existingGeneralGroups, generalGroups);
} catch (WebPageContentNotAvailableException e) {
generalGroupsFromRepository = verifyGroupsUsingRepository(generalGroups);
}
// verify generalGroups using timetable service
if (!generalGroupsFromRepository.containsAll(generalGroups)) {
generalGroups.removeAll(generalGroupsFromRepository);
throw new InvalidGroupIdentifierException(generalGroups);
verifyGeneralGroupsUsingRepository(generalGroups);
}
// if there are no subgroups save exam for exercise groups or whole year e.g.
// 12K2 - exercise group exam
// 12K1, 12K2, 12K3 - whole year exam
if (subgroups == null || subgroups.isEmpty()) {
return saveNewStudentGroups(generalGroups);
// exams for subgroups e.g. L04 must have only superior group to avoid ambiguity
} else if (generalGroups.size() == 1) {
// if there are only one group change it from Set<String> to String
String superiorGroup = generalGroups.iterator().next();
Set<String> subGroupsFromTimetable;
try {
subGroupsFromTimetable = new HashSet<>(timetableService.getAvailableSubGroups(superiorGroup));
} catch (JsonProcessingException |
SpecifiedGeneralGroupDoesntExistsException |
WebPageContentNotAvailableException e) {
throw new ServiceNotAvailableException("Couldn't verify groups using timetable service");
// TODO: add verification with repository when timetable service is unavailable
}
// verify if subgroups for specific general group exists
if (!subGroupsFromTimetable.containsAll(subgroups)) {
subgroups.removeAll(subGroupsFromTimetable);
throw new InvalidGroupIdentifierException(subgroups);
}
// change superior group format e.g. 12K2 to 12K
if (Character.isDigit(superiorGroup.charAt(superiorGroup.length() - 1)))
superiorGroup = superiorGroup.substring(0, superiorGroup.length() - 1);
// save subgroups with superior group identifier
subgroups.add(superiorGroup);
return saveNewStudentGroups(subgroups);
}
// only one general group could be assigned to subgroups (when there are more than 1 general group and
// more than 0 subgroups)
else if (generalGroups.isEmpty())
throw new InvalidGroupIdentifierException("general group is missing");
else
throw new InvalidGroupIdentifierException("ambiguous general groups for subgroups");
}

/**
* @param groups groups that would be verified using repository
* @return set of groups (String) when verification succeeded
* @throws WebPageContentNotAvailableException when verification not succeeded
* @param groups that would be verified using repository
* @throws ServiceNotAvailableException when verification not succeeded
*/
private Set<String> verifyGroupsUsingRepository(Set<String> groups) throws WebPageContentNotAvailableException {
private void verifyGeneralGroupsUsingRepository(Set<String> groups) throws ServiceNotAvailableException {
verifyGeneralGroupsFormat(groups);
Set<String> groupsFromRepository = groupRepository.findAllByNameIn(groups).stream()
.map(StudentGroup::getName)
.collect(Collectors.toSet()
);
if (groupsFromRepository.containsAll(groups))
return groups;
else
throw new ServiceNotAvailableException("Couldn't verify groups using repository");
if (!groupsFromRepository.containsAll(groups))
throw new ServiceNotAvailableException("Timetable service unavailable, couldn't verify groups using repository");
}

/**
* verifies provided subgroups using timetable service or repository when service is unavailable
* @param superiorGroup of provided subgroups
* @param subgroups that would be verified
*/
private void verifySubgroups(String superiorGroup, Set<String> subgroups){
try {
Set<String> subGroupsFromTimetable = new HashSet<>(timetableService.getAvailableSubGroups(superiorGroup));
if (!subGroupsFromTimetable.containsAll(subgroups))
throw new InvalidGroupIdentifierException(subGroupsFromTimetable, subgroups);
} catch (JsonProcessingException |
SpecifiedGeneralGroupDoesntExistsException |
WebPageContentNotAvailableException e) {
verifySubgroupsUsingRepository(superiorGroup, subgroups);
}
}

/**
* @param superiorGroup of provided subgroups
* @param groups subgroups for verification
* @throws ServiceNotAvailableException when verification not succeeded
*/
private void verifySubgroupsUsingRepository(String superiorGroup, Set<String> groups) throws ServiceNotAvailableException {
groups.add(trimLastDigit(superiorGroup));
if(examRepository.findCommonExamIdsForGroups(groups, groups.size()).isEmpty())
throw new ServiceNotAvailableException("Timetable service unavailable, couldn't verify groups using repository");
}

/**
* extract superior group form general group e.g. 12K2 -> 12K
* @param generalGroup group for transformation
* @return superior group
*/
private static String trimLastDigit(String generalGroup) {
char lastChar = generalGroup.charAt(generalGroup.length() - 1);
if (Character.isDigit(lastChar))
generalGroup = generalGroup.substring(0, generalGroup.length() - 1);
return generalGroup;
}

/**
* extract common superior group form provided general groups e.g. 12K2 -> 12K
* @param superiorGroups set of general groups from the same year of study
* @return single superior group of provided general groups
* @throws InvalidGroupIdentifierException when not all provided groups belong to the same year of study
*/
private static String trimLastDigit(Set<String> superiorGroups) throws InvalidGroupIdentifierException {
Set<String> trimmedGroups = superiorGroups.stream()
.map(ExamService::trimLastDigit)
.collect(Collectors.toSet());
if(trimmedGroups.size() > 1)
throw new InvalidGroupIdentifierException("ambiguous general groups for subgroups");
return trimmedGroups.iterator().next();
}

/**
* saves groups to groupRepository, existing group names are filtered out before saving
*
* @param groups groups that would be saved to repository
* @return set of StudentsGroup Entity with names from groups.
* @return set of StudentsGroup Entities with provided names
*/
private Set<StudentGroup> saveNewStudentGroups(Set<String> groups) {
// remove duplicates before saving records
Expand All @@ -211,17 +240,25 @@ private Set<StudentGroup> saveNewStudentGroups(Set<String> groups) {
return existingGroups;
}

/**
* @param generalGroups general groups for verification
* @throws SpecifiedGeneralGroupDoesntExistsException when format is invalid
*/
private static void verifyGeneralGroupsFormat(Set<String> generalGroups) throws SpecifiedGeneralGroupDoesntExistsException {
generalGroups.forEach(group -> {
if (!group.matches("^\\d.*"))
throw new SpecifiedGeneralGroupDoesntExistsException(group);
});
}

private static void verifySubgroupsFormat(Set<String> subgroups) {
/**
* @param subgroups subgroups for verification
* @throws SpecifiedSubGroupDoesntExistsException when format is invalid
*/
private static void verifySubgroupsFormat(Set<String> subgroups) throws SpecifiedSubGroupDoesntExistsException {
subgroups.forEach(group -> {
if (!group.matches("^[A-Z].*"))
throw new SpecifiedSubGroupDoesntExistsException(group);
});
}
}
}
10 changes: 9 additions & 1 deletion src/main/java/org/pkwmtt/examCalendar/mapper/ExamDtoMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import java.util.stream.Collectors;

/**
* maps ExamDto to Exam entity. Couldn't be utility class, because needs ExamTypeRepository to validate exam types
* Utility class for mapping Exam entity to ExamDto and ExamDto to Exam entity
*/
@Slf4j
public class ExamDtoMapper {
Expand Down Expand Up @@ -51,10 +51,18 @@ public static Exam mapToExistingExam(ExamDto examDto, Set<StudentGroup> groups,
.build();
}

/**
* @param exams Set of Exams that would be mapped
* @return List of ExamDtos
*/
public static List<ExamDto> mapToExamDto(Set<Exam> exams) {
return exams.stream().map(ExamDtoMapper::mapToExamDto).collect(Collectors.toList());
}

/**
* @param exam single exam that would be mapped
* @return ExamDto
*/
public static ExamDto mapToExamDto(Exam exam) {
Set<String> groups = exam.getGroups().stream().map(StudentGroup::getName).collect(Collectors.toSet());
Set<String> generalGroups = groups.stream().filter(group -> Character.isDigit(group.charAt(0))).collect(Collectors.toSet());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,52 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Set;

public interface ExamRepository extends JpaRepository<Exam, Integer> {

Set<Exam> findAllByTitle(String title);


/**
* @param groups set of generalGroups
* @return list of exams for generalGroups
* @return set of exams for generalGroups
*/
List<Exam> findAllByGroups_NameIn(Set<String> groups);
Set<Exam> findAllByGroups_NameIn(Set<String> groups);


/**
* @param generalGroup superior group of subgroups e.g. 12K
* @param subgroup exam groups
* @return list of exams for subgroups
* @param superiorGroup group that identifies whole year of study e.g. 12K
* @param generalGroup set of exercise groups e.g. 12K2
* @param subgroup set of subgroups of provided superior group e.g. L04
* @return set of exams containing generalGroups or superiorGroup with at least one provided subgroup
*/
@Query("""
SELECT DISTINCT e FROM Exam e
JOIN e.groups g1
JOIN FETCH e.groups g2
WHERE g1.name = :general AND g2.name IN :sub
WHERE (g1.name = :superior AND g2.name IN :sub)
OR g2.name IN :general
""")
Set<Exam> findAllBySubgroupsOfGeneralGroup(@Param("general") String generalGroup, @Param("sub") Set<String> subgroup);
Set<Exam> findAllBySubgroupsOfSuperiorGroupAndGeneralGroup(
@Param("superior") String superiorGroup,
@Param("general") Set<String> generalGroup,
@Param("sub") Set<String> subgroup);


/**
* Method could be used to check if provided subgroups belong to superior group by finding existing exam
* related to provided groups in repository
* @param groups set of subgroups with one superior group of provided subgroups e.g. 12K2, K04, K05
* @param expectedSize size of provided groups set
* @return set of ids of exams that contains all provided groups
*/
@Query(value = """
SELECT exam_id FROM exams_groups
INNER JOIN student_groups ON exams_groups.group_id = student_groups.group_id
WHERE student_groups.name IN (:groups)
GROUP BY exam_id
HAVING COUNT(DISTINCT exams_groups.group_id) = :expectedSize
""", nativeQuery = true)
Set<Integer> findCommonExamIdsForGroups(@Param("groups") Set<String> groups, @Param("expectedSize") int expectedSize);
}
Loading