Skip to content
Open
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
19 changes: 19 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM maven:3.8-openjdk-17 AS build
WORKDIR /app

COPY pom.xml .
RUN mvn dependency:go-offline -B

COPY src src
RUN mvn clean package -DskipTests -B

FROM openjdk:17-jre-slim
WORKDIR /app

COPY --from=build /app/target/jira-1.0.jar app.jar

RUN mkdir -p /app/attachments

EXPOSE 8080

ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,16 @@
- https://habr.com/ru/articles/259055/

Список выполненных задач:
...
1. Разобраться со структурой проекта (onboarding).
2. Удалить социальные сети: vk, yandex.
3. Вынести чувствительную информацию в отдельный проперти файл:
-логин
-пароль БД
-идентификаторы для OAuth регистрации/авторизации
-настройки почты
4. Переделать тесты так, чтоб во время тестов использовалась in memory БД (H2), а не PostgreSQL.
5. Написать тесты для всех публичных методов контроллера ProfileRestController.
6. Сделать рефакторинг метода com.javarush.jira.bugtracking.attachment.FileUtil#upload чтоб он использовал современный подход для работы с файловой системмой.
7. Добавить новый функционал: добавления тегов к задаче (REST API + реализация на сервисе).
8. Написать Dockerfile для основного сервера
9. Написать docker-compose файл для запуска контейнера сервера вместе с БД и nginx. Для nginx используй конфиг-файл config/nginx.conf. При необходимости файл конфига можно редактировать.
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
<optional>true</optional>
</dependency>
<dependency>
Expand Down Expand Up @@ -142,6 +143,11 @@
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
8 changes: 0 additions & 8 deletions resources/view/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,6 @@ <h3 class="mb-3">Sign in</h3>
type="button">
<i class="fa-brands fa-google"></i>
</a>
<a class="btn btn-primary btn-lg me-2" href="/oauth2/authorization/vk" style="padding-left: 17px; padding-right: 17px;"
type="button">
<i class="fa-brands fa-vk"></i>
</a>
<a class="btn btn-danger btn-lg me-2" href="/oauth2/authorization/yandex" style="padding-left: 21px; padding-right: 21px;"
type="button">
<i class="fa-brands fa-yandex"></i>
</a>
<a class="btn btn-dark btn-lg me-2" href="/oauth2/authorization/github" type="button">
<i class="fa-brands fa-github"></i>
</a>
Expand Down
8 changes: 0 additions & 8 deletions resources/view/unauth/register.html
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,6 @@ <h3 class="mb-3">Registration</h3>
type="button">
<i class="fa-brands fa-google"></i>
</a>
<a class="btn btn-primary btn-lg me-2" href="/oauth2/authorization/vk" style="padding-left: 17px; padding-right: 17px;"
type="button">
<i class="fa-brands fa-vk"></i>
</a>
<a class="btn btn-danger btn-lg me-2" href="/oauth2/authorization/yandex" style="padding-left: 21px; padding-right: 21px;"
type="button">
<i class="fa-brands fa-yandex"></i>
</a>
<a class="btn btn-dark btn-lg me-2" href="/oauth2/authorization/github" type="button">
<i class="fa-brands fa-github"></i>
</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@
import org.springframework.core.io.UrlResource;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
Expand All @@ -25,14 +23,24 @@ public static void upload(MultipartFile multipartFile, String directoryPath, Str
throw new IllegalRequestDataException("Select a file to upload.");
}

File dir = new File(directoryPath);
if (dir.exists() || dir.mkdirs()) {
File file = new File(directoryPath + fileName);
try (OutputStream outStream = new FileOutputStream(file)) {
outStream.write(multipartFile.getBytes());
} catch (IOException ex) {
throw new IllegalRequestDataException("Failed to upload file" + multipartFile.getOriginalFilename());
try {
Path directory = Paths.get(directoryPath);
Files.createDirectories(directory);

Path filePath = directory.resolve(fileName).normalize();

if (!filePath.startsWith(directory)) {
throw new IllegalRequestDataException("Invalid file path: " + fileName);
}

try (InputStream inputStream = multipartFile.getInputStream()) {
Files.copy(inputStream, filePath);
}

} catch (IOException ex) {
throw new IllegalRequestDataException(
"Failed to upload file: " + multipartFile.getOriginalFilename() + ex
);
}
}

Expand Down
11 changes: 9 additions & 2 deletions src/main/java/com/javarush/jira/bugtracking/task/Task.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

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

import java.util.HashSet;
import static com.javarush.jira.bugtracking.task.TaskUtil.checkStatusChangePossible;

@Entity
Expand Down Expand Up @@ -74,13 +74,20 @@ public class Task extends TitleEntity implements HasCode {
@OneToMany(mappedBy = "taskId", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<Activity> activities;

public Task(Long id, String title, String typeCode, String statusCode, Long parentId, long projectId, Long sprintId) {
public Task(Long id, String title, String typeCode, String statusCode, Long parentId, long projectId, Long sprintId, Set<String> tags) {
super(id, title);
this.typeCode = typeCode;
this.statusCode = statusCode;
this.parentId = parentId;
this.projectId = projectId;
this.sprintId = sprintId;
this.tags = tags != null ? new HashSet<>(tags) : new HashSet<>();
}

public Task(Long id, String title, String typeCode, String statusCode,
Long parentId, Long projectId, Long sprintId) {
this(id, title, typeCode, statusCode, parentId,
projectId != null ? projectId : 0L, sprintId, new HashSet<>());
}

public void checkAndSetStatusCode(String statusCode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
import com.javarush.jira.bugtracking.Handlers;
import com.javarush.jira.bugtracking.UserBelong;
import com.javarush.jira.bugtracking.UserBelongRepository;
import com.javarush.jira.bugtracking.task.to.ActivityTo;
import com.javarush.jira.bugtracking.task.to.TaskTo;
import com.javarush.jira.bugtracking.task.to.TaskToExt;
import com.javarush.jira.bugtracking.task.to.TaskToFull;
import com.javarush.jira.bugtracking.task.to.*;
import com.javarush.jira.bugtracking.tree.ITreeNode;
import com.javarush.jira.common.util.Util;
import com.javarush.jira.login.AuthUser;
Expand All @@ -20,9 +17,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.*;

import static com.javarush.jira.common.BaseHandler.createdResponse;

Expand Down Expand Up @@ -151,6 +146,61 @@ public void delete(@PathVariable long id) {
activityService.delete(id);
}



@GetMapping("/{id}/tags")
@ResponseStatus(HttpStatus.OK)
public Set<String> getTags(@PathVariable long id) {
log.info("Getting tags for task {}", id);
Set<String> tags = taskService.getTags(id);
return tags;
}

/**
* POST /api/tasks/{id}/tags - добавить тег к задаче
* Body: {"tag": "urgent"}
*/
@PostMapping("/{id}/tags")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void addTag(@PathVariable long id, @Valid @RequestBody TagRequest tagRequest) {
log.info("Adding tag '{}' to task {}", tagRequest.getTag(), id);
taskService.addTag(id, tagRequest.getTag());
}

/**
* DELETE /api/tasks/{id}/tags/{tag} - удалить тег у задачи
*/
@DeleteMapping("/{id}/tags/{tag}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void removeTag(@PathVariable long id, @PathVariable String tag) {
log.info("Removing tag '{}' from task {}", tag, id);
taskService.removeTag(id, tag);
}

/**
* PUT /api/tasks/{id}/tags - заменить все теги задачи
* Body: ["feature", "backend", "api"]
*/
@PutMapping("/{id}/tags")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void replaceTags(@PathVariable long id, @RequestBody Set<String> tags) {
log.info("Replacing tags for task {} with {}", id, tags);
taskService.replaceTags(id, tags);
}

/**
* GET /api/tasks/by-tag?tag=urgent - найти все задачи по тегу
*/
@GetMapping("/by-tag")
public Set<TaskTo> findTasksByTag(@RequestParam String tag) {
log.info("Finding tasks by tag: {}", tag);
Set<Task> tasks = taskService.findTasksByTag(tag);
List<TaskTo> taskToList = handler.getMapper().toList(new ArrayList<>(tasks));
return new HashSet<>(taskToList);
}



private record TaskTreeNode(TaskTo taskTo, List<TaskTreeNode> subNodes) implements ITreeNode<TaskTo, TaskTreeNode> {
public TaskTreeNode(TaskTo taskTo) {
this(taskTo, new LinkedList<>());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import com.javarush.jira.common.BaseRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;

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

@Transactional(readOnly = true)
public interface TaskRepository extends BaseRepository<Task> {
Expand Down Expand Up @@ -37,4 +39,34 @@ WITH RECURSIVE task_with_subtasks AS (
WHERE id IN (SELECT child FROM task_with_subtasks)
""", nativeQuery = true)
void setTaskAndSubTasksSprint(long taskId, Long sprintId);


@Query("SELECT t.tags FROM Task t WHERE t.id = :taskId")
Set<String> findTagsByTaskId(@Param("taskId") Long taskId);


@Modifying
@Transactional
@Query(value = "INSERT INTO task_tag (task_id, tag) VALUES (:taskId, :tag)",
nativeQuery = true)
void addTag(@Param("taskId") Long taskId, @Param("tag") String tag);

@Modifying
@Transactional
@Query(value = "DELETE FROM task_tag WHERE task_id = :taskId AND tag = :tag",
nativeQuery = true)
void removeTag(@Param("taskId") Long taskId, @Param("tag") String tag);

@Query("SELECT CASE WHEN COUNT(t) > 0 THEN true ELSE false END " +
"FROM Task t JOIN t.tags tag WHERE t.id = :taskId AND tag = :tag")
boolean existsByTaskIdAndTag(@Param("taskId") Long taskId, @Param("tag") String tag);

@Modifying
@Transactional
@Query(value = "DELETE FROM task_tag WHERE task_id = :taskId",
nativeQuery = true)
void deleteAllTags(@Param("taskId") Long taskId);

@Query("SELECT t FROM Task t JOIN t.tags tag WHERE tag = :tag")
Set<Task> findTasksByTag(@Param("tag") String tag);
}
72 changes: 72 additions & 0 deletions src/main/java/com/javarush/jira/bugtracking/task/TaskService.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;

import static com.javarush.jira.bugtracking.ObjectType.TASK;
import static com.javarush.jira.bugtracking.task.TaskUtil.fillExtraFields;
Expand All @@ -39,6 +40,7 @@ public class TaskService {
private final SprintRepository sprintRepository;
private final TaskExtMapper extMapper;
private final UserBelongRepository userBelongRepository;
private final TaskRepository taskRepository;

@Transactional
public void changeStatus(long taskId, String statusCode) {
Expand Down Expand Up @@ -140,4 +142,74 @@ private void checkAssignmentActionPossible(long id, String userType, boolean ass
throw new DataConflictException(String.format(assign ? CANNOT_ASSIGN : CANNOT_UN_ASSIGN, userType, task.getStatusCode()));
}
}



@Transactional(readOnly = true)
public Set<String> getTags(Long taskId) {
if (!taskRepository.existsById(taskId)) {
throw new NotFoundException("Task with id " + taskId + " not found");
}
return taskRepository.findTagsByTaskId(taskId);
}

@Transactional
public void addTag(Long taskId, String tag) {
String normalizedTag = tag.trim().toLowerCase();

if (normalizedTag.isEmpty()) {
throw new IllegalArgumentException("Tag cannot be empty");
}

if (normalizedTag.length() < 2 || normalizedTag.length() > 32) {
throw new IllegalArgumentException("Tag must be between 2 and 32 characters");
}

if (!taskRepository.existsById(taskId)) {
throw new NotFoundException("Task with id " + taskId + " not found");
}

if (taskRepository.existsByTaskIdAndTag(taskId, normalizedTag)) {
throw new DataConflictException("Tag '" + normalizedTag + "' already exists for task " + taskId);
}

taskRepository.addTag(taskId, normalizedTag);
}

@Transactional
public void removeTag(Long taskId, String tag) {
String normalizedTag = tag.trim().toLowerCase();

if (!taskRepository.existsById(taskId)) {
throw new NotFoundException("Task with id " + taskId + " not found");
}

taskRepository.removeTag(taskId, normalizedTag);
}

@Transactional
public void replaceTags(Long taskId, Set<String> newTags) {
if (!taskRepository.existsById(taskId)) {
throw new NotFoundException("Task with id " + taskId + " not found");
}

taskRepository.deleteAllTags(taskId);

if (newTags != null && !newTags.isEmpty()) {
for (String tag : newTags) {
String normalizedTag = tag.trim().toLowerCase();
if (normalizedTag.length() >= 2 && normalizedTag.length() <= 32) {
taskRepository.addTag(taskId, normalizedTag);
}
}
}
}

@Transactional(readOnly = true)
public Set<Task> findTasksByTag(String tag) {
String normalizedTag = tag.trim().toLowerCase();
return taskRepository.findTasksByTag(normalizedTag);
}


}
Loading