Utilizando Apachel Camel para agregar endpoints de REST APIs
Tags: java, camel, integration, rest
Published: 2020-07-27 14:39:56

## O que é o Apache Camel

Conforme a descrição do próprio criador Claus Ibsen, disponível no livro [Camel in Action](https://livebook.manning.com/book/camel-in-action-second-edition/chapter-1/11) (tradução livre):

> O cerne do Camel framework é ser um mecanismo de roteamento ou mais precisamente, um framework que possibilita a construção de rotas. Ele permite definir regras customizadas de roteamento,  decidir quais de quais fontes aceitar mensagens e como processar e enviar essas mensagens para outros destinos. O Camel usa uma linguagem de integração que permite definir regras complexas de roteamento, similar a processos de negócio. Como monstrado na figura abaixo, ele pode ser o elo que junta sistemas distintos.
> Um dos principios fundamentais do Camel é que ele faz deduções sobre os tipos de dados que você precisa processar. Isso é muito importante porque oferece ao desenvolvedor a oportunidade de integrar qualquer tipo de sistema, sem precisar converter os dados para um formato específico.

![Camel](https://dev-to-uploads.s3.amazonaws.com/i/x61iu90kp0r1kxt0w5qb.png)

Dentre diversas soluções de integração que podemos montar com o Camel, neste artigo, iremos construir um agregador de chamadas a APIs, utilizando o EIP *Enricher*.

Os códigos apresentados neste artigo podem ser encontrados em https://github.com/andredgusmao/camel-rest-aggregator

## O exemplo
Para mostrar como o EIP Enricher pode ser utilizado no Camel vamos disponibilizar uma aplicação que agrega chamada de duas APIs:

1. API de autores de livros com os endpoints:
   1. `[GET] /authors`
   2. `[GET] /authors/{name}`
2. API de livros com o endpoint:
   1. `[GET] /books/{authorId}`

Nossa aplicação Camel vai disponibilizar dois endpoints:

1. `[GET] /integration/authors` - Consulta a API de autores.
2. `[GET] /integration/authors/{name}` - Consulta na API de autores o autor pelo nome (ex: `jr-tolkien`, `jk-rowling`) e enriquece a resposta com a consulta de todos os livros do autor na API de livros.

## A aplicação Camel

### Criando o projeto

Para criar o projeto basta acessar https://start.spring.io/ e preencher a área de project metadata com:

```
Spring Boot: 2.3.1
Group: br.com.camel.bookstore
Artifact: camel-bookstore-aggregator
Version: 1.0
Name: camel-bookstore-aggregator
Dependencies: 'Apache Camel'
```

Clique em `Generate` para criar o projeto e então edite o pom para adicionar as dependências que serão utilizadas (undertow, rest e http)

o pom do projeto deve ficar algo similar a:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.1.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>br.com.camel.bookstore</groupId>
	<artifactId>camel-bookstore-aggregator</artifactId>
	<version>1.0</version>
	<name>camel-bookstore-aggregator</name>

	<properties>
		<java.version>1.8</java.version>
    		<camel.version>3.1.0</camel.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.apache.camel.springboot</groupId>
			<artifactId>camel-spring-boot-starter</artifactId>
			<version>${camel.version}</version>
		</dependency>
		
		<dependency>
			<groupId>org.apache.camel.springboot</groupId>
			<artifactId>camel-rest-starter</artifactId>
			<version>${camel.version}</version>
		</dependency>
		
		<dependency>
			<groupId>org.apache.camel</groupId>
			<artifactId>camel-undertow</artifactId>
			<version>${camel.version}</version>
		</dependency>

		<dependency>
			<groupId>org.apache.camel</groupId>
			<artifactId>camel-http</artifactId>
			<version>${camel.version}</version>
		</dependency>
        ...
	</dependencies>
</project>
```

### A rota Rest

Vamos criar duas classes com rotas Camel. Na classe `RestRoute` estarão os endpoints e na classe `RestAggregatorRoute` estarão as rotas que acessam as APIs e enriquecem o conteúdo.

```java
@Component
public class RestRoute extends RouteBuilder {

  @Override
  public void configure() throws Exception {
    //define as configurações do servidor como endereço de host e porta
    restConfiguration()
      .host("0.0.0.0").port(8080)
      .bindingMode(RestBindingMode.auto);

    //inicia delaração dos serviços REST
    rest("/integration")
      //Endpoint que consulta todos os autores
      .get("/authors")
        .route().routeId("rest-all-authors")
        .to("direct:call-rest-all")
      .endRest()
      
      //Endpoint que usa o Enrich EIP
      .get("/authors/{name}")
        .route().routeId("rest-author-by-name")
        .to("direct:call-rest-author")
      .endRest();
  }
}
```

Os endpoints `[GET] /integration/authors` e `[GET] /integration/authors/{name}` ao serem invocados chamam as rotas `direct:call-rest-all` e `direct:call-rest-author` que serão definidas na outra classe.

### A rota de integração

#### Consulta a API de autores

A rota que chama todos os autores utiliza o componente `http` para consumir o endpoint da API de autores. 

```java
from("direct:call-rest-all")
  .routeId("all-service")
  .removeHeaders("CamelHttp*")
  .setHeader(Exchange.HTTP_METHOD, constant("GET"))
.to("http://{{authors.url}}/authors");
```

Na rota é utilizado o `removeHeaders("CamelHttp*")` para garantir que na chamada da API teremos apenas headers do componente `http`, vamos usar `HTTP_METHOD` com o valor `GET`. O método `to` recebe como parâmetro `"http://{{authors.url}}/authors"`, ao usar duplas chaves envolta do `authors.url` o camel é capaz de substituir o valor direto do `application.properties`, dessa forma, o arquivo deve conter o valor da url:

```
#application.properties

authors.url=localhost:8081
```

Isso é tudo que precisamos para essa rota, o retorno do endpoint da API de autores é retornado diretamente pela aplicação Camel.

#### Agregação das consultas de autor e livros

A rota `direct:call-rest-author` busca o autor pelo nome informado, com base na resposta recuperamos o `ìd` do autor e em seguida enriquecemos o retorno com os livros do autor, conforme a imagem:

![Route 1](https://dev-to-uploads.s3.amazonaws.com/i/5eext1a3075r1v5ik7om.jpg)

O código da rota fica da seguinte maneira:

```java
from("direct:call-rest-author")
  .routeId("call-rest-services")
  .to("direct:author-service")
    .process(new GetAuthorIdProcessor())
  .enrich("direct:books-service", new JsonRestCallsAggregator())
```

Vamos falar das partes separadamente:

1. `to("direct:author-service")`: Assim como a rota que retorna todos os autores, é feita uma chamada simples a API de autores:

```java
from("direct:author-service")
  .routeId("author-service")
  .removeHeaders("CamelHttp*")
  .setHeader(Exchange.HTTP_METHOD, constant("GET"))
.toD("http://{{authors.url}}/authors/${header.name}");
```

Ao chamarmos o endpoint `/integration/authors/{name}` da nossa aplicação camel, o path que for passado em `name`  fica disponível no header da exchange (ex: `/integration/authors/jr-tolkien`; `/integration/authors/jane-austin`), como o path faz a url de consulta a API ter diversas variações temos que usar o método `toD` ao invés de `to`.

O retorno da API de autor é um json com as informações do autor, para recuperarmos o `id` precisamos fazer um parse do json e o mesmo se encontra encapsulado dentro da mensagem, que o Camel utiliza para transitar as informações entre as integrações, a *Exchange*, para o componente `http` o response encontra-se no corpo da parte *In* da *Exchange*, conforme a imagem. Mais detalhes sobre a *Exchange* podem ser encontrados no javadoc https://www.javadoc.io/doc/org.apache.camel/camel-api/latest/org/apache/camel/Exchange.html.

![Route 2](https://dev-to-uploads.s3.amazonaws.com/i/ljay2mmb6552fwa8hzxu.jpg)

2. `process(new GetAuthorIdProcessor())`: Para recuperarmos o `id` vamos usar um *processor* para ler a exchange, a classe `GetAuthorIdProcessor` que implementa `org.apache.camel.Processor` contem o código necessário.

Classe `GetAuthorIdProcessor.java`

```java
public class GetAuthorIdProcessor implements Processor {

	@Override
	public void process(Exchange exchange) throws Exception {
        String author = exchange.getIn().getBody(String.class);
        JsonParser parser = JsonParserFactory.getJsonParser();
        Map<String, Object> jsonMap = parser.parseMap(author);
        String authorId = (String) jsonMap.get("id");
    
        exchange.getIn().setHeader("id", authorId);
        exchange.getIn().setBody(author);
    }
}
```

O método process é responsável por ler a resposta da API de autores que se encontra no corpo da parte ***In*** da Exchange. Como a resposta está em json usaremos classes do próprio *Spring Boot* para fazer o parse e ler o id. Por fim criamos um novo header chamado `id` e setamos com o valor do `id` do autor e setamos novamente o corpo da mensagen ***In*** com o json de autor. 


3. `enrich("direct:books-service", new JsonRestCallsAggregator())`: Nesse trecho é feita uma chamada para a API de livros e com o retorno realizamos o agrupamento das mensagens na classe `JsonRestCallsAggregator`.

```java
from("direct:books-service")
  .routeId("books-service")
  .removeHeaders("CamelHttp*")
  .setHeader(Exchange.HTTP_METHOD, constant("GET"))
.toD("http://{{books.url}}/books/${header.id}");
```

Assim como a chamada `direct:author-service` usamos o componente `http` para chamar a API de livros, como o path exige um `id` passamos o que foi extraido do autor e inserido no header no método `process(new GetAuthorIdProcessor())`, conforme a imagem abaixo. O valor `{{books.url}}` deve ser declarado no `application.properties`

```
#application.properties

authors.url=localhost:8081
books.url=localhost:8082
```

![Route 3](https://dev-to-uploads.s3.amazonaws.com/i/n6giw8hs8qrujvol4y0i.jpg)

A classe que agrega as respostas é uma implementação de `org.apache.camel.AggregationStrategy`, com o código:

```java
public class JsonRestCallsAggregator implements AggregationStrategy {
    
    @Override
    public Exchange aggregate(Exchange oldExchange, Exchange newExchange) {
        JsonParser parser = JsonParserFactory.getJsonParser();

        String books = newExchange.getIn().getBody(String.class);
        String author = oldExchange.getIn().getBody(String.class);
        
        JsonObject authorJson = new JsonObject(parser.parseMap(author));        
        authorJson.put("books", new JsonArray(parser.parseList(books)));

        newExchange.getIn().setBody(authorJson.toJson());
        return newExchange;
    }
}
```

O método `aggregate` que implementamos possui dois parâmetros, a Exchange antes do `enrich` e depois dele. Com auxílio das classes do *Spring Boot* para manipular json recuperamos o json de autor do `oldExchange` e json dos livros do autor do `newExchange`, conforme a imagem:

![Route 4](https://dev-to-uploads.s3.amazonaws.com/i/3psitwff61ugon4zwanb.jpg)

Dessa forma criamos um Exchange de retorno contendo um json no formato:

```json
{
    "author": { 
        "id": "...",
        "name": "...",
        "fullName": "...",
        "books": [ ... ]
    }
}
```

## Lidando com erros

Com nossa aplicação Camel funcionando podemos consumir de forma integrada os serviços das duas APIs mas e se houver alguma falha na chamada delas?
Vamos cobrir dois casos e refatorar nossa API para ser mais tolerante a falhas.

### Cenários de falha

1. Falha na API de livros.
Caso seja utilizado um id de autor que não possua livros cadastrados durante a chamada da API de livros (`/books/{authorId}`) teremos como retorno o erro 404 (Not Found). Para evitar erros na nossa aplicação Camel vamos tratar a chamada ao endpoint. O componente `http` já possui um comportamento em que dependendo do código de status do *response* o componente retorna sucesso ou a exceção `HttpOperationFailedException` [1].

```java
//Sem tratamento de erro
from("direct:books-service")
  .routeId("books-service")
  .removeHeaders("CamelHttp*")
  .setHeader(Exchange.HTTP_METHOD, constant("GET"))
.toD("http://{{books.url}}/books/${header.id}");
```

```java
//Com tratamento de erro
from("direct:books-service")
  .routeId("books-service")
  
  //tratamento de exceção
  .onException(HttpOperationFailedException.class)
    .handled(true)
    .setBody(constant("[]"))
  .end()
  
  .removeHeaders("CamelHttp*")
  .setHeader(Exchange.HTTP_METHOD, constant("GET"))
.toD("http://{{books.url}}/books/${header.id}");   
```

Com a modificação, toda vez que tivermos o erro 404 da API de livros a aplicação Camel irá utilizar um array vazio `[]` pra completar o request.

2. Falha na API de autores.
Caso seja utilizado um nome que não exista durante a chamada da API de autores (`/authors/{name}`) a API irá retornar o código de status `204` (No Content). Para evitar erros na nossa aplicação Camel vamos tratar a chamada ao endpoint. Como o status `204` encontra-se na família de status sucesso podemos checar o status de retorno da API de autores e caso seja `204` iremos retornar o mesmo código pela aplicação Camel, simbolizando que o nome do autor não foi encontrado. Da mesma forma podemos retornar `404` ou outro código, com um simples alteração no código.


```java
//Sem tratamento de erro
from("direct:call-rest-author")
  .routeId("call-rest-services")
  .to("direct:author-service")
    .bean("authors", "getId")
  .enrich("direct:books-service", new JsonRestCallsAggregator())
```
```java
//variáveis
private static final int OK_CODE = 200;
private static final int APP_RESPONSE_CODE = 204;

//Com tratamento de erro
from("direct:call-rest-author")
  .routeId("call-rest-services")
  .to("direct:author-service")
  .choice()
    .when(header(Exchange.HTTP_RESPONSE_CODE).isEqualTo(OK_CODE))
      .bean("authors", "getId")
      .enrich("direct:books-service", new JsonRestCallsAggregator())
  .otherwise()
    .setHeader(Exchange.HTTP_RESPONSE_CODE).constant(APP_RESPONSE_CODE);
```

Com a modificação, toda vez que tivermos o erro `204` da API de autores a aplicação Camel irá retornar o erro definido na variável `APP_RESPONSE_CODE` encerrando a requisição, evitando a consulta a API de livros e uma possível exceção.


## Conclusão
O Apache Camel é um framework bastante robusto para construir diversos tipos de integrações como a que foi apresentada nesse artigo. O EIP *Enricher* assim como outros EIPs implementados no Camel são muito poderosos, para mais detalhes sobre cada um deles é possível consultar a documentação em https://camel.apache.org/components/latest/eips/enterprise-integration-patterns.html.
A aplicação Camel construída possui algumas possibilidades de expansão como a adição de integração com novos endpoints ou APIs, a melhoria no tratamento de erro (para verificar outras exceções e falhas), a chamada inversa ao buscar um livro e enriquecer com os informações do autor.