Queremos estilizar cada capítulo, por meio de CSS. Esse estilo deverá ser inserido no HTML que foi renderizado a partir do Markdown.
Para isso, criaremos uma classe responsável por aplicar um tema. Essa classe recebe um capítulo e, usando o Jsoup, insere o estilo CSS no HTML.
Jsoup é uma biblioteca feita em Java que provê uma API baseada no jQuery para manipular HTML.
Considere o HTML a seguir:
<div class="curso">
<h2 class="curso__titulo">Curso Design de código SOLID em Java</h2>
<p class="curso__info"><span>20</span> horas/aula</p>
</div>
Podemos usar o Jsoup para extrair texto e até mudar o HTML:
String html = //...
Document doc = Jsoup.parse(html);
Elements info = doc.select(".curso__info"); // <p>
String texto = info.text(); // "20 horas/aula"
doc.select(".curso").append("<a href=\"#turmas\">Turmas</a>"); // adiciona link depois do <p>
String novoHtml = doc.html(); // HTML atualizado
Crie uma classe AplicadorTema
, no pacote cotuba.tema
, que recebe um Capitulo
e insere uma borda tracejada abaixo do título do capítulo.
Use a biblioteca Jsoup.
- No
pom.xml
, declare como dependência a biblioteca Jsoup:
####### cotuba-cli/pom.xml
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.11.2</version>
</dependency>
- Crie a classe
AplicadorTema
em um novo pacotecotuba.tema
e defina o métodoaplica
, que recebe umCapitulo
como parâmetro.
####### cotuba.tema.AplicadorTema
package cotuba.tema;
import cotuba.domain.Capitulo;
public class AplicadorTema {
public void aplica(Capitulo capitulo) {
}
}
- Implemente o método
aplica
, usando a biblioteca Jsoup e definido o CSS apropriado:
####### cotuba.tema.AplicadorTema
String html = capitulo.getConteudoHTML();
Document document = Jsoup.parse(html);
String css = "h1 { border-bottom: 1px dashed black; }";
document.select("head").append("<style> " + css + " </style>");
capitulo.setConteudoHTML(document.html());
Certifique-se que os imports estão corretos:
####### cotuba.tema.AplicadorTema
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import cotuba.domain.Capitulo;
Quem deve chamar o AplicadorTema
?
Temos duas opções:
- a classe
RenderizadorMDParaHTML
chama oAplicadorTema
logo após renderizar cada capítulo
- a classe
Cotuba
chama oAplicadorTema
, depois de receber a lista de capítulos deRenderizadorMDParaHTML
Se pensarmos em termos de responsabilidades, qual decisão é a mais acertada?
Ao fazermos RenderizadorMDParaHTML
invocar AplicadorTema
, estaríamos adicionando mais uma responsabilidade a essa classe: disparar a aplicação do estilo, além de renderizar o Markdown para HTML. Por outro lado, aplicar um CSS está relacionado com a geração do HTML, que é a responsabilidade de RenderizadorMDParaHTML
.
Já se fizermos Cotuba
invocar AplicadorTema
, seria mais uma classe para ser coordenada.
E considerando as dependências? O que seria mais correto?
Precisamos considerar se a classe AplicadorTema
é de alto ou baixo nível. É algo um tanto subjetivo. Aplicar temas é ou não parte da regra de negócio do nosso gerador de ebooks?
Outra questão é que, ao colocarmos AplicadorTema
como dependência de RenderizadorMDParaHTML
, passaríamos a ter dependências com classes do Cotuba, do Java NIO, do CommonMark e, agora, com a nova classe.
Se colocarmos como dependência de Cotuba
, teríamos dependências com AplicadorTema
, além de com as classes de domínio Ebook
e Capitulo
e
as abstrações ParametrosCotuba
, RenderizadorMDParaHTML
, GeradorPDF
e GeradorEPUB
. Se considerarmos AplicadorTema
de baixo nível teríamos que inverter a dependência, criando uma nova abstração.
Não há uma resposta certa em um design. Podemos caminhar para um lado ou para outro. Se detectarmos que o caminho escolhido não é o melhor, podemos refatorar, melhorando o design.
No nosso caso, vamos fazer com que RenderizadorMDParaHTML
chame AplicadorTema
, considerando que a responsabilidade dessa classe é relacionada a um detalhe da geração do HTML e é de baixo nível.
Faça com que RenderizadorMDParaHTML
chame AplicadorTema
para cada capítulo, logo depois de renderizar o HTML.
- Na classe
RenderizadorMDParaHTMLComCommonMark
, instancie deAplicadorTema
e invoque o métodoaplica
passando oCapitulo
, logo depois de setar o HTML:
####### cotuba.md.RenderizadorMDParaHTMLComCommonMark
HtmlRenderer renderer = HtmlRenderer.builder().build();
String html = renderer.render(document);
capitulo.setConteudoHTML(html);
AplicadorTema tema = new AplicadorTema(); // inserido
tema.aplica(capitulo); // inserido
capitulos.add(capitulo);
Não esqueça de fazer o import:
####### cotuba.md.RenderizadorMDParaHTMLComCommonMark
import cotuba.tema.AplicadorTema;
- Teste a geração do PDF e do EPUB. Veja a borda nos títulos dos capítulos!
A empresa Paradizo, nossa cliente, quer definir seu próprio tema.
Para isso, os desenvolvedores da Paradizo querem inserir seu próprio CSS no HTML de cada capítulo.
Uma opção seria pedir que o time da Paradizo nos mandasse um arquivo .css
. No código do Cotuba, aplicaríamos os estilos deles no HTML dos capítulos.
Mas temos outros clientes! Alguns desses clientes também querem seus temas customizados, com seu próprio CSS. Outros não querem nenhum tema.
É inviável incluir, no código do Cotuba, os estilos CSS de todos os clientes, além de uma lista de quais não têm nenhum estilo.
Uma outra opção seria pedir que os clientes fornecessem JARs contendo classes que retornariam o CSS.
Seriam classes que estariam em outros JARs, fora do cotuba-cli.jar
.
Mas não sabemos quais são esses outros JARs nem o nome das classes e métodos que precisamos chamar. Não sabemos nem se esses JARs existem. Não podemos depender deles!
Precisamos de pontos de extensão, ou plugins, para o Cotuba. Se existirem, serão aplicados. Mas como implementá-los?
O Cotuba não pode depender das classes de terceiros, mas essas podem depender do Cotuba. Podemos inverter as dependências!
Mais uma vez, é o código de alto nível, do Cotuba, fornecendo abstrações para um código de baixo nível, que fornece detalhes de implementação.
É o DIP!
Para isso, vamos fornecer essa abstração de um ponto de extensão por meio da interface Plugin
.
Defina uma interface Plugin
no pacote cotuba.plugin
. Defina um método chamado cssDoTema
, que retorna uma String
.
- No pacote
cotuba.plugin
, crie a interfacePlugin
, conforme código a seguir:
####### cotuba.plugin.Plugin
package cotuba.plugin;
public interface Plugin {
String cssDoTema();
}
Crie um outro projeto Maven chamado tema-paradizo
.
Defina um recurso com o CSS do tema: uma borda sólida abaixo dos títulos dos capítulos e das seções e uma borda sólida envolvendo as citações.
Implemente a interface Plugin
do Cotuba, retornando o conteúdo do CSS.
- No Eclipse, vá em File > New > Maven Project.
Marque a opção Create a simple project (skip archetype selection).
Desmarque a opção Use default Workspace location.
Em Location, coloque /home/<usuario-do-curso>/tema-paradizo
.
Não esqueça de trocar <usuario-do-curso>
pelo nome de usuário do curso.
Clique em Next.
Na próxima tela, preencha:
- Group Id:
br.com.paradizo
- Artifact Id:
tema-paradizo
Deixe o campo Version como 0.0.1-SNAPSHOT
e Packaging como jar
.
Os demais campos podem ficar vazios.
Clique em Finish.
- No
pom.xml
do novo projeto, declare a codificação de caracteres e a versão do Java.
Declare também o Cotuba como dependência.
####### tema-paradizo/pom.xml
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>cotuba</groupId>
<artifactId>cotuba-cli</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
Para que as configurações tenham efeito, clique com o botão direito no projeto e vá em Maven > Update project.... Selecione o projeto tema-paradizo
e clique em OK.
- Crie um arquivo
tema.css
no diretóriosrc/main/resources
do projetotema-paradizo
, com o seguinte conteúdo:
####### tema-paradizo/src/main/resources/tema.css
h1 {
border-bottom: 1px dashed black;
font-size: 3em;
}
h2 {
border-left: 1px solid black;
padding-left: 5px;
border-bottom: 1px solid black;
}
blockquote {
border: 1px solid black;
padding: 5px;
}
- Crie uma classe
TemaParadizo
no pacotebr.com.paradizo.tema
que implementa a interfacePlugin
do Cotuba:
####### br.com.paradizo.tema.TemaParadizo
package br.com.paradizo.tema;
import cotuba.plugin.Plugin;
public class TemaParadizo implements Plugin {
@Override
public String cssDoTema() {
return null;
}
}
- Para obter o
tema.css
a partir da classeTemaParadizo
, vamos definir uma classe utilitáriaFileUtils
, no mesmo pacotebr.com.paradizo.tema
.
Essa classe ajuda a obter, de maneira simples, o conteúdo de recursos que estão (ou não) em JARs.
Você pode encontrar o código abaixo na seguinte URL: http://bit.ly/fj38-file-utils
####### br.com.paradizo.tema.FileUtils
package br.com.paradizo.tema;
public class FileUtils {
public static String getResourceContents(String resource) {
try {
Path resourcePath = getResourceAsPath(resource);
return getPathContents(resourcePath);
} catch(URISyntaxException | IOException ex) {
throw new RuntimeException(ex);
}
}
private static Path getResourceAsPath(String resource) throws URISyntaxException, IOException {
URI uri = FileUtils.class.getResource(resource).toURI();
if (isResourceInJar(uri)) {
return getResourceFromJar(uri);
} else {
return Paths.get(uri);
}
}
private static boolean isResourceInJar(URI uri) {
return uri.getScheme().equals("jar");
}
private static Path getResourceFromJar(URI fullURI) throws IOException {
String[] uriParts = fullURI.toString().split("!");
URI jarURI = URI.create(uriParts[0]);
FileSystem fs;
try {
fs = FileSystems.newFileSystem(jarURI, Collections.<String, String>emptyMap());
} catch (FileSystemAlreadyExistsException ex) {
fs = FileSystems.getFileSystem(jarURI);
}
String resourceURI = uriParts[1];
return fs.getPath(resourceURI);
}
private static String getPathContents(Path path) throws IOException {
return new String(Files.readAllBytes(path));
}
}
Não esqueça de fazer os imports adequados:
####### br.com.paradizo.tema.FileUtils
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemAlreadyExistsException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
- Na classe
TemaParadizo
, useFileUtils
para obter o conteúdo detema.css
:
####### br.com.paradizo.tema.TemaParadizo
public class TemaParadizo implements Plugin {
@Override
public String cssDoTema() {
return FileUtils.getResourceContents("/tema.css");
}
}
Temos um ponto de extensão no Cotuba através da interface Plugin
.
Temos uma implementação desse ponto de extensão através da classe TemaParadizo
.
Mas como ligar uma coisa com a outra, sem fazer com que o Cotuba dependa do código da Paradizo?
A ideia é que as implementações dos pontos de extensão do Cotuba sejam aplicadas pela simples presença de seus JARs.
Antigamente, na plataforma Java, para ligar um plugin de uma aplicação a uma implementação era necessário:
- criar uma solução caseira usando a Reflection API
- usar bibliotecas como JPF ou PF4J
- usar uma especificação robusta, mas complexa, como OSGi
Porém, a partir do Java SE 6, a própria JRE contém uma solução: a Service Loader API.
Na Service Loader API, um ponto de extensão é chamado de service.
Para provermos um service precisamos de:
- Service Provider Interface (SPI): interfaces ou classes abstratas que definem a assinatura do ponto de extensão. No nosso caso, a interface
Plugin
. - Service Provider: uma implementação da SPI. No nosso caso, a classe
TemaParadizo
.
Para ligar a SPI com seu service provider, o JAR do provider precisa definir o provider configuration file: um arquivo com o nome da SPI dentro da pasta META-INF/services
. O conteúdo desse arquivo deve ser o fully qualified name da classe de implementação.
No projeto que define a SPI, carregamos as implementações usando a classe java.util.ServiceLoader
.
A classe ServiceLoader
possui o método estático load
que recebe uma SPI como parâmetro e, depois de vasculhar os diretórios META-INF/services
dos JARs disponíveis no Classpath, retorna uma instância de ServiceLoader
que contém todas as implementações.
O ServiceLoader
é um Iterable
e, por isso, pode ser percorrido com um for-each. Caso não haja nenhum service provider para a SPI, o ServiceLoader
se comporta como uma lista vazia.
Perceba que uma mesma SPI pode ter vários service providers, o que traz bastante flexibilidade.
Com a Service Loader API, a simples presença de um .jar
que a implemente a abstração do plugin (ou SPI) fará com que o comportamento da aplicação seja estendido, sem precisarmos modificar nenhuma linha de código.
É o OCP ao extremo!
No projeto tema-paradizo
, ligue a SPI Plugin
com o service provider TemaParadizo
.
-
No diretório
src/main/resources
, crie o subdiretórioMETA-INF
e, dentro desse, oservices
. -
Dentro do diretório
src/main/resources/META-INF/services
, crie um arquivocotuba.plugin.Plugin
(assim mesmo, com os pontos). Defina como conteúdo desse arquivo, o nome do service provider:
####### src/main/resources/META-INF/services/cotuba.plugin.Plugin
br.com.paradizo.tema.TemaParadizo
No projeto cotuba-cli
, use a classe ServiceLoader
para carregar o service provider.
Use o tema retornado pelo service provider na classe AplicadorTema
.
- Na interface
Plugin
, defina um método estáticolistaDeTemas
que retorna umaList<String>
:
####### cotuba.plugin.Plugin
public interface Plugin {
String cssDoTema();
static List<String> listaDeTemas() { // inserido
List<String> temas = new ArrayList<>();
return temas;
}
}
####### cotuba.plugin.Plugin
Adicione os imports:
import java.util.ArrayList;
import java.util.List;
- Ainda no método
listaDeTemas
dePlugin
, use a classeServiceLoader
para obter todos os temas dos service providers:
####### cotuba.plugin.Plugin
List<String> temas = new ArrayList<>();
// inserido
ServiceLoader<Plugin> loader = ServiceLoader.load(Plugin.class);
for (Plugin plugin : loader) {
String css = plugin.cssDoTema();
temas.add(css);
}
Não esqueça do import:
####### cotuba.plugin.Plugin
import java.util.ServiceLoader;
- No método
aplica
da classeAplicadorTema
, chame o métodolistaDeTemas
dePlugin
e aplique os CSS retornados no HTML:
####### cotuba.tema.AplicadorTema
public class AplicadorTema {
public void aplica(Capitulo capitulo) {
// código omitido...
S̶t̶r̶i̶n̶g̶ ̶c̶s̶s̶ ̶=̶ ̶"̶h̶1̶ ̶{̶ ̶b̶o̶r̶d̶e̶r̶-̶b̶o̶t̶t̶o̶m̶:̶ ̶1̶p̶x̶ ̶d̶a̶s̶h̶e̶d̶ ̶b̶l̶a̶c̶k̶;̶ ̶}̶"̶;̶
// inserido
List<String> listaDeTemas = Plugin.listaDeTemas();
for (String css : listaDeTemas) {
document.select("head").append("<style> " + css + " </style>");
}
// código omitido...
}
}
Não deixe de fazer o import:
####### cotuba.tema.AplicadorTema
import cotuba.plugin.Plugin;
-
(opcional) No método
listaDeTemas
da classePlugin
, use recursos do Java 8 como Lambdas e Streams para trabalhar com oServiceLoader
. -
(desafio) O método estático
load
, da classeServiceLoader
, vasculha o Classpath em busca de implementações da SPI, o que é um processo lento. Otimize o código armazenando a instância deServiceLoader
retornada.
Gere os JARs dos projetos cotuba-cli
e tema-paradizo
.
Teste a geração de um ebook e veja se o tema foi aplicado.
- Abra um Terminal e entre na pasta do projeto
cotuba-cli
:
cd ~/cotuba
- Faça o build usando o Maven:
mvn install
- Descompacte o
.zip
gerado para o seu Desktop com o comando:
unzip -o target/cotuba-*-distribution.zip -d ~/Desktop
- Vá até o Desktop:
cd ~/Desktop
- Faça o teste de geração do PDF:
./cotuba.sh -d ~/cotuba/exemplo -f pdf
Como ainda não colocamos o JAR do plugin no Classpath, não deve haver nenhum estilo.
- Vá até a pasta do projeto
tema-paradizo
:
cd ~/tema-paradizo
- Faça o build do
tema-paradizo
usando o Maven:
mvn install
- Copie o JAR do plugin
tema-paradizo
para a pastalibs
do Cotuba, que está no Desktop:
cp target/tema-paradizo-*.jar ~/Desktop/libs/
- Volte ao Desktop:
cd ~/Desktop
- Gere o PDF novamente:
./cotuba.sh -d ~/cotuba/exemplo -f pdf
Veja que o tema foi aplicado!
Teste também a geração do EPUB.
O mecanismo de Service Provider já existia internamente desde a JDK 1.3.
A partir do Java SE 6, a Service Loader API ficou pública e disponível para aplicações. A API passou a ser usada em diferentes especificações da plataforma Java.
É possível usar o JPA em uma aplicação Java SE, fora de um servidor de aplicação Java EE.
Para isso, precisamos de uma implementação da interface EntityManager
. Mas como instanciar essa abstração?
EntityManager manager = // ???
A especificação JPA determina que devemos usar a interface EntityManagerFactory
.
Porém, o problema continua: como instanciar a Factory?
EntityManagerFactory factory = // ???
EntityManager manager = factory.createEntityManager();
Pela especificação, para criar uma EntityManagerFactory
, devemos usar um método estático da classe Persistence
, passando o nome de uma Persistence Unit:
EntityManagerFactory factory = Persistence.createEntityManagerFactory("financas");
EntityManager manager = factory.createEntityManager();
Todas essas classes e interfaces são do pacote javax.persistence
.
Como ligá-las com uma implementação do JPA, como o Hibernate ou o Eclipse Link?
Por meio da SPI javax.persistence.spi.PersistenceProvider
.
O Hibernate tem dentro do hibernate-core.jar
o arquivo:
####### META-INF/services/javax.persistence.spi.PersistenceProvider
org.hibernate.jpa.HibernatePersistenceProvider
Já o eclipselink.jar
terá, no mesmo arquivo:
####### META-INF/services/javax.persistence.spi.PersistenceProvider
org.eclipse.persistence.jpa.PersistenceProvider
É interessante notar que a especificação JPA 1.0, parte da EJB 3.0 e Java EE 5, foi lançada para uso com o J2SE 5.0. Portanto, a Service Loader API ainda não era pública. O mecanismo de disponibilização das implementações era responsabilidade do JPA Provider.
Antes do Java SE 6, era necessário carregar programaticamente a implementação da interface java.sql.Driver
de um driver JDBC.
Para isso, antes de obter uma conexão, usávamos um código parecido com o seguinte:
Class.forName("com.mysql.jdbc.Driver");
Esse código aparentemente inútil tinha, como efeito colateral, o carregamento das implementações de Driver
que seriam usadas posteriormente pela classe DriverManager
.
Do Java SE 6 em diante, a classe DriverManager
usa a Service Loader API para carregar automaticamente todos os drivers na inicialização. A chamada anterior ao método forName
de Class
passou a ser desnecessária.
A interface java.sql.Driver
passou a ser uma SPI.
No caso do MySQL, o arquivo mysql-connector-java.jar
passou a ter o arquivo:
####### META-INF/services/java.sql.Driver
com.mysql.jdbc.Driver
Um fato interessante é que o Tomcat 7+ desliga o carregamento automático de drivers via Service Loader API para evitar vazamento de memória.
A partir da especificação Servlet 3.0, parte do Java EE 6, é possível configurar Servlets e Filters sem ter que digitar várias linhas no web.xml
.
Podemos usar usar as anotações @WebServlet
e @WebFilter
em cima de nossas classes.
Mas e para Servlets e Filters de frameworks e bibliotecas, cujo código não conseguimos modificar? Estamos fadados ao web.xml
?
Há a SPI javax.servlet.ServletContainerInitializer
, que define o método onStartup
.
Um Servlet Container compatível com a Servlet 3.0, como o Tomcat 7+ ou Jetty 8+, usa a Service Loader API para carregar e executar as implementações de ServletContainerInitializer
na inicialização do servidor.
O Spring, por exemplo, tem em seu spring-web.jar
o arquivo:
####### META-INF/services/javax.servlet.ServletContainerInitializer
org.springframework.web.SpringServletContainerInitializer
Esse mecanismo faz com que o Spring dispare chamadas a classes como AbstractAnnotationConfigDispatcherServletInitializer
sem a necessidade de configuração XML.