diff --git a/src/main/java/edu/cmipt/gcs/constant/ApiPathConstant.java b/src/main/java/edu/cmipt/gcs/constant/ApiPathConstant.java index 4e81599..8bc2d0d 100644 --- a/src/main/java/edu/cmipt/gcs/constant/ApiPathConstant.java +++ b/src/main/java/edu/cmipt/gcs/constant/ApiPathConstant.java @@ -26,4 +26,6 @@ public class ApiPathConstant { public static final String USER_CHECK_USERNAME_VALIDITY_API_PATH = USER_API_PREFIX + "/username"; public static final String USER_DELETE_USER_API_PATH = USER_API_PREFIX + "/delete"; + public static final String USER_PAGE_USER_REPOSITORY_API_PATH = + USER_API_PREFIX + "/page/repository"; } diff --git a/src/main/java/edu/cmipt/gcs/constant/ValidationConstant.java b/src/main/java/edu/cmipt/gcs/constant/ValidationConstant.java index fc17adc..a1332c7 100644 --- a/src/main/java/edu/cmipt/gcs/constant/ValidationConstant.java +++ b/src/main/java/edu/cmipt/gcs/constant/ValidationConstant.java @@ -9,4 +9,10 @@ public class ValidationConstant { // so we just use '*' to ignore the length check public static final String USERNAME_PATTERN = "^[a-zA-Z0-9_]*$"; public static final String PASSWORD_PATTERN = "^[a-zA-Z0-9_.@]*$"; + public static final int MIN_REPOSITORY_NAME_LENGTH = 1; + public static final int MAX_REPOSITORY_NAME_LENGTH = 255; + public static final int MIN_REPOSITORY_DESCRIPTION_LENGTH = 0; + public static final int MAX_REPOSITORY_DESCRIPTION_LENGTH = 255; + // the length will be checked by @Size + public static final String REPOSITORY_NAME_PATTERN = "^[a-zA-Z0-9_-]*$"; } diff --git a/src/main/java/edu/cmipt/gcs/controller/UserController.java b/src/main/java/edu/cmipt/gcs/controller/UserController.java index 73154db..a083dfc 100644 --- a/src/main/java/edu/cmipt/gcs/controller/UserController.java +++ b/src/main/java/edu/cmipt/gcs/controller/UserController.java @@ -1,6 +1,7 @@ package edu.cmipt.gcs.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import edu.cmipt.gcs.constant.ApiPathConstant; import edu.cmipt.gcs.constant.HeaderParameter; @@ -8,9 +9,12 @@ import edu.cmipt.gcs.enumeration.ErrorCodeEnum; import edu.cmipt.gcs.exception.GenericException; import edu.cmipt.gcs.pojo.error.ErrorVO; +import edu.cmipt.gcs.pojo.repository.RepositoryPO; +import edu.cmipt.gcs.pojo.repository.RepositoryVO; import edu.cmipt.gcs.pojo.user.UserDTO; import edu.cmipt.gcs.pojo.user.UserPO; import edu.cmipt.gcs.pojo.user.UserVO; +import edu.cmipt.gcs.service.RepositoryService; import edu.cmipt.gcs.service.UserService; import edu.cmipt.gcs.util.JwtUtil; import edu.cmipt.gcs.validation.group.UpdateGroup; @@ -29,6 +33,10 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; @@ -46,6 +54,7 @@ @Tag(name = "User", description = "User Related APIs") public class UserController { @Autowired private UserService userService; + @Autowired private RepositoryService repositoryService; @GetMapping(ApiPathConstant.USER_GET_USER_API_PATH) @Operation( @@ -180,6 +189,56 @@ public void deleteUser( JwtUtil.blacklistToken(accessToken, refreshToken); } + @GetMapping(ApiPathConstant.USER_PAGE_USER_REPOSITORY_API_PATH) + @Operation( + summary = "Page user repositories", + description = "Page user repositories. If the given token is trying to get other's repositories, only public repositories will be shown", + tags = {"User", "Get Method"}) + @Parameters({ + @Parameter( + name = HeaderParameter.ACCESS_TOKEN, + description = "Access token", + required = true, + in = ParameterIn.HEADER, + schema = @Schema(implementation = String.class)), + @Parameter( + name = "id", + description = "User id", + required = true, + in = ParameterIn.QUERY, + schema = @Schema(implementation = Long.class)), + @Parameter( + name = "page", + description = "Page number", + example = "1", + required = true, + in = ParameterIn.QUERY, + schema = @Schema(implementation = Integer.class)), + @Parameter( + name = "size", + description = "Page size", + example = "10", + required = true, + in = ParameterIn.QUERY, + schema = @Schema(implementation = Integer.class)) + }) + @ApiResponse(responseCode = "200", description = "User repositories paged successfully") + public List pageUserRepositories( + @RequestParam("id") Long userId, + @RequestParam("page") Integer page, + @RequestParam("size") Integer size, + @RequestHeader(HeaderParameter.ACCESS_TOKEN) String accessToken) { + QueryWrapper wrapper = new QueryWrapper(); + String idInToken = JwtUtil.getID(accessToken); + assert idInToken != null; + if (!idInToken.equals(userId.toString())) { + // the user only can see the public repositories of others + wrapper.eq("is_private", false); + } + wrapper.eq("user_id", userId); + return repositoryService.page(new Page<>(page, size), wrapper).getRecords().stream().map(RepositoryVO::new).collect(Collectors.toList()); + } + @GetMapping(ApiPathConstant.USER_CHECK_EMAIL_VALIDITY_API_PATH) @Operation( summary = "Check email validity", diff --git a/src/main/java/edu/cmipt/gcs/dao/RepositoryMapper.java b/src/main/java/edu/cmipt/gcs/dao/RepositoryMapper.java new file mode 100644 index 0000000..f543bf0 --- /dev/null +++ b/src/main/java/edu/cmipt/gcs/dao/RepositoryMapper.java @@ -0,0 +1,7 @@ +package edu.cmipt.gcs.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import edu.cmipt.gcs.pojo.repository.RepositoryPO; + +public interface RepositoryMapper extends BaseMapper {} diff --git a/src/main/java/edu/cmipt/gcs/enumeration/ErrorCodeEnum.java b/src/main/java/edu/cmipt/gcs/enumeration/ErrorCodeEnum.java index 5bef6de..89d618a 100644 --- a/src/main/java/edu/cmipt/gcs/enumeration/ErrorCodeEnum.java +++ b/src/main/java/edu/cmipt/gcs/enumeration/ErrorCodeEnum.java @@ -32,7 +32,18 @@ public enum ErrorCodeEnum { USER_NOT_FOUND("USER_NOT_FOUND"), USER_UPDATE_FAILED("USER_UPDATE_FAILED"), - USER_DELETE_FAILED("USER_DELETE_FAILED"); + USER_DELETE_FAILED("USER_DELETE_FAILED"), + + REPOSITORYDTO_ID_NULL("RepositoryDTO.id.Null"), + REPOSITORYDTO_ID_NOTNULL("RepositoryDTO.id.NotNull"), + REPOSITORYDTO_REPOSITORYNAME_SIZE("RepositoryDTO.repositoryName.Size"), + REPOSITORYDTO_REPOSITORYNAME_NOTBLANK("RepositoryDTO.repositoryName.NotBlank"), + REPOSITORYDTO_REPOSITORYDESCRIPTION_SIZE("RepositoryDTO.repositoryDescription.Size"), + REPOSITORYDTO_STAR_MIN("RepositoryDTO.star.Min"), + REPOSITORYDTO_FORK_MIN("RepositoryDTO.fork.Min"), + REPOSITORYDTO_WATCHER_MIN("RepositoryDTO.watcher.Min"), + + REPOSITORYNAME_PATTERN_MISMATCH("REPOSITORYNAME_PATTERN_MISMATCH"); // code means the error code in the message.properties private String code; diff --git a/src/main/java/edu/cmipt/gcs/pojo/repository/RepositoryDTO.java b/src/main/java/edu/cmipt/gcs/pojo/repository/RepositoryDTO.java new file mode 100644 index 0000000..fba7891 --- /dev/null +++ b/src/main/java/edu/cmipt/gcs/pojo/repository/RepositoryDTO.java @@ -0,0 +1,57 @@ +package edu.cmipt.gcs.pojo.repository; + +import edu.cmipt.gcs.constant.ValidationConstant; +import edu.cmipt.gcs.validation.group.CreateGroup; +import edu.cmipt.gcs.validation.group.UpdateGroup; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Null; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +@Schema(description = "Repository Data Transfer Object") +public record RepositoryDTO( + @Schema(description = "Repository ID") + @Null(groups = CreateGroup.class, message = "REPOSITORYDTO_ID_NULL {RepositoryDTO.id.Null}") + @NotNull( + groups = UpdateGroup.class, + message = "REPOSITORYDTO_ID_NOTNULL {RepositoryDTO.id.NotNull}") + String id, + @Schema( + description = "Repository Name", + requiredMode = Schema.RequiredMode.REQUIRED, + example = "gcs") + @Size( + groups = {CreateGroup.class, UpdateGroup.class}, + min = ValidationConstant.MIN_REPOSITORY_NAME_LENGTH, + max = ValidationConstant.MAX_REPOSITORY_NAME_LENGTH, + message = "REPOSITORYDTO_REPOSITORYNAME_SIZE {RepositoryDTO.repositoryName.Size}") + @NotBlank( + groups = {CreateGroup.class}, + message = "REPOSITORYDTO_REPOSITORYNAME_NOTBLANK {RepositoryDTO.repositoryName.NotBlank}") + @Pattern( + regexp = ValidationConstant.REPOSITORY_NAME_PATTERN, + groups = {CreateGroup.class, UpdateGroup.class}, + message = "REPOSITORYNAME_PATTERN_MISMATCH {REPOSITORYNAME_PATTERN_MISMATCH}") + String repositoryName, + @Schema(description = "Repository Description") + @Size( + groups = {CreateGroup.class, UpdateGroup.class}, + min = ValidationConstant.MIN_REPOSITORY_DESCRIPTION_LENGTH, + max = ValidationConstant.MAX_REPOSITORY_DESCRIPTION_LENGTH, + message = "REPOSITORYDTO_REPOSITORYDESCRIPTION_SIZE {RepositoryDTO.repositoryDescription.Size}") + String repositoryDescription, + @Schema(description = "Whether or Not Private Repo") + Boolean isPrivate, + @Schema(description = "Star Count") + @Min(groups = {CreateGroup.class, UpdateGroup.class}, value = 0, message = "REPOSITORYDTO_STAR_MIN {RepositoryDTO.star.Min}") + Integer star, + @Schema(description = "Fork Count") + @Min(groups = {CreateGroup.class, UpdateGroup.class}, value = 0, message = "REPOSITORYDTO_FORK_MIN {RepositoryDTO.fork.Min}") + Integer fork, + @Schema(description = "Watcher Count") + @Min(groups = {CreateGroup.class, UpdateGroup.class}, value = 0, message = "REPOSITORYDTO_WATCHER_MIN {RepositoryDTO.watcher.Min}") + Integer watcher +) {} diff --git a/src/main/java/edu/cmipt/gcs/pojo/repository/RepositoryPO.java b/src/main/java/edu/cmipt/gcs/pojo/repository/RepositoryPO.java new file mode 100644 index 0000000..72b9036 --- /dev/null +++ b/src/main/java/edu/cmipt/gcs/pojo/repository/RepositoryPO.java @@ -0,0 +1,38 @@ +package edu.cmipt.gcs.pojo.repository; + +import com.baomidou.mybatisplus.annotation.TableName; + +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.TableLogic; +import lombok.Data; + +@Data +@TableName("t_repository") +public class RepositoryPO { + private Long id; + private String repositoryName; + private String repositoryDescription; + private Boolean isPrivate; + private Long userId; + private Integer star; + private Integer fork; + private Integer watcher; + private LocalDateTime gmtCreated; + private LocalDateTime gmtUpdated; + @TableLogic private LocalDateTime gmtDeleted; + + public RepositoryPO(RepositoryDTO repositoryDTO, Long userId) { + try { + this.id = Long.valueOf(repositoryDTO.id()); + } catch (NumberFormatException e) { + this.id = null; + } + this.repositoryName = repositoryDTO.repositoryName(); + this.repositoryDescription = repositoryDTO.repositoryDescription(); + this.isPrivate = repositoryDTO.isPrivate(); + this.userId = userId; + this.star = repositoryDTO.star(); + this.fork = repositoryDTO.fork(); + this.watcher = repositoryDTO.watcher(); + } +} diff --git a/src/main/java/edu/cmipt/gcs/pojo/repository/RepositoryVO.java b/src/main/java/edu/cmipt/gcs/pojo/repository/RepositoryVO.java new file mode 100644 index 0000000..92a5722 --- /dev/null +++ b/src/main/java/edu/cmipt/gcs/pojo/repository/RepositoryVO.java @@ -0,0 +1,17 @@ +package edu.cmipt.gcs.pojo.repository; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record RepositoryVO( + @Schema(description = "Repository ID") String id, + @Schema(description = "Repository Name") String repositoryName, + @Schema(description = "Repository Description") String repositoryDescription, + @Schema(description = "Whether or Not Private Repo") Boolean isPrivate, + @Schema(description = "Star Count") Integer star, + @Schema(description = "Fork Count") Integer fork, + @Schema(description = "Watcher Count") Integer watcher) { + public RepositoryVO(RepositoryPO repositoryPO) { + this(repositoryPO.getId().toString(), repositoryPO.getRepositoryName(), repositoryPO.getRepositoryDescription(), + repositoryPO.getIsPrivate(), repositoryPO.getStar(), repositoryPO.getFork(), repositoryPO.getWatcher()); + } +} diff --git a/src/main/java/edu/cmipt/gcs/pojo/user/UserDTO.java b/src/main/java/edu/cmipt/gcs/pojo/user/UserDTO.java index 929a6f8..3e2dad7 100644 --- a/src/main/java/edu/cmipt/gcs/pojo/user/UserDTO.java +++ b/src/main/java/edu/cmipt/gcs/pojo/user/UserDTO.java @@ -20,7 +20,7 @@ */ @Schema(description = "User Data Transfer Object") public record UserDTO( - @Schema(description = "User ID", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "User ID") @Null(groups = CreateGroup.class, message = "USERDTO_ID_NULL {UserDTO.id.Null}") @NotNull( groups = UpdateGroup.class, diff --git a/src/main/java/edu/cmipt/gcs/service/RepositoryService.java b/src/main/java/edu/cmipt/gcs/service/RepositoryService.java new file mode 100644 index 0000000..71706a3 --- /dev/null +++ b/src/main/java/edu/cmipt/gcs/service/RepositoryService.java @@ -0,0 +1,7 @@ +package edu.cmipt.gcs.service; + +import com.baomidou.mybatisplus.extension.service.IService; + +import edu.cmipt.gcs.pojo.repository.RepositoryPO; + +public interface RepositoryService extends IService {} diff --git a/src/main/java/edu/cmipt/gcs/service/RepositoryServiceImpl.java b/src/main/java/edu/cmipt/gcs/service/RepositoryServiceImpl.java new file mode 100644 index 0000000..d7eba27 --- /dev/null +++ b/src/main/java/edu/cmipt/gcs/service/RepositoryServiceImpl.java @@ -0,0 +1,11 @@ +package edu.cmipt.gcs.service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +import edu.cmipt.gcs.dao.RepositoryMapper; +import edu.cmipt.gcs.pojo.repository.RepositoryPO; + +import org.springframework.stereotype.Service; + +@Service +public class RepositoryServiceImpl extends ServiceImpl implements RepositoryService {} diff --git a/src/main/resources/message/message.properties b/src/main/resources/message/message.properties index acbad52..48edbd8 100644 --- a/src/main/resources/message/message.properties +++ b/src/main/resources/message/message.properties @@ -29,3 +29,14 @@ USER_NOT_FOUND=User not found: {} USER_UPDATE_FAILED=User update failed: {} USER_DELETE_FAILED=User delete failed: {} + +RepositoryDTO.id.Null=Repository id must be null when creating a new repository +RepositoryDTO.id.NotNull=Repository id cannot be null +RepositoryDTO.name.Size=Repository name must be between {min} and {max} characters +RepositoryDTO.name.NotBlank=Repository name cannot be blank +RepositoryDTO.description.Size=Repository description must be between {min} and {max} characters +RepositoryDTO.star.Min=Star must be greater than or equal to {value} +RepositoryDTO.fork.Min=Fork must be greater than or equal to {value} +RepositoryDTO.watcher.Min=Watcher must be greater than or equal to {value} + +REPOSITORYNAME_PATTERN_MISMATCH=Repository name can only be alphanumeric, hyphen or underline