Merge branch '105-paper-latex-editing' into 'dev'

Resolve "Генерация PDF файла из формата latex"

Closes #105

See merge request romanov73/ng-tracker!60
This commit is contained in:
Anton Romanov 2019-04-18 18:31:18 +00:00
commit 3bdb8798cd
14 changed files with 371 additions and 64 deletions

View File

@ -19,6 +19,9 @@ public class FileData extends BaseEntity {
private byte[] data;
@Column(name = "is_latex_attach")
private boolean isLatexAttach;
public String getName() {
return name;
}
@ -50,4 +53,12 @@ public class FileData extends BaseEntity {
public void setData(byte[] data) {
this.data = data;
}
public boolean isLatexAttach() {
return isLatexAttach;
}
public void setLatexAttach(boolean latexAttach) {
isLatexAttach = latexAttach;
}
}

View File

@ -9,6 +9,7 @@ public class FileDataDto {
private String fileName;
private String tmpFileName;
private boolean deleted;
private boolean isLatexAttach;
public FileDataDto() {
}
@ -16,17 +17,20 @@ public class FileDataDto {
@JsonCreator
public FileDataDto(@JsonProperty("id") Integer id,
@JsonProperty("name") String name,
@JsonProperty("isLatexAttach") boolean isLatexAttach,
@JsonProperty("fileName") String fileName,
@JsonProperty("tmpFileName") String tmpFileName) {
this.id = id;
this.name = name;
this.fileName = fileName;
this.tmpFileName = tmpFileName;
this.isLatexAttach = isLatexAttach;
}
public FileDataDto(FileData fileData) {
this.id = fileData.getId();
this.name = fileData.getName();
this.isLatexAttach = fileData.isLatexAttach();
}
public FileDataDto(String fileName, String tmpFileName) {
@ -73,4 +77,19 @@ public class FileDataDto {
this.deleted = deleted;
}
public boolean isLatexAttach() {
return isLatexAttach;
}
public boolean getIsLatexAttach() {
return isLatexAttach;
}
public void setLatexAttach(boolean latexAttach) {
isLatexAttach = latexAttach;
}
public void setIsLatexAttach(boolean latexAttach) {
isLatexAttach = latexAttach;
}
}

View File

@ -6,7 +6,10 @@ import org.springframework.web.multipart.MultipartFile;
import ru.ulstu.file.model.FileData;
import ru.ulstu.file.model.FileDataDto;
import ru.ulstu.file.repostory.FileRepository;
import ru.ulstu.paper.model.PaperDto;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@ -15,6 +18,7 @@ import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import static java.nio.charset.StandardCharsets.UTF_8;
@ -107,6 +111,7 @@ public class FileService {
private FileData copyFromDto(FileData fileData, FileDataDto fileDataDto) {
fileData.setName(fileDataDto.getName());
fileData.setLatexAttach(fileDataDto.isLatexAttach());
return fileData;
}
@ -118,4 +123,27 @@ public class FileService {
public FileDataDto createFromMultipartFile(MultipartFile multipartFile) throws IOException {
return new FileDataDto(multipartFile.getOriginalFilename(), uploadToTmpDir(multipartFile));
}
public void createLatexAttachs(PaperDto paper) throws IOException {
for (FileDataDto fileDataDto : paper.getFiles()
.stream()
.filter(f -> f.isLatexAttach() && !f.isDeleted())
.collect(Collectors.toList())) {
if (fileDataDto.getId() == null) {
File oldFile = getTmpFilePath(fileDataDto.getTmpFileName()).toFile();
File renamed = getTmpFilePath(fileDataDto.getName()).toFile();
oldFile.renameTo(renamed);
} else {
Files.write(getTmpFilePath(fileDataDto.getName()), fileRepository.findOne(fileDataDto.getId()).getData());
}
}
}
public File createLatexFile(PaperDto paper) throws IOException {
BufferedWriter writer = Files.newBufferedWriter(getTmpFilePath(paper.getTitle() + ".tex"));
writer.write(paper.getLatexText());
writer.close();
return getTmpFilePath(paper.getTitle() + ".tex").toFile();
}
}

View File

@ -1,5 +1,8 @@
package ru.ulstu.paper.controller;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.Errors;
@ -13,17 +16,20 @@ import ru.ulstu.deadline.model.Deadline;
import ru.ulstu.paper.model.Paper;
import ru.ulstu.paper.model.PaperDto;
import ru.ulstu.paper.model.PaperFilterDto;
import ru.ulstu.paper.service.LatexService;
import ru.ulstu.paper.service.PaperService;
import ru.ulstu.user.model.User;
import springfox.documentation.annotations.ApiIgnore;
import javax.validation.Valid;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.stream.Collectors;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.springframework.util.StringUtils.isEmpty;
@ -32,9 +38,11 @@ import static org.springframework.util.StringUtils.isEmpty;
@ApiIgnore
public class PaperController {
private final PaperService paperService;
private final LatexService latexService;
public PaperController(PaperService paperService) {
public PaperController(PaperService paperService, LatexService latexService) {
this.paperService = paperService;
this.latexService = latexService;
}
@GetMapping("/papers")
@ -111,6 +119,14 @@ public class PaperController {
return years;
}
@PostMapping("/generatePdf")
public ResponseEntity<byte[]> getPdfFile(PaperDto paper) throws IOException, InterruptedException {
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition", "attachment; filename='" +
URLEncoder.encode(paper.getTitle() + ".pdf", UTF_8.toString()) + "'");
return new ResponseEntity<>(latexService.generatePdfFromLatexFile(paper), headers, HttpStatus.OK);
}
private void filterEmptyDeadlines(PaperDto paperDto) {
paperDto.setDeadlines(paperDto.getDeadlines().stream()
.filter(dto -> dto.getDate() != null || !isEmpty(dto.getDescription()))

View File

@ -91,6 +91,9 @@ public class Paper extends BaseEntity implements UserContainer {
@ManyToMany(fetch = FetchType.EAGER)
private Set<User> authors = new HashSet<>();
@Column(name = "latex_text")
private String latexText;
public PaperStatus getStatus() {
return status;
}
@ -179,6 +182,14 @@ public class Paper extends BaseEntity implements UserContainer {
this.url = url;
}
public String getLatexText() {
return latexText;
}
public void setLatexText(String latexText) {
this.latexText = latexText;
}
@Override
public Set<User> getUsers() {
return getAuthors();

View File

@ -4,8 +4,8 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.validator.constraints.NotEmpty;
import ru.ulstu.file.model.FileDataDto;
import ru.ulstu.deadline.model.Deadline;
import ru.ulstu.file.model.FileDataDto;
import ru.ulstu.user.model.UserDto;
import javax.validation.constraints.Size;
@ -36,6 +36,7 @@ public class PaperDto {
private Set<Integer> authorIds;
private Set<UserDto> authors;
private Integer filterAuthorId;
private String latexText;
public PaperDto() {
deadlines.add(new Deadline());
@ -49,6 +50,7 @@ public class PaperDto {
@JsonProperty("updateDate") Date updateDate,
@JsonProperty("deadlines") List<Deadline> deadlines,
@JsonProperty("comment") String comment,
@JsonProperty("latex_text") String latexText,
@JsonProperty("url") String url,
@JsonProperty("locked") Boolean locked,
@JsonProperty("files") List<FileDataDto> files,
@ -62,6 +64,7 @@ public class PaperDto {
this.deadlines = deadlines;
this.comment = comment;
this.url = url;
this.latexText = latexText;
this.locked = locked;
this.files = files;
this.authors = authors;
@ -76,6 +79,7 @@ public class PaperDto {
this.deadlines = paper.getDeadlines();
this.comment = paper.getComment();
this.url = paper.getUrl();
this.latexText = paper.getLatexText();
this.locked = paper.getLocked();
this.files = convert(paper.getFiles(), FileDataDto::new);
this.authorIds = convert(paper.getAuthors(), user -> user.getId());
@ -178,6 +182,14 @@ public class PaperDto {
this.url = url;
}
public String getLatexText() {
return latexText;
}
public void setLatexText(String latexText) {
this.latexText = latexText;
}
public String getAuthorsString() {
return StringUtils.abbreviate(authors
.stream()

View File

@ -0,0 +1,72 @@
package ru.ulstu.paper.service;
import org.springframework.stereotype.Service;
import ru.ulstu.file.service.FileService;
import ru.ulstu.paper.model.PaperDto;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
@Service
public class LatexService {
private String errorMessage;
private File pdfFile;
private FileService fileService;
private final String pdfLatexError = "Errors occurred while executing pdfLaTeX.";
private final String bibtexError = "Errors occurred while executing bibtex.";
public LatexService(FileService fileService) {
this.fileService = fileService;
}
public byte[] generatePdfFromLatexFile(PaperDto paper) throws IOException, InterruptedException {
fileService.createLatexAttachs(paper);
File tex = fileService.createLatexFile(paper);
if (!generate(paper.getTitle(), tex.getParentFile())) {
throw new IOException(errorMessage);
}
return Files.readAllBytes(pdfFile.toPath());
}
private int startProcess(String[] args, File dir, String message) throws IOException, InterruptedException {
ProcessBuilder processBuilder = new ProcessBuilder(args);
processBuilder.redirectErrorStream(true);
processBuilder.directory(dir);
Process process = processBuilder.start();
InputStreamReader inputStreamReader = new InputStreamReader(process.getInputStream());
try (BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
while ((bufferedReader.readLine()) != null) ;
}
int exitCode = process.waitFor();
if (exitCode != 0) {
errorMessage = message + " Exit value of the process: " + exitCode;
}
return exitCode;
}
private boolean generate(String filename, File dir) throws IOException, InterruptedException {
startProcess(new String[]{"pdflatex", filename, "--interaction=nonstopmode"}, dir, pdfLatexError);
startProcess(new String[]{"bibtex", filename}, dir, bibtexError);
if (startProcess(new String[]{"pdflatex", filename, "--interaction=nonstopmode"}, dir, pdfLatexError) != 0)
return false;
return checkPdf(filename, dir);
}
private boolean checkPdf(String filename, File dir) {
pdfFile = new File(dir.getAbsolutePath() + File.separator + filename + ".pdf");
if (pdfFile.isFile()) return true;
else {
errorMessage = "The pdf file could not be created.";
return false;
}
}
}

View File

@ -95,6 +95,7 @@ public class PaperService {
private Paper copyFromDto(Paper paper, PaperDto paperDto) throws IOException {
paper.setComment(paperDto.getComment());
paper.setUrl(paperDto.getUrl());
paper.setLatexText(paperDto.getLatexText());
paper.setCreateDate(paper.getCreateDate() == null ? new Date() : paper.getCreateDate());
paper.setLocked(paperDto.getLocked());
paper.setStatus(paperDto.getStatus() == null ? DRAFT : paperDto.getStatus());

View File

@ -0,0 +1,14 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="masha" id="20190323_000000-1">
<addColumn tableName="paper">
<column name="latex_text" type="varchar"/>
</addColumn>
<addColumn tableName="file">
<column name="is_latex_attach" type="boolean"/>
</addColumn>
</changeSet>
</databaseChangeLog>

View File

@ -25,5 +25,6 @@
<include file="db/changelog-20190331_000010-schema.xml"/>
<include file="db/changelog-20190410_000000-schema.xml"/>
<include file="db/changelog-20190418_000000-schema.xml"/>
<include file="db/changelog-20190323_000001-schema.xml"/>
<include file="db/common/changelog-20190312_130000-schema.xml"/>
</databaseChangeLog>

View File

@ -1,5 +1,9 @@
#files-list .row > div:nth-child(6) {
#files-list .row > div:nth-child(7) {
display: flex;
justify-content: center;
flex-direction: column;
}
.nav-tabs {
margin-bottom: 20px;
}

View File

@ -3,6 +3,7 @@
var urlFileUpload = "/api/1.0/files/uploadTmpFile";
var urlFileDownload = "/api/1.0/files/download/";
var urlPdfGenerating = "/papers/generatePdf";
var urlFileDownloadTmp = "/api/1.0/files/download-tmp/";
/* exported MessageTypesEnum */

View File

@ -0,0 +1,45 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
</head>
<body>
<body>
<div th:fragment="filesList (isLatexAttach)" th:remove="tag">
<th:block th:each="file, rowStat : *{files}">
<span th:if="${file.isLatexAttach == isLatexAttach}" th:remove="tag">
<div class="row" th:id="|files${rowStat.index}|"
th:style="${file.deleted} ? 'display: none;' :''">
<input type="hidden" th:field="*{files[__${rowStat.index}__].id}"/>
<input type="hidden"
th:field="*{files[__${rowStat.index}__].deleted}"/>
<input type="hidden"
th:field="*{files[__${rowStat.index}__].name}"/>
<input type="hidden"
th:field="*{files[__${rowStat.index}__].tmpFileName}"/>
<input type="hidden"
th:field="*{files[__${rowStat.index}__].isLatexAttach}"/>
<div class="col-2">
<a class="btn btn-danger float-right"
th:onclick="|$('#files${rowStat.index}\\.deleted').val('true'); $('#files${rowStat.index}').hide(); |">
<span aria-hidden="true"><i class="fa fa-times"/></span>
</a>
</div>
<div class="col-10">
<a th:onclick="${file.id==null} ? 'downloadFile('+${file.tmpFileName}+',null,\''+${file.name}+'\')':
'downloadFile(null,'+${file.id}+',\''+${file.name}+'\')' "
href="javascript:void(0)"
th:text="*{files[__${rowStat.index}__].name}">
</a>
</div>
</div>
</span>
</th:block>
</div>
</body>
</body>
</html>

View File

@ -20,35 +20,47 @@
<hr/>
<div class="row">
<div class="col-lg-12">
<form id="paper-form" method="post" th:action="@{'/papers/paper?id='+ *{id == null ? '' : id} + ''}"
<form name="paperform" id="paper-form" method="post"
th:action="@{'/papers/paper?id='+ *{id == null ? '' : id} + ''}"
th:object="${paperDto}">
<div class="row">
<div class="col-md-7 col-sm-12">
<input type="hidden" name="id" th:field="*{id}"/>
<div class="form-group">
<label for="title">Название:</label>
<input class="form-control" id="title" type="text"
placeholder="Название статьи"
th:field="*{title}"/>
<p th:if="${#fields.hasErrors('title')}" th:errors="*{title}"
class="alert alert-danger">Incorrect title</p>
<p class="help-block text-danger"></p>
</div>
<nav>
<div class="nav nav-tabs" id="nav-tab" role="tablist">
<a class="nav-item nav-link active" id="nav-main-tab" data-toggle="tab"
href="#nav-main" role="tab" aria-controls="nav-main" aria-selected="true">Статья</a>
<a class="nav-item nav-link" id="nav-latex-tab" data-toggle="tab"
href="#nav-latex" role="tab" aria-controls="nav-latex" aria-selected="false">Latex</a>
</div>
</nav>
<div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-main" role="tabpanel"
aria-labelledby="nav-main-tab">
<input type="hidden" name="id" th:field="*{id}"/>
<div class="form-group">
<label for="title">Название:</label>
<input class="form-control" id="title" type="text"
placeholder="Название статьи"
th:field="*{title}"/>
<p th:if="${#fields.hasErrors('title')}" th:errors="*{title}"
class="alert alert-danger">Incorrect title</p>
<p class="help-block text-danger"></p>
</div>
<div class="form-group">
<label for="status">Статус:</label>
<select class="form-control" th:field="*{status}" id="status">
<option th:each="status : ${allStatuses}" th:value="${status}"
th:text="${status.statusName}">Status
</option>
</select>
</div>
<div class="form-group">
<label for="status">Статус:</label>
<select class="form-control" th:field="*{status}" id="status">
<option th:each="status : ${allStatuses}" th:value="${status}"
th:text="${status.statusName}">Status
</option>
</select>
</div>
<div class="form-group">
<label for="comment">Комментарий:</label>
<textarea class="form-control" rows="5" id="comment"
th:field="*{comment}"></textarea>
</div>
<div class="form-group">
<label for="comment">Комментарий:</label>
<textarea class="form-control" rows="5" id="comment"
th:field="*{comment}"></textarea>
</div>
<div class="form-group">
<label for="title">Ссылка на сайт конференции:</label>
@ -99,42 +111,52 @@
class="alert alert-danger">Incorrect title</p>
</div>
<div class="form-group" id="files-list">
<label>Файлы:</label>
<th:block th:each="file, rowStat : *{files}">
<div class="row" th:id="|files${rowStat.index}|"
th:style="${file.deleted} ? 'display: none;' :''">
<input type="hidden" th:field="*{files[__${rowStat.index}__].id}"/>
<input type="hidden" th:field="*{files[__${rowStat.index}__].deleted}"/>
<input type="hidden" th:field="*{files[__${rowStat.index}__].name}"/>
<input type="hidden" th:field="*{files[__${rowStat.index}__].tmpFileName}"/>
<div class="col-2">
<a class="btn btn-danger float-right"
th:onclick="|$('#files${rowStat.index}\\.deleted').val('true'); $('#files${rowStat.index}').hide(); |">
<span aria-hidden="true"><i class="fa fa-times"/></span>
</a>
</div>
<div class="col-10">
<a th:onclick="${file.id==null} ? 'downloadFile('+${file.tmpFileName}+',null,\''+${file.name}+'\')':
'downloadFile(null,'+${file.id}+',\''+${file.name}+'\')' "
href="javascript:void(0)"
th:text="*{files[__${rowStat.index}__].name}">
</a>
<div class="form-group files-list" id="files-list">
<label>Файлы:</label>
<div th:replace="papers/fragments/paperFilesListFragment :: filesList(isLatexAttach = ${false})"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="locked"
th:field="*{locked}"/>
<label class="form-check-label" for="locked">Заблокирована</label>
</div>
<div class="form-group">
<label for="loader">Загрузить статью:</label>
<div id="loader">
</div>
</div>
</th:block>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="locked"
th:field="*{locked}"/>
<label class="form-check-label" for="locked">Заблокирована</label>
</div>
<div class="form-group">
<label for="loader">Загрузить статью:</label>
<div id="loader">
</div>
<div class="tab-pane fade" id="nav-latex" role="tabpanel"
aria-labelledby="nav-profile-tab">
<div class="form-group">
<label for="latex-text">Latex текст:</label>
<textarea class="form-control" id="latex-text" type="text" rows="10"
placeholder="Latex.."
th:field="*{latexText}"/>
</div>
<div class="form-group files-list" id="latex-files-list">
<label>Файлы:</label>
<div th:replace="papers/fragments/paperFilesListFragment :: filesList(isLatexAttach = ${true})"/>
</div>
<div class="form-group">
<label for="latex-loader">Загрузить файлы:</label>
<div id="latex-loader">
</div>
</div>
<div class="form-group">
<button id="pdfBtn" class="btn btn-primary text-uppercase"
onclick="generatePDF()"
type="button">
<i id="pdfLoadingIcon" class='fa fa-circle-o-notch fa-spin'
style="display: none;"></i>
pdf
</button>
</div>
</div>
</div>
</div>
@ -207,14 +229,26 @@
showFeedbackMessage("Файл успешно загружен");
console.debug(response);
addNewFile(response);
addNewFile(response, $("#files-list"), false);
}
});
new FileLoader({
div: "latex-loader",
url: urlFileUpload,
maxSize: -1,
extensions: [],
callback: function (response) {
showFeedbackMessage("Файл успешно загружен");
console.debug(response);
addNewFile(response, $("#latex-files-list"), true);
}
});
$('.selectpicker').selectpicker();
});
/*]]>*/
function addNewFile(fileDto) {
var fileNumber = $("#files-list div.row").length;
function addNewFile(fileDto, listElement, isLatexAttach) {
var fileNumber = $('.files-list div.row').length;
var newFileRow = $("<div/>")
.attr("id", 'files' + fileNumber)
@ -248,6 +282,13 @@
.attr("name", "files[" + fileNumber + "].tmpFileName");
newFileRow.append(tmpFileNameInput);
var isLatexInput = $("<input/>")
.attr("type", "hidden")
.attr("id", "files" + fileNumber + ".isLatexAttach")
.attr("value", isLatexAttach)
.attr("name", "files[" + fileNumber + "].isLatexAttach");
newFileRow.append(isLatexInput);
var nextDiv = $("<div/>")
.addClass("col-2");
@ -266,7 +307,7 @@
.attr("onclick", "downloadFile('" + fileDto.tmpFileName + "',null,'" + fileDto.fileName + "')"));
newFileRow.append(nameDiv);
$("#files-list").append(newFileRow);
listElement.append(newFileRow);
}
@ -298,6 +339,37 @@
}
}
}
function generatePDF() {
$('#pdfLoadingIcon').show();
$('#pdfBtn').prop('disabled', true);
var formData = new FormData(document.forms.paperform);
var xhr = new XMLHttpRequest();
xhr.open("POST", urlPdfGenerating);
xhr.send(formData);
xhr.responseType = 'blob';
xhr.onload = function () {
if (this.status == 200) {
console.debug(this.response);
var blob = new Blob([this.response], {type: 'application/pdf'});
let a = document.createElement("a");
a.style = "display: none";
document.body.appendChild(a);
let url = window.URL.createObjectURL(blob);
a.href = url;
a.download = $('#title').val() + '.pdf';
a.click();
window.URL.revokeObjectURL(url);
} else {
showFeedbackMessage("Ошибка при создании PDF", MessageTypesEnum.DANGER);
}
$('#pdfLoadingIcon').hide();
$('#pdfBtn').prop('disabled', false);
}
}
</script>
</div>