Digamos que um outro time implementou uma interface Web para o Cotuba, que é composta por 3 diferentes telas:
- Uma tela principal, que lista todos os livros cadastrados
- Uma tela que detalha os capítulos do livro e que permite gerar o EPUB e o PDF
- Uma tela de edição de um capítulo
Essa UI Web foi implementada em um módulo específico.
Teríamos, então, duas UIs:
- uma interface de linha de comando, implementada pelo módulo
cotuba-cli
- uma interface Web, implementada pelo novo módulo
cotuba-web
- Abra um Terminal e baixe o Cotuba Modular usando o Git:
git clone https://github.com/caelum/cotuba-modular.git
- Copie o submódulo
cotuba-web
para o seu projeto:
cp -R cotuba-web/ ~/modulos/cotuba/
-
No Eclipse, vá em File > Import... > Existing Maven Projects e clique em Next. Em Root Directory, aponte para o diretório que contém a UI Web do Cotuba, preenchendo o campo com
/home/<usuario-do-curso>/modulos/cotuba/cotuba-web
. Não esqueça de trocar<usuario-do-curso>
pelo nome de usuário do curso. Clique em OK. Clique em Finish e seu projeto será importado. -
Abra o
pom.xml
do supermódulo Cotuba e declare o submódulo Web:
####### cotuba/pom.xml
<modules>
<module>cotuba-core</module>
<module>cotuba-cli</module>
<module>cotuba-pdf</module>
<module>cotuba-epub</module>
<module>cotuba-web</module> <!-- inserido -->
</modules>
- Execute a UI Web do Cotuba clicando com o botão direito na classe
Boot
do pacotecotuba.web
e escolhendo Run As... > Java Application.
Depois de várias mensagens, você deve ver o Console algo como:
2018-10-15 14:01:34.860 INFO 14993 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer :
Tomcat started on port(s): 8080 (http) with context path ''
2018-10-15 14:01:34.865 INFO 14993 --- [ restartedMain] cotuba.web.Boot :
Started Boot in 11.356 seconds (JVM running for 12.392)
Cotuba Web no ar!
- Acesse e explore o Cotuba Web a partir da URL a seguir:
A UI Web do Cotuba foi implementada usando as seguintes tecnologias:
- Spring Boot, com configurações em
src/main/resources/application.properties
- Thymeleaf para as telas, que ficam em
src/main/resources/templates
- MySQL como banco de dados
- Flyway para manter scripts de BD, que ficam em
src/main/resources/db/migration
- Spring Data JPA, para facilitar o código de acesso a dados
O código do módulo cotuba-web
está organizado no seguinte pacotes:
cotuba.web
, que contém a classe principalBoot
, que sobe o servidor Webcotuba.web.application
, que contém as classesCadastroDeCapitulos
eCadastroDeLivros
, que representam as histórias de usuário, e as interfacesRepositorioDeCapitulos
eRepositorioDeLivros
, que usa o Spring Data JPA para implementar o acesso a dadoscotuba.web.domain
, que contém as classesLivro
eCapitulo
, o pequeno modelo de domínio do Cotuba Webcotuba.web.controller
, que expõe as histórias do usuário na Web, associando-as aos templates Thymeleaf
Implemente um controller para a geração de ebooks com mapeamentos para PDF e para EPUB.
Por enquanto, disponibilize para download apenas os arquivos já gerados durante o curso.
- Depois de gerar um ebook, deve ser disponibilizado o arquivo para download.
No Spring MVC, uma das maneiras de se fazer isso é através de um ResponseEntity<ByteArrayResource>
.
Defina uma classe SpringFileUtils
no pacote cotuba.web
, que recebe um Path
e um media type e retorna um ResponseEntity<ByteArrayResource>
configurado com os dados do arquivo e os cabeçalhos necessários:
####### cotuba.web.SpringFileUtils
public class SpringFileUtils {
public static ResponseEntity<ByteArrayResource> montaResponseOArquivo(Path path, String type) {
try {
byte[] data = Files.readAllBytes(path);
ByteArrayResource resource = new ByteArrayResource(data);
String filename = path.getFileName().toString();
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_TYPE, type);
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename);
return ResponseEntity.ok().headers(headers).contentLength(data.length).body(resource);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
Não deixe de fazer os imports:
####### cotuba.web.SpringFileUtils
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
- Defina um Controller, no pacote
cotuba.web.controller
, com as URLs apropriadas para cada formato de ebook, PDF e EPUB.
Por enquanto, disponibilize para download, por meio da classe SpringFileUtils
, os arquivos já gerados para cada formato.
####### cotuba.web.controller.GeracaoDeLivrosController
@Controller
public class GeracaoDeLivrosController {
@GetMapping("/livros/{id}/pdf")
public ResponseEntity<ByteArrayResource> geraPDF(@PathVariable("id") Long id, Model model) {
// ATENÇÃO: troque <usuario-do-curso> pelo usuário do curso
Path pdf = Paths.get("/home/<usuario-do-curso>/Desktop/book.pdf");
return SpringFileUtils.montaResponseOArquivo(pdf, "application/pdf");
}
@GetMapping("/livros/{id}/epub")
public ResponseEntity<ByteArrayResource> geraEPUB(@PathVariable("id") Long id, Model model) {
// ATENÇÃO: troque <usuario-do-curso> pelo usuário do curso
Path epub = Paths.get("/home/<usuario-do-curso>/Desktop/book.epub");
return SpringFileUtils.montaResponseOArquivo(epub, "application/epub+zip");
}
}
Não esqueça de trocar <usuario-do-curso>
pelo nome de usuário do curso.
Certifique-se que os imports corretos foram feitos:
####### cotuba.web.controller.GeracaoDeLivrosController
import java.nio.file.Path;
import java.nio.file.Paths;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
- A aplicação já deve ter sido reiniciada automaticamente. Acesse a página de detalhes de livros e clique nos botões de geração de PDF ou EPUB. O download do ebook deve ser realizado.
Execute a classe Cotuba
passando os dados do livro e o formato como parâmetro.
Disponibilize o arquivo gerado para download, alterando o controller.
- Adicione como dependência do módulo
cotuba-web
os módulos:
cotuba-core
, cujas classes serão chamadas diretamente pelo código Webcotuba-pdf
ecotuba-epub
, que não serão chamados diretamente, mas cujos JARs precisam estar presentes para servirem como plugins
####### cotuba-web/pom.xml
<!-- inserido -->
<dependency>
<groupId>cotuba</groupId>
<artifactId>cotuba-core</artifactId>
<version>${project.version}</version>
</dependency>
<!-- inserido -->
<dependency>
<groupId>cotuba</groupId>
<artifactId>cotuba-pdf</artifactId>
<version>${project.version}</version>
</dependency>
<!-- inserido -->
<dependency>
<groupId>cotuba</groupId>
<artifactId>cotuba-epub</artifactId>
<version>${project.version}</version>
</dependency>
- Para chamar o Cotuba, vamos criar um classe
GeracaoDeLivros
, no pacotecotuba.web.application
. Anote-a com@Service
. Receba umCadastroDeLivros
no construtor, armazenando-o em um atributo.
####### cotuba.web.application.GeracaoDeLivros
package cotuba.web.application;
import org.springframework.stereotype.Service;
@Service
public class GeracaoDeLivros {
private final CadastroDeLivros livros;
public GeracaoDeLivros(CadastroDeLivros livros) {
this.livros = livros;
}
}
- Nessa classe, definiremos um método
geraLivro
, que recebe oid
do livro a ser gerado e o formato do ebook. O retorno será umPath
com o arquivo de saída.
No novo método, use o CadastroDeLivros
para obter o Livro
a partir do id
recebido como parâmetro.
####### cotuba.web.application.GeracaoDeLivros
@Service
public class GeracaoDeLivros {
// código omitido...
public Path geraLivro(Long id, FormatoEbook formato) { // inserido
Livro livro = livros.detalha(id); // inserido
// CHAMADA DO COTUBA AQUI..
}
}
Não esqueça de adicionar os seguintes imports:
####### cotuba.web.application.GeracaoDeLivros
import java.nio.file.Path;
import cotuba.domain.FormatoEbook;
import cotuba.web.domain.Livro;
O código não compilará com sucesso ainda. Precisamos chamar o Cotuba!
- Na classe
GeracaoDeLivrosController
, receba no construtor um objeto do tipoGeracaoDeLivros
, armazenando-o em um atributo.
####### cotuba.web.controller.GeracaoDeLivrosController
@Controller
public class GeracaoDeLivrosController {
private final GeracaoDeLivros geracao; // inserido
// inserido
public GeracaoDeLivrosController(GeracaoDeLivros geracao) {
this.geracao = geracao;
}
// código omitido...
}
Faça o import:
####### cotuba.web.controller.GeracaoDeLivrosController
import cotuba.web.application.GeracaoDeLivros;
- Modifique os métodos
geraPDF
egeraEPUB
doGeracaoDeLivrosController
, invocando o métodogeraLivro
deGeracaoDeLivros
com oFormatoEbook
adequado.
####### cotuba.web.controller.GeracaoDeLivrosController
@Controller
public class GeracaoDeLivrosController {
// código omitido...
@GetMapping("/livros/{id}/pdf")
public ResponseEntity<ByteArrayResource> geraPDF(@PathVariable("id") Long id, Model model) {
P̶a̶t̶h̶ ̶p̶d̶f̶ ̶=̶ ̶P̶a̶t̶h̶s̶.̶g̶e̶t̶(̶"̶/̶h̶o̶m̶e̶/̶<̶u̶s̶u̶a̶r̶i̶o̶-̶d̶o̶-̶c̶u̶r̶s̶o̶>̶/̶D̶e̶s̶k̶t̶o̶p̶/̶b̶o̶o̶k̶.̶p̶d̶f̶"̶)̶;̶
Path pdf = geracao.geraLivro(id, FormatoEbook.PDF); // inserido
return SpringFileUtils.montaResponseOArquivo(pdf, "application/pdf");
}
@GetMapping("/livros/{id}/epub")
public ResponseEntity<ByteArrayResource> geraEPUB(@PathVariable("id") Long id, Model model) {
P̶a̶t̶h̶ ̶e̶p̶u̶b̶ ̶=̶ ̶P̶a̶t̶h̶s̶.̶g̶e̶t̶(̶"̶/̶h̶o̶m̶e̶/̶<̶u̶s̶u̶a̶r̶i̶o̶-̶d̶o̶-̶c̶u̶r̶s̶o̶>̶/̶D̶e̶s̶k̶t̶o̶p̶/̶b̶o̶o̶k̶.̶e̶p̶u̶b̶"̶)̶;̶
Path epub = geracao.geraLivro(id, FormatoEbook.EPUB); // inserido
return SpringFileUtils.montaResponseOArquivo(epub, "application/epub+zip");
}
}
Não deixe de fazer o import:
####### cotuba.web.controller.GeracaoDeLivrosController
import cotuba.domain.FormatoEbook;
Não é possível testar ainda, pois há um erro de compilação em GeracaoDeLivros
.
Como chamar o Cotuba?
A primeira coisa é, no método geraLivro
de GeracaoDeLivros
, instanciá-lo:
Cotuba cotuba = new Cotuba();
Precisamos invocar o método executa
de Cotuba
, que recebe como parâmetros uma instância da interface ParametrosCotuba
e um Consumer<String>
.
O Consumer<String>
pode ser o method reference System.out::println
, que imprime as mensagens recebidas do Cotuba na saída padrão. No caso, o Console do Servidor Web.
Já para definir o ParametrosCotuba
, precisamos criar uma implementação dessa interface. Podemos defini-la por meio da classe interna ParametrosCotubaWeb
.
@Service
public class GeracaoDeLivros {
private static class ParametrosCotubaWeb implements ParametrosCotuba {
@Override
public FormatoEbook getFormato() {
}
@Override
public Path getArquivoDeSaida() {
}
@Override
public Path getDiretorioDosMD() {
}
}
}
Na classe interna ParametrosCotubaWeb
, devemos receber o Livro
e o FormatoEbook
como parâmetros do construtor, definindo-os em atributos.
private static class ParametrosCotubaWeb implements ParametrosCotuba {
private Livro livro;
private FormatoEbook formato;
private ParametrosCotubaWeb(Livro livro, FormatoEbook formato) {
this.livro = livro;
this.formato = formato;
}
// código omitido...
}
O formato recebido deve ser retornado no método getFormato
:
private static class ParametrosCotubaWeb implements ParametrosCotuba {
// código omitido...
@Override
public FormatoEbook getFormato() {
return formato;
}
}
Para definir o arquivo de saída, vamos criar um arquivo em um diretório temporário com o nome book.pdf
ou book.epub
, dependendo do formato.
private static class ParametrosCotubaWeb implements ParametrosCotuba {
// código omitido...
@Override
public Path getArquivoDeSaida() {
try {
Path diretorioTemporario = Files.createTempDirectory("ebooks");
String nomeDoArquivoDeSaida = "book" + formato.name().toLowerCase();
return diretorioTemporario.resolve(nomeDoArquivoDeSaida);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
Se o formato a ser gerado for um EPUB, o arquivo de saída seria algo parecido com /tmp/ebooks4379875856573865319/book.epub
.
No caso de um arquivo PDF, teríamos algo como /tmp/ebooks1653289186159041686/book.pdf
.
Ainda é preciso definir o diretório onde estão os arquivos .md
. Mas peraí...
Temos dois clientes que invocam o Cotuba: a interface de linha de comando e a interface Web.
Cada cliente obtém os arquivos MD de maneiras distintas:
- o
cotuba-cli
obtém os.md
de um diretório - o
cotuba-web
obtém os.md
da tabela de capítulos de seu BD
Poderíamos criar arquivos .md
, salvando-os em um diretório, a partir dos capítulos do BD.
Mas será que é a melhor opção? Será que não há um problema no design? Há algum um ponto de mudança que não foi antecipado?
A classe que usa o diretório com os arquivos .md
é a RenderizadorMDParaHTMLComCommonMark
, que implementa a interface RenderizadorMDParaHTML
.
Se repararmos bem, essa classe quebra o SRP. Há dois motivos para mudá-la:
- uma mudança na maneira de renderizar os arquivos MD para HTML
- uma mudança na maneira de obter esses arquivos MD
Há responsabilidades demais na classe RenderizadorMDParaHTMLComCommonMark
!
Às vezes é difícil antecipar mudanças que acontecerão no problema que estamos resolvendo e o respectivo impacto no código.
Consequentemente, não é trivial criar um design que tem flexibilidade onde realmente é necessário e que "abraça a mudança".
Quando detectarmos um problema no design, ao invés de tentarmos encaixar o código no design já existente, devemos corrigi-lo. Assim, com o tempo, teremos flexibilidades nos pontos certos.
Escrever código de qualidade é sempre incremental; você modela, observa seu modelo, aprende com ele e o melhora.
Maurício Aniche, no livro OO e SOLID para Ninjas (ANICHE, 2015)
Para trazer flexibilidade na obtenção dos arquivos MD, crie uma abstração por meio da interface RepositorioDeMDs
.
Faça com que a classe Cotuba
receba essa abstração. A use na implementação de RenderizadorMDParaHTML
.
Modifique apenas o código do módulo cotuba-core
. Por enquanto, deixe outros módulos apresentando erros de compilação.
- No pacote
cotuba.application
do módulocotuba-core
, crie a interfaceRepositorioDeMDs
. Nessa interface, defina um métodoobtemMDsDosCapitulos
, que retorna uma lista deString
. A lista retornada conterá os MDs dos capítulos.
####### cotuba.application.RepositorioDeMDs
package cotuba.application;
import java.util.List;
public interface RepositorioDeMDs {
List<String> obtemMDsDosCapitulos();
}
- Como teremos a abstração para a obtenção de MDs, podemos remover o diretório dos
ParametrosCotuba
:
####### cotuba.application.ParametrosCotuba
public interface ParametrosCotuba {
P̶a̶t̶h̶ ̶g̶e̶t̶D̶i̶r̶e̶t̶o̶r̶i̶o̶D̶o̶s̶M̶D̶(̶)̶;̶
FormatoEbook getFormato();
Path getArquivoDeSaida();
}
- No método
renderiza
da interfaceRenderizadorMDParaHTML
, do pacotecotuba.application
, troque o parâmetroPath
por umRepositorioDeMDs
.
####### cotuba.application.RenderizadorMDParaHTML
package cotuba.application;
i̶m̶p̶o̶r̶t̶ ̶j̶a̶v̶a̶.̶n̶i̶o̶.̶f̶i̶l̶e̶.̶P̶a̶t̶h̶;̶
import java.util.List;
import cotuba.domain.Capitulo;
import cotuba.md.RenderizadorMDParaHTMLComCommonMark;
public interface RenderizadorMDParaHTML {
L̶i̶s̶t̶<̶C̶a̶p̶i̶t̶u̶l̶o̶>̶ ̶r̶e̶n̶d̶e̶r̶i̶z̶a̶(̶P̶a̶t̶h̶ ̶d̶i̶r̶e̶t̶o̶r̶i̶o̶D̶o̶s̶M̶D̶)̶;̶
List<Capitulo> renderiza(RepositorioDeMDs repositorioDeMDs);
public static RenderizadorMDParaHTML cria() {
return new RenderizadorMDParaHTMLComCommonMark();
}
}
- Atualize a classe
RenderizadorMDParaHTMLComCommonMark
, que implementa a interfaceRenderizadorMDParaHTML
, para cumprir o novo contrato.
####### cotuba.md.RenderizadorMDParaHTMLComCommonMark
public class RenderizadorMDParaHTMLComCommonMark implements RenderizadorMDParaHTML {
@Override
p̶u̶b̶l̶i̶c̶ ̶L̶i̶s̶t̶<̶C̶a̶p̶i̶t̶u̶l̶o̶>̶ ̶r̶e̶n̶d̶e̶r̶i̶z̶a̶(̶P̶a̶t̶h̶ ̶d̶i̶r̶e̶t̶o̶r̶i̶o̶D̶o̶s̶M̶D̶)̶ ̶{̶
public List<Capitulo> renderiza(RepositorioDeMDs repositorioDeMDs) {
// código omitido...
}
}
Adicione o import:
####### cotuba.md.RenderizadorMDParaHTMLComCommonMark
import cotuba.application.RepositorioDeMDs;
- Remova o trecho de código que lê os arquivos
.md
de um diretório deRenderizadorMDParaHTMLComCommonMark
. Essa implementação ficará em outra classe.
####### cotuba.md.RenderizadorMDParaHTMLComCommonMark
public class RenderizadorMDParaHTMLComCommonMark implements RenderizadorMDParaHTML {
@Override
public List<Capitulo> renderiza(RepositorioDeMDs repositorioDeMDs) {
List<Capitulo> capitulos = new ArrayList<>();
P̶a̶t̶h̶M̶a̶t̶c̶h̶e̶r̶ ̶m̶a̶t̶c̶h̶e̶r̶ ̶=̶ ̶F̶i̶l̶e̶S̶y̶s̶t̶e̶m̶s̶.̶g̶e̶t̶D̶e̶f̶a̶u̶l̶t̶(̶)̶.̶g̶e̶t̶P̶a̶t̶h̶M̶a̶t̶c̶h̶e̶r̶(̶"̶g̶l̶o̶b̶:̶*̶*̶/̶*̶.̶m̶d̶"̶)̶;̶
t̶r̶y̶ ̶(̶S̶t̶r̶e̶a̶m̶<̶P̶a̶t̶h̶>̶ ̶a̶r̶q̶u̶i̶v̶o̶s̶M̶D̶ ̶=̶ ̶F̶i̶l̶e̶s̶.̶l̶i̶s̶t̶(̶d̶i̶r̶e̶t̶o̶r̶i̶o̶D̶o̶s̶M̶D̶)̶)̶ ̶{̶
a̶r̶q̶u̶i̶v̶o̶s̶M̶D̶
.̶f̶i̶l̶t̶e̶r̶(̶m̶a̶t̶c̶h̶e̶r̶:̶:̶m̶a̶t̶c̶h̶e̶s̶)̶
.̶s̶o̶r̶t̶e̶d̶(̶)̶
.̶f̶o̶r̶E̶a̶c̶h̶(̶a̶r̶q̶u̶i̶v̶o̶M̶D̶ ̶-̶>̶ ̶{̶
CapituloBuilder capituloBuilder = new CapituloBuilder();
Parser parser = Parser.builder().build();
Node document = null;
try {
document = parser.parseReader(Files.newBufferedReader(arquivoMD));
// código omitido...
} catch (Exception ex) {
throw new RuntimeException("Erro ao fazer parse do arquivo " + arquivoMD, ex);
}
try {
HtmlRenderer renderer = HtmlRenderer.builder().build();
String html = renderer.render(document);
// código omitido...
Capitulo capitulo = capituloBuilder.constroi();
capitulos.add(capitulo);
} catch (Exception ex) {
throw new RuntimeException("Erro ao renderizar para HTML o arquivo " + arquivoMD, ex);
}
}̶)̶;̶
}̶ ̶c̶a̶t̶c̶h̶ ̶(̶I̶O̶E̶x̶c̶e̶p̶t̶i̶o̶n̶ ̶e̶x̶)̶ ̶{̶
t̶h̶r̶o̶w̶ ̶n̶e̶w̶ ̶R̶u̶n̶t̶i̶m̶e̶E̶x̶c̶e̶p̶t̶i̶o̶n̶(̶"̶E̶r̶r̶o̶ ̶t̶e̶n̶t̶a̶n̶d̶o̶ ̶e̶n̶c̶o̶n̶t̶r̶a̶r̶ ̶a̶r̶q̶u̶i̶v̶o̶s̶ ̶.̶m̶d̶ ̶e̶m̶ ̶"̶ ̶+̶ ̶d̶i̶r̶e̶t̶o̶r̶i̶o̶D̶o̶s̶M̶D̶.̶t̶o̶A̶b̶s̶o̶l̶u̶t̶e̶P̶a̶t̶h̶(̶)̶,̶ ̶e̶x̶)̶;̶
}̶
return capitulos;
}
}
- Use o método
obtemMDsDosCapitulos
deRepositorioDeMDs
para adquirir os MDs a serem renderizados para HTML. Altere os trechos de código que dependem de arquivos. Mude as mensagens das exceções.
####### cotuba.md.RenderizadorMDParaHTMLComCommonMark
public class RenderizadorMDParaHTMLComCommonMark implements RenderizadorMDParaHTML {
@Override
public List<Capitulo> renderiza(RepositorioDeMDs repositorioDeMDs) {
List<Capitulo> capitulos = new ArrayList<>();
for (String md : repositorioDeMDs.obtemMDsDosCapitulos()) { // inserido
CapituloBuilder capituloBuilder = new CapituloBuilder();
Parser parser = Parser.builder().build();
Node document = null;
try {
d̶o̶c̶u̶m̶e̶n̶t̶ ̶=̶ ̶p̶a̶r̶s̶e̶r̶.̶p̶a̶r̶s̶e̶R̶e̶a̶d̶e̶r̶(̶F̶i̶l̶e̶s̶.̶n̶e̶w̶B̶u̶f̶f̶e̶r̶e̶d̶R̶e̶a̶d̶e̶r̶(̶a̶r̶q̶u̶i̶v̶o̶M̶D̶)̶)̶;̶
document = parser.parse(md);
// código omitido...
} catch (Exception ex) {
t̶h̶r̶o̶w̶ ̶n̶e̶w̶ ̶R̶u̶n̶t̶i̶m̶e̶E̶x̶c̶e̶p̶t̶i̶o̶n̶(̶"̶E̶r̶r̶o̶ ̶a̶o̶ ̶f̶a̶z̶e̶r̶ ̶p̶a̶r̶s̶e̶ ̶d̶o̶ ̶a̶r̶q̶u̶i̶v̶o̶ ̶"̶ ̶+̶ ̶a̶r̶q̶u̶i̶v̶o̶M̶D̶,̶ ̶e̶x̶)̶;̶
throw new RuntimeException("Erro ao fazer parse de markdown ", ex);
}
try {
HtmlRenderer renderer = HtmlRenderer.builder().build();
String html = renderer.render(document);
// código omitido...
Capitulo capitulo = capituloBuilder.constroi();
capitulos.add(capitulo);
} catch (Exception ex) {
t̶h̶r̶o̶w̶ ̶n̶e̶w̶ ̶R̶u̶n̶t̶i̶m̶e̶E̶x̶c̶e̶p̶t̶i̶o̶n̶(̶"̶E̶r̶r̶o̶ ̶a̶o̶ ̶r̶e̶n̶d̶e̶r̶i̶z̶a̶r̶ ̶p̶a̶r̶a̶ ̶H̶T̶M̶L̶ ̶o̶ ̶a̶r̶q̶u̶i̶v̶o̶ ̶"̶ ̶+̶ ̶a̶r̶q̶u̶i̶v̶o̶M̶D̶,̶ ̶e̶x̶)̶;̶
throw new RuntimeException("Erro ao renderizar MD para HTML", ex);
}
} // inserido
return capitulos;
}
}
Limpe os imports desnecessários:
####### cotuba.md.RenderizadorMDParaHTMLComCommonMark
i̶m̶p̶o̶r̶t̶ ̶j̶a̶v̶a̶.̶i̶o̶.̶I̶O̶E̶x̶c̶e̶p̶t̶i̶o̶n̶;̶
i̶m̶p̶o̶r̶t̶ ̶j̶a̶v̶a̶.̶n̶i̶o̶.̶f̶i̶l̶e̶.̶F̶i̶l̶e̶S̶y̶s̶t̶e̶m̶s̶;̶
i̶m̶p̶o̶r̶t̶ ̶j̶a̶v̶a̶.̶n̶i̶o̶.̶f̶i̶l̶e̶.̶F̶i̶l̶e̶s̶;̶
i̶m̶p̶o̶r̶t̶ ̶j̶a̶v̶a̶.̶n̶i̶o̶.̶f̶i̶l̶e̶.̶P̶a̶t̶h̶;̶
i̶m̶p̶o̶r̶t̶ ̶j̶a̶v̶a̶.̶n̶i̶o̶.̶f̶i̶l̶e̶.̶P̶a̶t̶h̶M̶a̶t̶c̶h̶e̶r̶;̶
i̶m̶p̶o̶r̶t̶ ̶j̶a̶v̶a̶.̶u̶t̶i̶l̶.̶s̶t̶r̶e̶a̶m̶.̶S̶t̶r̶e̶a̶m̶;̶
- Ajuste o método
executa
da classeCotuba
, para receber umRepositorioDeMDs
como parâmetro. Remova odiretorioDosMD
e use esse novo parâmetro.
####### cotuba.application.Cotuba
public class Cotuba {
p̶u̶b̶l̶i̶c̶ ̶v̶o̶i̶d̶ ̶e̶x̶e̶c̶u̶t̶a̶(̶P̶a̶r̶a̶m̶e̶t̶r̶o̶s̶C̶o̶t̶u̶b̶a̶ ̶p̶a̶r̶a̶m̶e̶t̶r̶o̶s̶,̶ ̶C̶o̶n̶s̶u̶m̶e̶r̶<̶S̶t̶r̶i̶n̶g̶>̶ ̶a̶c̶a̶o̶P̶o̶s̶G̶e̶r̶a̶c̶a̶o̶)̶ ̶{̶
public void executa(ParametrosCotuba parametros, Consumer<String> acaoPosGeracao, RepositorioDeMDs repositorioDeMDs) { // modificado
FormatoEbook formato = parametros.getFormato();
P̶a̶t̶h̶ ̶d̶i̶r̶e̶t̶o̶r̶i̶o̶D̶o̶s̶M̶D̶ ̶=̶ ̶p̶a̶r̶a̶m̶e̶t̶r̶o̶s̶.̶g̶e̶t̶D̶i̶r̶e̶t̶o̶r̶i̶o̶D̶o̶s̶M̶D̶(̶)̶;̶
Path arquivoDeSaida = parametros.getArquivoDeSaida();
RenderizadorMDParaHTML renderizador = RenderizadorMDParaHTML.cria();
L̶i̶s̶t̶<̶C̶a̶p̶i̶t̶u̶l̶o̶>̶ ̶c̶a̶p̶i̶t̶u̶l̶o̶s̶ ̶=̶ ̶r̶e̶n̶d̶e̶r̶i̶z̶a̶d̶o̶r̶.̶r̶e̶n̶d̶e̶r̶i̶z̶a̶(̶d̶i̶r̶e̶t̶o̶r̶i̶o̶D̶o̶s̶M̶D̶)̶;̶
List<Capitulo> capitulos = renderizador.renderiza(repositorioDeMDs);
// código omitido...
}
}
O código do módulo cotuba-core
deve ser compilado com sucesso.
Já os módulos cotuba-cli
e cotuba-web
ainda devem apresentar erros de compilação.
No módulo cotuba-cli
, defina uma implementação de RepositorioDeMDs
, obtendo os MDs dos diretórios.
Passe um objeto dessa nova classe para o Cotuba
.
Faça outros ajustes necessários.
- No pacote
cotuba.cli
do módulocotuba-cli
, crie a classeMDsDoDiretorio
que implementa a interfaceRepositorioDeMDs
.
Receba, no construtor, um Path
com o diretório onde estão os MDs, armazenando-o em um atributo.
####### cotuba.cli.MDsDoDiretorio
public class MDsDoDiretorio implements RepositorioDeMDs {
private Path diretorioDosMD;
public MDsDoDiretorio(Path diretorioDosMD) {
this.diretorioDosMD = diretorioDosMD;
}
@Override
public List<String> obtemMDsDosCapitulos() {
List<String> mds = new ArrayList<>();
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**/*.md");
try (Stream<Path> arquivosMD = Files.list(diretorioDosMD)) {
arquivosMD
.filter(matcher::matches)
.sorted()
.forEach(arquivoMD -> {
try {
String md = new String(Files.readAllBytes(arquivoMD));
mds.add(md);
} catch (Exception ex) {
throw new RuntimeException("Erro ao ler arquivo " + arquivoMD, ex);
}
});
return mds;
} catch (IOException ex) {
throw new RuntimeException("Erro tentando encontrar arquivos .md em " + diretorioDosMD.toAbsolutePath(),
ex);
}
}
}
Não deixe de adicionar os imports corretos:
####### cotuba.cli.MDsDoDiretorio
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import cotuba.application.RepositorioDeMDs;
- Remova a anotação
@Override
do métodogetDiretorioDosMD
da classeLeitorOpcoesCLI
. O método não está mais definido na interfaceParametrosCotuba
.
####### cotuba.cli.LeitorOpcoesCLI
public class LeitorOpcoesCLI implements ParametrosCotuba {
// código omitido...
@̶O̶v̶e̶r̶r̶i̶d̶e̶
public Path getDiretorioDosMD() {
return diretorioDosMD;
}
// código omitido...
}
- Na classe
Main
, instancieMDsDoDiretorio
com o diretório que contém os MDs e passe essa instância para a classeCotuba
.
####### cotuba.cli.Main
public class Main {
public static void main(String[] args) {
LeitorOpcoesCLI opcoesCLI = new LeitorOpcoesCLI(args);
Path arquivoDeSaida = opcoesCLI.getArquivoDeSaida();
boolean modoVerboso = opcoesCLI.isModoVerboso();
Path diretorioDosMD = opcoesCLI.getDiretorioDosMD(); // inserido
try {
RepositorioDeMDs repositorio = new MDsDoDiretorio(diretorioDosMD); // inserido
Cotuba cotuba = new Cotuba();
c̶o̶t̶u̶b̶a̶.̶e̶x̶e̶c̶u̶t̶a̶(̶o̶p̶c̶o̶e̶s̶C̶L̶I̶,̶ ̶S̶y̶s̶t̶e̶m̶.̶o̶u̶t̶:̶:̶p̶r̶i̶n̶t̶l̶n̶)̶;̶
cotuba.executa(opcoesCLI, System.out::println, repositorio);
System.out.println("Arquivo gerado com sucesso: " + arquivoDeSaida);
} catch (Exception ex) {
// código omitido...
}
}
}
Defina o import:
####### cotuba.cli.Main
import cotuba.application.RepositorioDeMDs;
- Teste a geração de PDFs e EPUBs pela linha de comando. Deve funcionar!
Faça a chamada da classe Cotuba
a partir do módulo cotuba-web
.
Define implementações adequadas para as interfaces ParametrosCotuba
e para RepositorioDeMDs
.
- Crie a classe
ParametrosCotubaWeb
, no pacotecotuba.web.application
do módulocotuba-web
, que implementa a interfaceParametrosCotuba
. No construtor, receba o formato do ebook a ser gerado. Defina o arquivo de saída em um diretório temporário.
####### cotuba.web.application.ParametrosCotubaWeb
public class ParametrosCotubaWeb implements ParametrosCotuba {
private FormatoEbook formato;
private Path arquivoDeSaida;
public ParametrosCotubaWeb(FormatoEbook formato) {
this.formato = formato;
}
@Override
public FormatoEbook getFormato() {
return formato;
}
@Override
public Path getArquivoDeSaida() {
if (arquivoDeSaida != null) {
return arquivoDeSaida;
}
try {
Path diretorio = Files.createTempDirectory("ebooks");
String nomeDoArquivoDeSaida = "book." + formato.name().toLowerCase();
this.arquivoDeSaida = diretorio.resolve(nomeDoArquivoDeSaida);
return arquivoDeSaida;
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
Não deixe de fazer os imports corretos:
####### cotuba.web.application.ParametrosCotubaWeb
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import cotuba.application.ParametrosCotuba;
import cotuba.domain.FormatoEbook;
- Defina a classe
MDsDoBancoDeDados
, também no pacotecotuba.web.application
, com um construtor que recebe umLivro
.
Nessa classe, implemente RepositorioDeMDs
. No método obtemMDsDosCapitulos
, monte uma lista com os MD dos capítulos do livro.
Faça com que o MD comece com o nome do capítulo como título (#
).
####### cotuba.web.application.MDsDoBancoDeDados
public class MDsDoBancoDeDados implements RepositorioDeMDs {
private Livro livro;
public MDsDoBancoDeDados(Livro livro) {
this.livro = livro;
}
@Override
public List<String> obtemMDsDosCapitulos() {
List<String> mds = new ArrayList<>();
for (Capitulo capitulo : livro.getCapitulos()) {
String md ="# " + capitulo.getNome() + "\n";
md += capitulo.getMarkdown();
mds.add(md);
}
return mds;
}
}
As classes Livro
e Capitulo
devem ser as do pacote cotuba.web.domain
.
Adicione os imports adequados:
####### cotuba.web.application.MDsDoBancoDeDados
import java.util.ArrayList;
import java.util.List;
import cotuba.application.RepositorioDeMDs;
import cotuba.web.domain.Capitulo;
import cotuba.web.domain.Livro;
- Na classe
GeracaoDeLivros
, do pacotecotuba.web.application
, chame oCotuba
passando como parâmetrosParametrosCotubaWeb
eMDsDoBancoDeDados
. AacaoPosGeracao
deve imprimir os resultados no console do servidor.
@Service
public class GeracaoDeLivros {
// código omitido...
public Path geraLivro(Long id, FormatoEbook formato) {
Livro livro = livros.detalha(id);
Cotuba cotuba = new Cotuba();
ParametrosCotuba parametros = new ParametrosCotubaWeb(formato);
RepositorioDeMDs mdsDoBD = new MDsDoBancoDeDados(livro);
cotuba.executa(parametros, System.out::println, mdsDoBD);
return parametros.getArquivoDeSaida();
}
}
Defina os imports necessários:
import cotuba.application.Cotuba;
import cotuba.application.ParametrosCotuba;
import cotuba.application.RepositorioDeMDs;
- Certifique-se que o servidor Web está rodando. Se não estiver, execute a classe
Boot
.
Teste a geração do PDF e do EPUB pela Web usando os botões apropriados da seguinte URL:
http://localhost:8080/livros/1
Deve funcionar!