viernes, 12 de mayo de 2023

Testcontainers: Uso básico

Ya hace 3 años, sobre Agosto de 2020 vi un post sobre cómo realizar pruebas unitarias con contenedores. Y aunque tuve la intención de hacer un post, nunca llegue a hacerlo. Pero ahora tras ver cómo funcionan las pruebas con Quarkus, del cual intentaré hablar más adelante, me he interesado en sacar este post. 

En múltiples de los posts que he realizado, he utilizado siempre contenedores para poder realizar las pruebas, y poder comprobar el ejemplo práctico. Normalmente dejando un docker-compose.yml que permitiese levantar y configurar los sistemas externos con los cuales iba a trabajar la aplicación. 

Ahora vamos a hacer algo parecido Y para llevarlo a cabo utilizaremos Testcontainers. Es una librería Java que nos permite crear instancias de Docker y manejarlas en base a nuestro interés. En el ejemplo utilizaremos las siguientes librerías:

  • org.testcontainers:testcontainers:1.16.0
  • org.testcontainers:junit-jupiter:1.16.0
  • org.testcontainers:mysql:1.16.0
  • org.apache.camel:camel-test-spring-junit5:3.16.0
  • org.springframework.boot:spring-boot-starter-test:2.5.1

Para empezarla a utilizar, usaremos principalmente dos anotaciones:

  • @Testcontainers: que nos permite utilizar Testcontainers con JUnit 5.
  • @Containers: Nos permite indicar a las instancias de contenedores que queremos que sean gestionadas por Testcontainers. Aunque como veremos más adelante podemos utilizar Testcontainers sin ella. 
Antes de empezar debemos aclarar un poco el funcionamiento. Testcontainers nos permite levantar gestionar contenedores de cualquier imagen que queramos. Pudiéndole añadir a través de sus métodos una breve configuración. Pero también tiene módulos específicos para hacer uso de contenedores comunes, como puede ser de BBDD, Hashicorp, RabitMQ o Kafka.

Otro asunto importante a tener en cuenta, que aunque nosotros les indiquemos los puertos que queremos exponer, no podremos crear bindings tal y como se realiza en un docker-compose. Sino que los puertos expuestos serán enlazados con puertos aleatorios de nuestra máquina. Debido a esto, deberemos modificar antes de realizar las pruebas la configuración que tenemos por defecto. Para que podamos indicar los puertos que utilizarán los sistemas, una vez han sido arrancados con Testcontainers. 

Para ver cómo funciona, utilizaremos ejemplos hechos con Apache Camel y Spring Boot. En el primer ejemplo enviaremos y recibiremos mensajes de una cola de ActiveMQ. A continuación detallo los componentes más importantes del ejemplo:
  • La instancia de GenericContainer nos permitirá indicar la imagen de ActiveMQ a crear y los puertos a exponer.
  • Crear un método BeforeAll que permita arrancar el contenedor y el puerto vinculado externo. 
  • Modificar la configuración de los componentes que realizan la comunicación con el sistema externo. En Spring Boot lo podemos hacer fácilmente con un método anotado con @DynamicPropertySource.
@Testcontainers
@CamelSpringBootTest
@SpringBootTest(classes = ApacheCamelTestApplication.class)
@Log4j2
public class ApacheActiveMqRouterTest {
    @Autowired
    ProducerTemplate producer;
    @Container
    private static GenericContainer container = new GenericContainer("rmohr/activemq").withExposedPorts(61616, 8161);
    private static Integer tcpPort;
    @BeforeAll
    public static void beforeAll() {
        container.start();
        tcpPort = container.getMappedPort(61616);
    }
    @DynamicPropertySource
    static void replaceProperties(DynamicPropertyRegistry registry) {
        registry.add("activemq.broker-url", () -> "tcp://localhost:" + tcpPort);
    }
    @Test
    public void amqTo01() throws InterruptedException {
        producer.sendBody("direct:SendToPublic", "mensaje ");
    }
}

En el segundo ejemplo, será similar en funcionamiento al anterior. Con la excepción de qué utilizaremos un módulo concreto de Testcontainers. Con él, a través de la clase MySQLContainer podremos hacer configuraciones específicas para crear el contenedor. Como indicar un script para inicializar la base de datos. 

@Testcontainers
@CamelSpringBootTest
@SpringBootTest(classes = {ApacheCamelTestApplication.class})
@Slf4j
public class ApacheMySQLRouterTest{
    static final DockerImageName MYSQL_57_IMAGE = DockerImageName.parse("mysql:5.7.34");
    static MySQLContainer<?> database = new MySQLContainer<>(MYSQL_57_IMAGE)
    .withInitScript("scripts/init_mysql.sql")
    .withDatabaseName("library").withLogConsumer(new Slf4jLogConsumer(log));
    @Autowired
    ProducerTemplate producer;
@BeforeAll public static void beforeAll() throws IOException { database.start(); } @DynamicPropertySource static void databaseProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", database::getJdbcUrl); registry.add("spring.datasource.username", database::getUsername); registry.add("spring.datasource.password", database::getPassword); } @Test public void getBookById() throws InterruptedException { producer.sendBodyAndHeader("direct:getBookById", null, "id", 1); } }

Antes de terminar dos cosas. El primero, es que durante la realización de los ejemplos he tenido problemas con distintas versiones de Testcontainers: 
  • 1.18: Genero la excepción NoSuchMethodError asociada al método optionallyMapResourceParameterAsVolume. 
  • 1.17: Genero ClassNotFoundException: asociado a la clase org.testcontainers.shaded.org.apache.commons.lang.StringUtils
Y lo segundo, es que sí debemos utilizar un varios contenedores como es en este caso podemos tener errores debido a que nuestras aplicación se intente conectar a un sistema externo y no tengamos su contenedor levantado. Por tanto como recomendación, podemos crear una clase que contenga toda la configuración de los contenedores y de la que extenderemos. 

@SpringBootTest
@Testcontainers
@CamelSpringBootTest
@Slf4j
public class TestcontainersConf {
    public static final DockerImageName MYSQL_57_IMAGE = DockerImageName.parse("mysql:5.7.34");
    static GenericContainer<?> container = new GenericContainer<>("rmohr/activemq").withExposedPorts(61616, 8161);    
    static MySQLContainer<?> database = new MySQLContainer<>(MYSQL_57_IMAGE)
    .withInitScript("scripts/init_mysql.sql")
    .withDatabaseName("library").withLogConsumer(new Slf4jLogConsumer(log));
    @BeforeAll
    public static void beforeAll() {
        container.start();
        database.start();
    }
    @DynamicPropertySource
    static void replaceProperties(DynamicPropertyRegistry registry) {
        registry.add("activemq.broker-url", () -> "tcp://localhost:" + container.getMappedPort(61616));
        registry.add("spring.datasource.url", database::getJdbcUrl);
        registry.add("spring.datasource.username", database::getUsername);
        registry.add("spring.datasource.password", database::getPassword);
    }
}
// ............................
public class ApacheMySQLRouterTest extends TestcontainersConf{
    @Autowired
    ProducerTemplate producer;
    @Test
    public void getBookById() throws InterruptedException {
        producer.sendBodyAndHeader("direct:getBookById", null, "id", 1);
    }
}

Espero que os haya ayudado, y si os interesa, aquí tenéis todo el código fuente. 

viernes, 3 de marzo de 2023

WSO2 Secret: Creación básica de claves

WSO2 permite la creación de claves que son manejadas por su propio Secure Vault. Una buena herramienta que nos permite el manejo de claves de forma segura e implementar buenas practicas. 

Para este ejemplo hemos utilizado la imagen: docker.wso2.com/wso2mi:1.2.0

Clave estáticas

Por un lado, las configuramos en el fichero de configuración, deployment.toml, sin encriptar. Deben ir entre comillas dobles y corchetes.

[secrets]
server_secret = "[secret_1]"
synapse_secret = "[secret_2]"

Si quieres activar dicha implementación en un entorno con VM lo podemos hacer ejecutando el comando:

sh <MI_HOME>/bin/ciphertool.sh -Dconfigure

Tras ejecutarlo, tendremos que indicar la clave de nuestro keystore. Que por defecto es, wso2carbon. Tras realizar este paso, si volvemos al fichero de configuración, podremos ver cómo las variables han sido encriptadas.

  • Como usarlas en fichero de configuración
Para ello solo tendremos que hacer referencias a las mismas, a través del alias que le dimos.

[keystore.primary]
password = "$secret{server_secret}"

  • Como usarlas en el contexto de synapse

En este caso, lo realizaremos a través del mediator property y el alias que le dimos

<property expression="wso2:vault-lookup('synapse_secret')" name="secret"/>

Clave dinámicas

Este es el funcionamiento básico, que nos ayuda a gestionar claves privadas, pero el manejo y creación de nuevas claves require el reinicio del servidor. Lo cual puede ser un inconveniente en determinados entornos. Por lo que la creación dinámica de estas contraseñas puede ser un punto fuerte. 

Podemos indicar claves dinámicas a través de variables del entorno o variables del sistema. Para ello primero necesitaremos encriptar las contraseñas, luego setear la variable y posteriormente configurarla.

Para encriptar la variable, podemos utilizar el Micro Integrator CLI o directamente ejecutar el comando:

sh <MI_HOME>/bin/ciphertool.sh

Este comando primero nos pedirá la clave del keystore y posteriormente la contraseña a encriptar, dos veces. 

Una vez echo esto, seteamos la variable. Por ejemplo, para almacenarla en el entorno, ejecutamos:

export env_secret=<ENCRYPTED_VALUE>

Y por último solo nos quedará configurarla. Ejemplo:

[secrets]
env_secret = "$env{env_secret}"

Ya podremos utilizar como vimos anteriormente en nuestros ficheros de configuración o en el código synapse. Ejemplo:

<log level="custom">
   <property name="MSG" value="secretExample_v1_reader_api - GET - /reader/ - init"/>
   <property name="keystore_pwd" expression="wso2:vault-lookup('keystore_pwd')" />
   <property name="env_secret" expression="wso2:vault-lookup('env_secret')" />
</log>

Obteniendo la siguiente salida

wso2mi    | [2022-06-15 10:15:15,424]  INFO {LogMediator} - {api:secretExample_v1_reader_api} 
    MSG = secretExample_v1_reader_api - GET - /reader/ - init, 
    keystore_pwd = wso2carbon, env_secret = envpwd

También podemos configurarlas a través de ficheros o secrets de Docker, con las cuales podremos aumentar el dinamismo de la configuración y utilización de las claves. 


lunes, 13 de febrero de 2023

Wiremock y JUnit5

Hoy vamos a ver un sencillo post sobre cómo configurar Wiremock con JUnit 5. Algo sencillo pero que cambia respecto a cómo era con JUnit 4 rules. Para nuestro ejemplo utilizaremos las siguientes versiones:

  • junit-jupiter-api:5.9.2
  • wiremock-jre8:2.35.0

Para empezar, si queremos utilizar Wiremock de forma muy básica, simplemente nos bastará con la anotación @WireMockTest. La cual nos permitirá modificar lo siguiente:

  • httpPort: Para indicar en que puerto podremos hacer llamadas HTTP.
  • httpsEnabled y httpsPort. Para indicar que queremos hacer llamadas HTTPS y en que puerto. 
  • proxyMode. Para emular un nombre de dominio distinto a localhost. En dicho caso y usando HTTPClient deberemos usar el método useSystemProperties a la hora de crear el cliente. 
A continuación podemos ver un ejemplo de un invocación a un nombre de dominio distinto a localhost y HTTPS. Para este ultimo ya no necesitaremos crear un certificado autoafirmado como lo hacía antiguamente. 

@Log4j2
@WireMockTest(httpsEnabled = true, httpsPort = 9090, proxyMode = true)
public class WiremockBasicTest {
  private static final String BEARER_TOKEN = "Bearer 77d9b8f0-fafe-3778-addf-2755bdc53c88";
  private static final String JSON_CONTENT = "{\"hellow\":\"world\"}";

  @Test
  public void doGetAndGetResponse_proxyMode() throws Exception{
    String sEndpoint = "https://mydomain.com:9090/sample";
    Map<String, String> headers = new HashMap<>();
    headers.put(HttpHeaders.AUTHORIZATION, BEARER_TOKEN);
    stubFor(get("/sample").withHeader(HttpHeaders.AUTHORIZATION, WireMock.equalTo(BEARER_TOKEN))
        .withHost(WireMock.equalTo("mydomain.com"))
        .willReturn(aResponse().withBody(JSON_CONTENT).withStatus(200)));

    String body = null;
    HttpGet get = new HttpGet(sEndpoint);
    get.setHeaders(headers.entrySet().stream().map(entry -> new BasicHeader(entry.getKey(), entry.getValue())).toArray(Header[]::new));
    try (CloseableHttpClient httpClient = createAcceptSelfSignedCertificateClient(); CloseableHttpResponse response = httpClient.execute(get)) {
        body = EntityUtils.toString(response.getEntity(), Charset.defaultCharset());
    }
    assertThat(body, equalTo(JSON_CONTENT));
  }
}

Y aunque con muy poco ya podemos hacer mucho. Puede que haya casos en los que necesitemos un poco más de configuración. Realizar las invocaciones como hacíamos antes con los JUnit 4 rules y tener acceso al método wireMockConfig

Esto lo podremos hacer a través de las extensiones de JUnit5. Pero la instancia que creemos será la misma que debemos utilizar para crear los distintos stub. Lo beneficioso de este enfoque, es que además nos permite crear distintas instancias de la extensión y usar ambas. 

public class WiremockComplexTest {
  private static final String BEARER_TOKEN = "Bearer 77d9b8f0-fafe-3778-addf-2755bdc53c88";
  private static final String JSON_CONTENT = "{\"hellow\":\"world\"}";

  @RegisterExtension
  static WireMockExtension wme = WireMockExtension.newInstance()
      .options(wireMockConfig().httpsPort(9090).port(8085)
      .notifier(new ConsoleNotifier(true))).proxyMode(true).build();

  @Test
  public void doGetAndGetResponse_proxyMode() throws Exception {
    String sEndpoint = "https://mydomain.com:9090/sample";
    Map<String, String> headers = new HashMap<>();
    headers.put(HttpHeaders.AUTHORIZATION, BEARER_TOKEN);
    wme.stubFor(get("/sample").withHeader(HttpHeaders.AUTHORIZATION, WireMock.equalTo(BEARER_TOKEN)).withHost(WireMock.equalTo("mydomain.com"))
        .willReturn(aResponse().withBody(JSON_CONTENT).withStatus(200)));

    String body = null;
    HttpGet get = new HttpGet(sEndpoint);
    get.setHeaders(headers.entrySet().stream().map(entry -> new BasicHeader(entry.getKey(), entry.getValue())).toArray(Header[]::new));
    try (CloseableHttpClient httpClient = createAcceptSelfSignedCertificateClient(); CloseableHttpResponse response = httpClient.execute(get)) {
      body = EntityUtils.toString(response.getEntity(), Charset.defaultCharset());
    }
    assertThat(body, equalTo(JSON_CONTENT));
  }
}

Por último, si este enfoque es el ideal para tus pruebas. Pero no quieres estar indicando la instancia en todos los métodos de WireMock. Podemos indicar que la instancia la cree de forma estática y ya si que sería de la misma forma que cuando usábamos los JUnit 4 rules. Para ello solo tendríamos que utilizar el método configureStaticDsl(true)

@RegisterExtension
static WireMockExtension wme = WireMockExtension.newInstance()
    .options(wireMockConfig().httpsPort(9090).port(8085)
    .notifier(new ConsoleNotifier(true)))
    .configureStaticDsl(true).proxyMode(true).build();

Espero que este post haya sido útil y os ayude en la actualización de vuestro software de pruebas. 

miércoles, 18 de enero de 2023

Http Client 5: Manual básico

Ya hemos hecho otros posts sobre esta gran librería, sobre configuración y uso de la interfaz Fluent. Hoy veremos un manual básico sobre la nueva versión, HTTP Client 5 en su versión clásica. Y cuatro sencillos ejemplos para su uso más común. 

Veremos el ejemplo más básico y completo, con una llamada HTTP Post. En 5 sencillos pasos, podemos invocar el método y obtener su respuesta. Incluso en métodos que no necesitan enviar datos, como GET o DELETE se puede hacer incluso en menos pasos. 

public static String post(final String url, final String jsonBody) {
  // 1. Create HTTP Method
  HttpPost httpPost = new HttpPost(url);
  // 2. Set payload and content-type
  httpPost.setEntity(new StringEntity(jsonBody, ContentType.APPLICATION_JSON));
  String result = null;
  // 3. Create HTTP client
  try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
    // 4. Execute the method through the HTTP client
    try (CloseableHttpResponse response = httpclient.execute(httpPost)) {
      // 5. Read Response
      result = EntityUtils.toString(response.getEntity());
      log.info("Status Code: " + response.getCode() + " " + response.getReasonPhrase());
    }
  } catch (IOException | ParseException e) {
    log.error(e.getMessage(), e);
  }
  return result;
}

El siguiente punto añadir un CookieStore que nos permita almacenar las cookies que nos envíe el servidor a través de la cabecera 'set-cookie'. Y que nos permita devolver dicha cookie al servidor. Muy útil para los casos en que se necesita una comunicación con el servidor, sobre todo con tareas de login.

private static CookieStore cookieStore = new BasicCookieStore();
private static CloseableHttpClient httpclient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();

public static String postWithCookieStore(final String url, final String jsonBody) {
  // 1. Create HTTP Method
  HttpPost httpPost = new HttpPost(url);
  // 2. Set payload and content-type
  httpPost.setEntity(new StringEntity(jsonBody, ContentType.APPLICATION_JSON));
  String result = null;
  try {
    // 3. Execute the method through the HTTP client
    try (CloseableHttpResponse response = httpclient.execute(httpPost)) {
      // 4. Read Response
      result = EntityUtils.toString(response.getEntity());
      log.info("Status Code: " + response.getCode() + " " + response.getReasonPhrase());
    }
  } catch (IOException | ParseException e) {
    log.error(e.getMessage(), e);
  }
  return result;
}

Ahora veremos cómo realizar una llamada con autenticación de usuario y contraseña. Y aunque siempre se puede añadir la cabecera a mano en el método HTTP, esta librería tiene sus propias clases que permiten realizar la autenticación de una forma más segura. 

public static String getWithBasicAuth(final String url, final String user, final String pass)
    throws URISyntaxException, IOException, ParseException {
  String result = null;
  URI uri = new URI(url);
  // 1. Create a Basic Credentials provider to authenticate the call
  final BasicCredentialsProvider credsProvider = new BasicCredentialsProvider();
  AuthScope authScope = new AuthScope(uri.getHost(), uri.getPort());
  credsProvider.setCredentials(authScope, new UsernamePasswordCredentials(user, pass.toCharArray()));
  // 2. Add the credentials to the HHTP Client to use it in the call
  try (final CloseableHttpClient httpclient = HttpClients.custom().setDefaultCredentialsProvider(credsProvider).build()) {
    final HttpGet httpget = new HttpGet(url);
    try (final CloseableHttpResponse response = httpclient.execute(httpget)) {
      result = EntityUtils.toString(response.getEntity());
    }
  } catch (IOException | ParseException e) {
    log.error(e.getMessage(), e);
  }
  return result;
} 

Con BasicCredentialsProvider, el cliente web no adjuntará la cabecera de autenticación a menos que reciba un código de petición 401. Aquí podemos ver una prueba de su funcionamiento con wiremock.

final String book = "{\"name\":\"Dune\",\"author\":\"Frank Herbert\"}";
final String bookUrl = "http://localhost:57001/book";
final String basicAuth = "Basic dXNlcjpwYXNz";

@RegisterExtension
static WireMockExtension wm1 = WireMockExtension.newInstance().options(wireMockConfig().port(57001).notifier(new ConsoleNotifier(true))).build();

@Test
public void getTest(final WireMockRuntimeInfo wmRuntimeInfo) throws URISyntaxException, ParseException, IOException {
  wm1.stubFor(get("/book/1")
      .willReturn(aResponse().withStatus(401).withHeader("Connection", "keep-alive").withHeader("WWW-Authenticate", "Basic realm=\"Fake Realm\"")));
  wm1.stubFor(get("/book/1").withHeader("Authorization", equalToIgnoreCase(basicAuth)).willReturn(ok().withBody(book)));
  String result = HttpClientUtil.getWithBasicAuth(bookUrl + "/1", "user", "pass");
  assertThat(result, equalTo(book));
  wm1.verify(2, getRequestedFor(urlEqualTo("/book/1")));
  wm1.verify(1, getRequestedFor(urlEqualTo("/book/1")).withoutHeader("Authorization"));
  wm1.verify(1, getRequestedFor(urlEqualTo("/book/1")).withHeader("Authorization", equalToIgnoreCase(basicAuth)));
}

El último ejemplo será crear un cliente que invocar a cualquier endpoint con certificado autofirmado y por tanto no de confianza. No hace falta mencionar, que aunque esto es muy util en entornos de desarrollo, no se debe realizar nunca en entornos productivos

public static String trustedAllPost(final String url) {
  String result = null;
  try {
    // 1. Create SSLContextBuilder to trust in every host
    SSLContextBuilder builder = new SSLContextBuilder();
    builder.loadTrustMaterial(null, (chain, authType) -> true);
    // 2. Create a SSLConnectionSocketFactory to not verify any hostname
    SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(builder.build(), new NoopHostnameVerifier());
    HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create().setSSLSocketFactory(sslsf).build();
    // 3. Associate both configurations through HttpClientConnectionManager to the HTTP client
    try (CloseableHttpClient httpclient = HttpClients.custom().setConnectionManager(cm).build()) {
      HttpPost method = new HttpPost(url);
      try (CloseableHttpResponse response = httpclient.execute(method)) {
        result = EntityUtils.toString(response.getEntity());
      }
    }
  } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException | IOException | ParseException e) {
    log.error(e.getMessage(), e);
  }
  return result;
}

sábado, 22 de octubre de 2022

Apache Camel: ActiveMQ

Hace poco vimos como hacer una integración con WSO2 y ActiveMQ, que puedes ver aquí. Esta vez. veremos el mismo ejemplo pero con Apache Camel y Spring Boot. Donde además añadiremos la complejidad de utilizar colas que requieren autenticación. Para el ejemplo utilizaremos las siguientes versiones:

  • Apache Camel 3.11.0
  • Spring Boot 2.5.1
  • Active MQ 5.16.2 

1. Docker Compose y configuración

activemq:
  image: rmohr/activemq
  mem_limit: 1G
  hostname: sandbox-activemq
  container_name: sandbox-activemq
  ports:
    - 8161:8161
    - 61616:61616
  volumes:
    - ./activemq.xml:/opt/apache-activemq-5.15.6/conf/activemq.xml

Sobre esta configuración podemos indicar lo siguiente:

  • El puerto 8161 es el de administración y el que nos permitirá acceder a la consola de gestión a través de la URL http://localhost:8161/admin/index.jsp. 
  • El puerto 61616 es el puerto TCP utilizado para la comunicación con ActiveMQ
  • El fichero activemq.xml, el cual se puede encontrar dentro de la misma imagen, es el fichero que nos permitirá configurar el comportamiento de ActiveMQ. Entre otras cosas, la gestión de los usuarios que tengan acceso a las colas o topics. 
Si queremos autenticación para hacer uso de las colas deberemos incluir el siguiente trozo de código en el fichero activemq.xml:

<plugins>
    <simpleAuthenticationPlugin anonymousAccessAllowed="true">
        <users>
            <authenticationUser username="privateUser" password="P#s5W0rd" groups="admins" />
            <authenticationUser username="system" password="manager" groups="admins,publishers,consumers"/>
        </users>
    </simpleAuthenticationPlugin>
    <authorizationPlugin>
        <map>
            <authorizationMap>
                <authorizationEntries>
                    <authorizationEntry queue="privateQueue" read="admins" write="admins" admin="admins" /> 
                    <authorizationEntry queue=">" write="anonymous,admins" read="anonymous,admins" admin="anonymous,admins" />
                    <authorizationEntry topic="ActiveMQ.Advisory.>" write="*" read="*" admin="*" />
                </authorizationEntries>
            </authorizationMap>
        </map>
     </authorizationPlugin>
</plugins>

Con este código estamos configurando lo siguiente:

  • simpleAuthenticationPlugin: Nos permite configurar manualmente usuarios, contraseñas y los grupos a los que están asociados. La otra opción para securizar el acceso, más profesional,  sería a través del JAAS plugin. 
  • anonymousAccessAllowed: Nos permite indicar que también permitiremos el acceso anónimo. De esta forma podemos tener algunas colas securizadas y otras no. El usuario y grupo asociado por defecto es anonymous
  • Hay que añadir al usuario system para evitar errores al realizar operaciones desde la consola. 
  • authorizationEntries: Es la configuración que queremos aplicar a las colas de forma genérica o específica. En esta configuración permitimos:
    • Crear, escribir o leer en la cola 'privateQueue' solo a los usuarios administradores. 
    • Crear, escribir o leer de cualquier cola. El símbolo '>' indica 'todos'. Esta configuración esta asociada a la inferior, que permite la creación de los canales de asesoramiento a cualquier usuario, 'ActiveMQ.Advisory.>'. 
Para temrinar la configuración, respecto a Apache Camel/Spring Boot, como usamos la dependencia camel-activemq-starter, no necesitaremos configurar nada especial. Simplemente cual es la ruta de comunicación del broker:
camel.component.activemq.brokerUrl=tcp://localhost:61616

2. Envío de mensajes

Ahora que lo tenemos correctamente configurado podemos proceder a crear el recurso que nos permita realizar el envío de mensajes. La idea es crear un servicio REST que almacene la información que recibe en la cola de ActiveMQ. 

rest().post("activemq/public").produces(MediaType.APPLICATION_JSON_VALUE)
  .consumes(MediaType.APPLICATION_JSON_VALUE).route()
  .routeId("postPublicQueue").log("Message incoming - ${body}")
  .to("activemq:queue:publicQueue?exchangePattern=InOnly")
  .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(202));

Como vemos en la configuración, necesitamos indicar al Apache que no se quede esperando por respuesta, ya que no recibirá ninguna del ActiveMQ. Esto lo podemos hacer a través de la configuración 'exchangePattern=InOnly'. De otra forma, en cada llamada, recibiríamos un mensaje de error similar al siguiente:

org.apache.camel.ExchangeTimedOutException: The OUT message was not received
within: 20000 millis due reply message with correlationID

Ahora sí hiciéramos la siguiente invocación podríamos mandar un mensaje a la cola pública. 

curl --location --request POST 'http://localhost:8080/camel/activemq/public' \
--header 'Content-Type: application/json' \
--data-raw '{"prop":"value"}'

Y para la cola privada será un poco más complicado. Primero creamos una conexión de tipo ActiveMQConnectionFactory que permita configurar un usuario y contraseña. Por defecto usa SingleConnectionFactory y este no admite la autenticación. 

@Bean
private ConnectionFactory activeMQConnectionFactory() {
  return new ActiveMQConnectionFactory();
}

Y segundo, será indicar en la cadena de conexión el usuario y contraseña. Si lo quisiésemos de forma generalizada podríamos configurarlo a nivel del fichero application.properties. Pero en este caso solo se lo queremos aplicar a un route

rest().post("activemq/private").produces(MediaType.APPLICATION_JSON_VALUE)
    .consumes(MediaType.APPLICATION_JSON_VALUE).route().routeId("postPrivateQueue")
    .log("Private message incoming - ${body}")
    .to("activemq:queue:privateQueue?connectionFactory=activeMQConnectionFactory&exchangePattern=InOnly&username=privateUser&password={{activemq.privateQueue.password}}")
    .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(202));

3. Lectura de mensajes en la cola

Ahora veremos como hacer un recurso de la API que nos permita leer de la cola. Esta es la parte más sencilla, pues simplemente será cambiar el método to que nos permite producir, por el método from que nos permite consumir. La configuración será la misma. 

// Consumers
from("activemq:queue:publicQueue").log("Reading public message incoming - ${body}").end();

from(
    "activemq:queue:privateQueue?connectionFactory=activeMQConnectionFactory&exchangePattern=InOnly&username=privateUser&password={{activemq.privateQueue.password}}")
        .log("Reading private message incoming - ${body}").end();

Y esto ha sido todo, espero que haya sido util. Yy como siempre puedes ver todo el código aquí