`;
35 | return div;
36 | }
37 |
38 | function calculateShipping(id, cep) {
39 | fetch('http://localhost:3000/shipping/' + cep)
40 | .then((data) => {
41 | if (data.ok) {
42 | return data.json();
43 | }
44 | throw data.statusText;
45 | })
46 | .then((data) => {
47 | swal('Frete', `O frete é: R$${data.value.toFixed(2)}`, 'success');
48 | })
49 | .catch((err) => {
50 | swal('Erro', 'Erro ao consultar frete', 'error');
51 | console.error(err);
52 | });
53 | }
54 |
55 | document.addEventListener('DOMContentLoaded', function () {
56 | const books = document.querySelector('.books');
57 |
58 | fetch('http://localhost:3000/products')
59 | .then((data) => {
60 | if (data.ok) {
61 | return data.json();
62 | }
63 | throw data.statusText;
64 | })
65 | .then((data) => {
66 | if (data) {
67 | data.forEach((book) => {
68 | books.appendChild(newBook(book));
69 | });
70 |
71 | document.querySelectorAll('.button-shipping').forEach((btn) => {
72 | btn.addEventListener('click', (e) => {
73 | const id = e.target.getAttribute('data-id');
74 | const cep = document.querySelector(`.book[data-id="${id}"] input`).value;
75 | calculateShipping(id, cep);
76 | });
77 | });
78 |
79 | document.querySelectorAll('.button-buy').forEach((btn) => {
80 | btn.addEventListener('click', (e) => {
81 | swal('Compra de livro', 'Sua compra foi realizada com sucesso', 'success');
82 | });
83 | });
84 | }
85 | })
86 | .catch((err) => {
87 | swal('Erro', 'Erro ao listar os produtos', 'error');
88 | console.error(err);
89 | });
90 | });
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Micro-Livraria: Exemplo Prático de Microsserviços
2 |
3 | Este repositório contem um exemplo simples de uma livraria virtual construída usando uma **arquitetura de microsserviços**.
4 |
5 | O exemplo foi projetado para ser usado em uma **aula prática sobre microsserviços**, que pode, por exemplo, ser realizada após o estudo do [Capítulo 7](https://engsoftmoderna.info/cap7.html) do livro [Engenharia de Software Moderna](https://engsoftmoderna.info).
6 |
7 | O objetivo da aula é permitir que o aluno tenha um primeiro contato com microsserviços e com tecnologias normalmente usadas nesse tipo de arquitetura, tais como **Node.js**, **REST**, **gRPC** e **Docker**.
8 |
9 | Como nosso objetivo é didático, na livraria virtual estão à venda apenas três livros, conforme pode ser visto na próxima figura, que mostra a interface Web do sistema. Além disso, a operação de compra apenas simula a ação do usuário, não efetuando mudanças no estoque. Assim, os clientes da livraria podem realizar apenas duas operações: (1) listar os produtos à venda; (2) calcular o frete de envio.
10 |
11 |
12 |
13 |
14 |
15 | No restante deste documento vamos:
16 |
17 | - Descrever o sistema, com foco na sua arquitetura.
18 | - Apresentar instruções para sua execução local, usando o código disponibilizado no repositório.
19 | - Descrever duas tarefas práticas para serem realizadas pelos alunos, as quais envolvem:
20 | - Tarefa Prática #1: Implementação de uma nova operação em um dos microsserviços
21 | - Tarefa Prática #2: Criação de containers Docker para facilitar a execução dos microsserviços.
22 |
23 | ## Arquitetura
24 |
25 | A micro-livraria possui quatro microsserviços:
26 |
27 | - Front-end: microsserviço responsável pela interface com usuário, conforme mostrado na figura anterior.
28 | - Controller: microsserviço responsável por intermediar a comunicação entre o front-end e o backend do sistema.
29 | - Shipping: microserviço para cálculo de frete.
30 | - Inventory: microserviço para controle do estoque da livraria.
31 |
32 | Os quatro microsserviços estão implementados em **JavaScript**, usando o Node.js para execução dos serviços no back-end.
33 |
34 | No entanto, **você conseguirá completar as tarefas práticas mesmo se nunca programou em JavaScript**. O motivo é que o nosso roteiro já inclui os trechos de código que devem ser copiados para o sistema.
35 |
36 | Para facilitar a execução e entendimento do sistema, também não usamos bancos de dados ou serviços externos.
37 |
38 | ## Protocolos de Comunicação
39 |
40 | Como ilustrado no diagrama a seguir, a comunicação entre o front-end e o backend usa uma **API REST**, como é comum no caso de sistemas Web.
41 |
42 | Já a comunicação entre o Controller e os microsserviços do back-end é baseada em [gRPC](https://grpc.io/).
43 |
44 |
45 |
46 |
47 |
48 | Optamos por usar gRPC no backend porque ele possui um desempenho melhor do que REST. Especificamente, gRPC é baseado no conceito de **Chamada Remota de Procedimentos (RPC)**. A ideia é simples: em aplicações distribuídas que usam gRPC, um cliente pode chamar funções implementadas em outros processos de forma transparente, isto é, como se tais funções fossem locais. Em outras palavras, chamadas gRPC tem a mesma sintaxe de chamadas normais de função.
49 |
50 | Para viabilizar essa transparência, gRPC usa dois conceitos centrais:
51 |
52 | - uma linguagem para definição de interfaces
53 | - um protocolo para troca de mensagens entre aplicações clientes e servidoras.
54 |
55 | Especificamente, no caso de gRPC, a implementação desses dois conceitos ganhou o nome de **Protocol Buffer**. Ou seja, podemos dizer que:
56 |
57 | > Protocol Buffer = linguagem para definição de interfaces + protocolo para definição das mensagens trocadas entre aplicações clientes e servidoras
58 |
59 | ### Exemplo de Arquivo .proto
60 |
61 | Quando trabalhamos com gRPC, cada microserviço possui um arquivo `.proto` que define a assinatura das operações que ele disponibiliza para os outros microsserviços.
62 | Neste mesmo arquivo, declaramos também os tipos dos parâmetros de entrada e saída dessas operações.
63 |
64 | O exemplo a seguir mostra o arquivo [.proto](https://github.com/aserg-ufmg/micro-livraria/blob/main/proto/shipping.proto) do nosso microsserviço de frete. Nele, definimos que esse microsserviço disponibiliza uma função `GetShippingRate`. Para chamar essa função devemos passar como parâmetro de entrada um objeto contendo o CEP (`ShippingPayLoad`). Após sua execução, a função retorna como resultado um outro objeto (`ShippingResponse`) com o valor do frete.
65 |
66 |
67 |
68 |
69 |
70 | Em gRPC, as mensagens (exemplo: `Shippingload`) são formadas por um conjunto de campos, tal como em um `struct` da linguagem C, por exemplo. Todo campo possui um nome (exemplo: `cep`) e um tipo (exemplo: `string`). Além disso, todo campo tem um número inteiro que funciona como um identificador único para o mesmo na mensagem (exemplo: ` = 1`). Esse número é usado pela implementação de gRPC para identificar o campo no formato binário de dados usado por gRPC para comunicação distribuída.
71 |
72 | Arquivos .proto são usados para gerar **stubs**, que nada mais são do que proxies que encapsulam os detalhes de comunicação em rede, incluindo troca de mensagens, protocolos, etc. Mais detalhes sobre o padrão de projeto Proxy podem ser obtidos no [Capítulo 6](https://engsoftmoderna.info/cap6.html).
73 |
74 | Em linguagens estáticas, normalmente precisa-se chamar um compilador para gerar o código de tais stubs. No caso de JavaScript, no entanto, esse passo não é necessário, pois os stubs são gerados de forma transparente, em tempo de execução.
75 |
76 | ## Executando o Sistema
77 |
78 | A seguir vamos descrever a sequência de passos para você executar o sistema localmente em sua máquina. Ou seja, todos os microsserviços estarão rodando na sua máquina.
79 |
80 | **IMPORTANTE:** Você deve seguir esses passos antes de implementar as tarefas práticas descritas nas próximas seções.
81 |
82 | 1. Faça um fork do repositório. Para isso, basta clicar no botão **Fork** no canto superior direito desta página.
83 |
84 | 2. Vá para o terminal do seu sistema operacional e clone o projeto (lembre-se de incluir o seu usuário GitHub na URL antes de executar)
85 |
86 | ```
87 | git clone https://github.com//micro-livraria.git
88 | ```
89 |
90 | 3. É também necessário ter o Node.js instalado na sua máquina. Se você não tem, siga as instruções para instalação contidas nessa [página](https://nodejs.org/en/download/).
91 |
92 | 4. Em um terminal, vá para o diretório no qual o projeto foi clonado e instale as dependências necessárias para execução dos microsserviços:
93 |
94 | ```
95 | cd micro-livraria
96 | npm install
97 | ```
98 |
99 | 5. Inicie os microsserviços através do comando:
100 |
101 | ```
102 | npm run start
103 | ```
104 |
105 | 6. Para fins de teste, efetue uma requisição para o microsserviço responsável pela API do backend.
106 |
107 | - Se tiver o `curl` instalado na sua máquina, basta usar:
108 |
109 | ```
110 | curl -i -X GET http://localhost:3000/products
111 | ```
112 |
113 | - Caso contrário, você pode fazer uma requisição acessando, no seu navegador, a seguinte URL: `http://localhost:3000/products`.
114 |
115 | 7. Teste agora o sistema como um todo, abrindo o front-end em um navegador: http://localhost:5000. Faça então um teste das principais funcionalidades da livraria.
116 |
117 | ## Tarefa Prática #1: Implementando uma Nova Operação
118 |
119 | Nesta primeira tarefa, você irá implementar uma nova operação no serviço `Inventory`. Essa operação, chamada `SearchProductByID` vai pesquisar por um produto, dado o seu ID.
120 |
121 | Como descrito anteriormente, as assinaturas das operações de cada microsserviço são definidas em um arquivo `.proto`, no caso [proto/inventory.proto](https://github.com/aserg-ufmg/micro-livraria/blob/main/proto/inventory.proto).
122 |
123 | #### Passo 1
124 |
125 | Primeiro, você deve declarar a assinatura da nova operação. Para isso, inclua a definição dessa assinatura no referido arquivo `.proto` (na linha logo após a assinatura da função `SearchAllProducts`):
126 |
127 | ```proto
128 | service InventoryService {
129 | rpc SearchAllProducts(Empty) returns (ProductsResponse) {}
130 | rpc SearchProductByID(Payload) returns (ProductResponse) {}
131 | }
132 | ```
133 |
134 | Em outras palavras, você está definindo que o microsserviço `Inventory` vai responder a uma nova requisição, chamada `SearchProductByID`, que tem como parâmetro de entrada um objeto do tipo `Payload` e como parâmetro de saída um objeto do tipo `ProductResponse`.
135 |
136 | #### Passo 2
137 |
138 | Inclua também no mesmo arquivo a declaração do tipo do objeto `Payload`, o qual apenas contém o ID do produto a ser pesquisado.
139 |
140 | ```proto
141 | message Payload {
142 | int32 id = 1;
143 | }
144 | ```
145 |
146 | Veja que `ProductResponse` -- isto é, o tipo de retorno da operação -- já está declarado mais abaixo no arquivo `proto`:
147 |
148 | ```proto
149 | message ProductsResponse {
150 | repeated ProductResponse products = 1;
151 | }
152 | ```
153 |
154 | Ou seja, a resposta da nossa requisição conterá um único campo, do tipo `ProductResponse`, que também já está implementando no mesmo arquivo:
155 |
156 | ```proto
157 | message ProductResponse {
158 | int32 id = 1;
159 | string name = 2;
160 | int32 quantity = 3;
161 | float price = 4;
162 | string photo = 5;
163 | string author = 6;
164 | }
165 | ```
166 |
167 | #### Passo 3
168 |
169 | Agora você deve implementar a função `SearchProductByID` no arquivo [services/inventory/index.js](https://github.com/aserg-ufmg/micro-livraria/blob/main/services/inventory/index.js).
170 |
171 | Reforçando, no passo anterior, apenas declaramos a assinatura dessa função. Então, agora, vamos prover uma implementação para ela.
172 |
173 | Para isso, você precisa implementar a função requerida pelo segundo parâmetro da função `server.addService`, localizada na linha 17 do arquivo [services/inventory/index.js](https://github.com/aserg-ufmg/micro-livraria/blob/main/services/inventory/index.js).
174 |
175 | De forma semelhante à função `SearchAllProducts`, que já está implementada, você deve adicionar o corpo da função `SearchProductByID` com a lógica de pesquisa de produtos por ID. Este código deve ser adicionado logo após o `SearchAllProducts` na linha 23.
176 |
177 | ```js
178 | SearchProductByID: (payload, callback) => {
179 | callback(
180 | null,
181 | products.find((product) => product.id == payload.request.id)
182 | );
183 | },
184 | ```
185 |
186 | A função acima usa o método `find` para pesquisar em `products` pelo ID de produto fornecido. Veja que:
187 |
188 | - `payload` é o parâmetro de entrada do nosso serviço, conforme definido antes no arquivo .proto (passo 2). Ele armazena o ID do produto que queremos pesquisar. Para acessar esse ID basta escrever `payload.request.id`.
189 |
190 | - `product` é uma unidade de produto a ser pesquisado pela função `find` (nativa de JavaScript). Essa pesquisa é feita em todos os items da lista de produtos até que um primeiro `product` atenda a condição de busca, isto é `product.id == payload.request.id`.
191 |
192 | - [products](https://github.com/aserg-ufmg/micro-livraria/blob/main/services/inventory/products.json) é um arquivo JSON que contém a descrição dos livros à venda na livraria.
193 |
194 | - `callback` é uma função que deve ser invocada com dois parâmetros:
195 | - O primeiro parâmetro é um objeto de erro, caso ocorra. No nosso exemplo nenhum erro será retornado, portanto `null`.
196 | - O segundo parâmetro é o resultado da função, no nosso caso um `ProductResponse`, assim como definido no arquivo [proto/inventory.proto](https://github.com/aserg-ufmg/micro-livraria/blob/main/proto/inventory.proto).
197 |
198 | #### Passo 4
199 |
200 | Para finalizar, temos que incluir a função `SearchProductByID` em nosso `Controller`. Para isso, você deve incluir uma nova rota `/product/{id}` que receberá o ID do produto como parâmetro. Na definição da rota, você deve também incluir a chamada para o método definido no Passo 3.
201 |
202 | Sendo mais específico, o seguinte trecho de código deve ser adicionado na linha 44 do arquivo [services/controller/index.js](https://github.com/aserg-ufmg/micro-livraria/blob/main/services/controller/index.js), logo após a rota `/shipping/:cep`.
203 |
204 | ```js
205 | app.get('/product/:id', (req, res, next) => {
206 | // Chama método do microsserviço.
207 | inventory.SearchProductByID({ id: req.params.id }, (err, product) => {
208 | // Se ocorrer algum erro de comunicação
209 | // com o microsserviço, retorna para o navegador.
210 | if (err) {
211 | console.error(err);
212 | res.status(500).send({ error: 'something failed :(' });
213 | } else {
214 | // Caso contrário, retorna resultado do
215 | // microsserviço (um arquivo JSON) com os dados
216 | // do produto pesquisado
217 | res.json(product);
218 | }
219 | });
220 | });
221 | ```
222 |
223 | Finalize, efetuando uma chamada no novo endpoint da API: http://localhost:3000/product/1
224 |
225 | Para ficar claro: até aqui, apenas implementamos a nova operação no backend. A sua incorporação no frontend ficará pendente, pois requer mudar a interface Web, para, por exemplo, incluir um botão "Pesquisar Livro".
226 |
227 | **IMPORTANTE**: Se tudo funcionou corretamente, dê um **COMMIT & PUSH** (e certifique-se de que seu repositório no GitHub foi atualizado; isso é fundamental para seu trabalho ser devidamente corrigido).
228 |
229 | ```bash
230 | git add --all
231 | git commit -m "Tarefa prática #1 - Microservices"
232 | git push origin main
233 | ```
234 |
235 | ## Tarefa Prática #2: Criando um Container Docker
236 |
237 | Nesta segunda tarefa, você irá criar um container Docker para o seu microserviço. Os containers são importantes para isolar e distribuir os microserviços em ambientes de produção. Em outras palavras, uma vez "copiado" para um container, um microsserviço pode ser executado em qualquer ambiente, seja ele sua máquina local, o servidor de sua universidade, ou um sistema de cloud (como Amazon AWS, Google Cloud, etc).
238 |
239 | Como nosso primeiro objetivo é didático, iremos criar apenas uma imagem Docker para exemplificar o uso de containers.
240 |
241 | Caso você não tenha o Docker instalado em sua máquina, é preciso instalá-lo antes de iniciar a tarefa. Um passo-a-passo de instalação pode ser encontrado na [documentação oficial](https://docs.docker.com/get-docker/).
242 |
243 | #### Passo 1
244 |
245 | Crie um arquivo na raiz do projeto com o nome `shipping.Dockerfile`. Este arquivo armazenará as instruções para criação de uma imagem Docker para o serviço `Shipping`.
246 |
247 | Como ilustrado na próxima figura, o Dockerfile é utilizado para gerar uma imagem. A partir dessa imagem, você pode criar várias instâncias de uma aplicação. Com isso, conseguimos escalar o microsserviço de `Shipping` de forma horizontal.
248 |
249 |
250 |
251 |
252 |
253 | No Dockerfile, você precisa incluir cinco instruções
254 |
255 | - `FROM`: tecnologia que será a base de criação da imagem.
256 | - `WORKDIR`: diretório da imagem na qual os comandos serão executados.
257 | - `COPY`: comando para copiar o código fonte para a imagem.
258 | - `RUN`: comando para instalação de dependências.
259 | - `CMD`: comando para executar o seu código quando o container for criado.
260 |
261 | Ou seja, nosso Dockerfile terá as seguintes linhas:
262 |
263 | ```Dockerfile
264 | # Imagem base derivada do Node
265 | FROM node
266 |
267 | # Diretório de trabalho
268 | WORKDIR /app
269 |
270 | # Comando para copiar os arquivos para a pasta /app da imagem
271 | COPY . /app
272 |
273 | # Comando para instalar as dependências
274 | RUN npm install
275 |
276 | # Comando para inicializar (executar) a aplicação
277 | CMD ["node", "/app/services/shipping/index.js"]
278 | ```
279 |
280 | #### Passo 2
281 |
282 | Agora nós vamos compilar o Dockerfile e criar a imagem. Para isto, execute o seguinte comando em um terminal do seu sistema operacional (esse comando precisa ser executado na raiz do projeto; ele pode também demorar um pouco mais para ser executado).
283 |
284 | ```
285 | docker build -t micro-livraria/shipping -f shipping.Dockerfile ./
286 | ```
287 |
288 | onde:
289 |
290 | - `docker build`: comando de compilação do Docker.
291 | - `-t micro-livraria/shipping`: tag de identificação da imagem criada.
292 | - `-f shipping.Dockerfile`: dockerfile a ser compilado.
293 |
294 | O `./` no final indica que estamos executando os comandos do Dockerfile tendo como referência a raiz do projeto.
295 |
296 | #### Passo 3
297 |
298 | Antes de iniciar o serviço via container Docker, precisamos remover a inicialização do serviço de Shipping do comando `npm run start`. Para isso, basta remover o sub-comando `start-shipping` localizado na linha 7 do arquivo [package.json](https://github.com/aserg-ufmg/micro-livraria/blob/main/package.json), conforme mostrado no próximo diff (a linha com o símbolo "-" no início representa a linha original do arquivo; a linha com o símbolo "+" representa como essa linha deve ficar após a sua alteração):
299 |
300 | ```diff
301 | diff --git a/package.json b/package.json
302 | index 25ff65c..552a04e 100644
303 | --- a/package.json
304 | +++ b/package.json
305 | @@ -4,7 +4,7 @@
306 | "description": "Toy example of microservice",
307 | "main": "",
308 | "scripts": {
309 | - "start": "run-p start-frontend start-controller start-shipping start-inventory",
310 | + "start": "run-p start-frontend start-controller start-inventory",
311 | "start-controller": "nodemon services/controller/index.js",
312 | "start-shipping": "nodemon services/shipping/index.js",
313 | "start-inventory": "nodemon services/inventory/index.js",
314 |
315 | ```
316 |
317 | Em seguida, você precisa parar o comando antigo (basta usar um CTRL-C no terminal) e rodar o comando `npm run start` para efetuar as mudanças.
318 |
319 | Por fim, para executar a imagem criada no passo anterior (ou seja, colocar de novo o microsserviço de `Shipping` no ar), basta usar o comando:
320 |
321 | ```
322 | docker run -ti --name shipping -p 3001:3001 micro-livraria/shipping
323 | ```
324 |
325 | onde:
326 |
327 | - `docker run`: comando de execução de uma imagem docker.
328 | - `-ti`: habilita a interação com o container via terminal.
329 | - `--name shipping`: define o nome do container criado.
330 | - `-p 3001:3001`: redireciona a porta 3001 do container para sua máquina.
331 | - `micro-livraria/shipping`: especifica qual a imagem deve-se executar.
332 |
333 | Se tudo estiver correto, você irá receber a seguinte mensagem em seu terminal:
334 |
335 | ```
336 | Shipping Service running
337 | ```
338 |
339 | E o Controller pode acessar o serviço diretamente através do container Docker.
340 |
341 | **Mas qual foi exatamente a vantagem de criar esse container?** Agora, você pode levá-lo para qualquer máquina ou sistema operacional e colocar o microsserviço para rodar sem instalar mais nada (incluindo bibliotecas, dependências externas, módulos de runtime, etc). Isso vai ocorrer com containers implementados em JavaScript, como no nosso exemplo, mas também com containers implementados em qualquer outra linguagem.
342 |
343 | **IMPORTANTE**: Se tudo funcionou corretamente, dê um **COMMIT & PUSH** (e certifique-se de que seu repositório no GitHub foi atualizado; isso é fundamental para seu trabalho ser devidamente corrigido).
344 |
345 | ```bash
346 | git add --all
347 | git commit -m "Tarefa prática #2 - Docker"
348 | git push origin main
349 | ```
350 |
351 | #### Passo 4
352 |
353 | Como tudo funcionou corretamente, já podemos encerrar o container e limpar nosso ambiente. Para isso, utilizaremos os seguintes comandos:
354 |
355 | ```
356 | docker stop shipping
357 | ```
358 |
359 | onde:
360 |
361 | - `docker stop`: comando para interromper a execução de um container.
362 | - `shipping`: nome do container que será interrompido.
363 |
364 |
365 | ```
366 | docker rm shipping
367 | ```
368 |
369 | onde:
370 |
371 | - `docker rm`: comando para remover um container.
372 | - `shipping`: nome do container que será removido.
373 |
374 |
375 | ```
376 | docker rmi micro-livraria/shipping
377 | ```
378 |
379 | onde:
380 |
381 | - `docker rmi`: comando para remover uma imagem.
382 | - `micro-livraria/shipping`: nome da imagem que será removida.
383 |
384 | ## Comentários Finais
385 |
386 | Nesta aula, trabalhamos em uma aplicação baseada em microsserviços. Apesar de pequena, ela ilustra os princípios básicos de microsserviços, bem como algumas tecnologias importantes quando se implementa esse tipo de arquitetura.
387 |
388 | No entanto, é importante ressaltar que em uma aplicação real existem outros componentes, como bancos de dados, balanceadores de carga e orquestradores.
389 |
390 | A função de um **balanceador de carga** é dividir as requisições quando temos mais de uma instância do mesmo microsserviço. Imagine que o microsserviço de frete da loja virtual ficou sobrecarregado e, então, tivemos que colocar para rodar múltiplas instâncias do mesmo. Nesse caso, precisamos de um balanceador para dividir as requisições que chegam entre essas instâncias.
391 |
392 | Já um **orquestrador** gerencia o ciclo de vida de containers. Por exemplo, se um servidor para de funcionar, ele automaticamente move os seus containers para um outro servidor. Se o número de acessos ao sistema aumenta bruscamente, um orquestrador também aumenta, em seguida, o número de containers. [Kubernetes](https://kubernetes.io/) é um dos orquestradores mais usados atualmente.
393 |
394 | Se quiser estudar um segundo sistema de demonstração de microsserviços, sugerimos este [repositório](https://github.com/GoogleCloudPlatform/microservices-demo), mantido pelo serviço de nuvem do Google.
395 |
396 | ## Créditos
397 |
398 | Este exercício prático, incluindo o seu código, foi elaborado por **Rodrigo Brito**, aluno de mestrado do DCC/UFMG, como parte das suas atividades na disciplina Estágio em Docência, cursada em 2020/2, sob orientação do **Prof. Marco Tulio Valente**.
399 |
400 | O código deste repositório possui uma licença MIT. O roteiro descrito acima possui uma licença CC-BY.
401 |
--------------------------------------------------------------------------------