CodeGym /Cursos /Java Server /Comandos catalog en ReadLater

Comandos catalog en ReadLater

Java Server
Nivel 17, Lección 2
Disponible

1. Ubicación de los comandos en ReadLaterApplication

Mientras la aplicación no sepa arrancar normalmente «desde fuera» a través de args, sigue siendo algo así como un trabajo de laboratorio: hay código, pero es difícil de reproducir, difícil de mostrar a otra persona y difícil de comprobar. Los comandos catalog search y catalog details son nuestro «panel de control» mínimo, que convierte el proyecto en un artefacto real, y no en un conjunto de clases «por si acaso».

Acordamos que hoy ReadLaterApplication entiende exactamente dos formas de inicio:

Comando Ejemplo de ejecución Significado
catalog search <query...>
./gradlew run --args="catalog search clean code"
Buscamos libros en el catálogo externo según la consulta
catalog details <externalId>
./gradlew run --args="catalog details OL12345M"
Obtenemos detalles por el identificador externo

Observa un detalle importante: en search, la consulta puede constar de varias palabras, por lo que es la «cola» de los argumentos: todo lo que viene después de search. En details, hay exactamente un parámetro: externalId. Esto simplifica mucho el análisis y hace que el comportamiento sea predecible.

Para que esta CLI no se convierta en una máquina multifunción de red, el punto de entrada debe basarse en el ya existente CatalogClient. HTTP, DTO del proveedor y el mapeo permanecen dentro de la capa del cliente, mientras que ReadLaterApplication se ocupa solo de los comandos y la salida para el usuario.

Y un principio más que mantenemos hoy como un mantra: el punto de entrada no debe construir URIs, ni hacer peticiones de red, ni leer JSON. Su tarea es reconocer el comando y delegar la ejecución al «código normal» — nuestra capa interna del cliente, que ya sabe trabajar con DTO.

2. String[] args y tokens

Cuando ejecutas una aplicación Java, main(String[] args) no recibe «una cadena de comandos», sino tokens ya separados. Esto es conveniente, pero a veces sorprende: crees que has pasado clean code, pero Java ve dos argumentos: "clean" y "code". Y esto es normal: así funciona el lanzamiento de comandos en la mayoría de los sistemas.

Para no tener que adivinar, es útil al menos una vez imprimir args y verlos con tus propios ojos:

import java.util.Arrays;

public class ArgsDemo {
    public static void main(String[] args) {
        // Es útil mostrar los argumentos exactamente como los recibe la JVM.
        // Esto ayuda a entender rápidamente dónde terminan el "comando" y los "parámetros".
        System.out.println(Arrays.toString(args)); // [catalog, search, clean, code]
    }
}

Si ejecutas a través de Gradle así:

./gradlew run --args="catalog search clean code"

entonces, normalmente, en un entorno típico, Java verá exactamente el array:

[catalog, search, clean, code]

Y de esto se deduce una solución de ingeniería simple: para catalog search no intentamos adivinar dónde empieza y termina la consulta, simplemente tomamos todos los elementos del array a partir del índice 2 y los unimos de nuevo con un espacio.

Y aquí aparece una excelente noticia: no necesitamos construir un analizador complejo de línea de comandos, no necesitamos montar comillas, escapes y demás delicias de la vida. Nuestros requisitos son muy simples, por lo tanto el código también será simple. Y el código simple es una rareza que hay que cuidar como el último trozo de pizza en la oficina.

3. Gramática del comando

El problema más típico de un novato es escribir el análisis del comando directamente en main(), y luego ir añadiendo todo sobre la marcha. Primero un if, luego otro, luego «bueno, aquí imprimo algo», luego «bueno, aquí intento construir la URI», y de repente tenemos en main un enrutador, una capa de transporte, un mapeador JSON y el dios de la salida local. Así más o menos nacen las leyendas de que Java es «demasiado verboso».

Hagámoslo con cuidado: crearemos un pequeño modelo de comando y un método analizador separado. El modelo será tan simple que incluso se puede mantener anidado dentro de ReadLaterApplication.

public class ReadLaterApplication {

    // Dos comandos de catálogo soportados: búsqueda y detalles.
    private enum CatalogCommandType { SEARCH, DETAILS }

    // Modelo unificado del resultado del análisis:
    // - type: qué comando es
    // - value: o query (para SEARCH) o externalId (para DETAILS)
    private record CatalogCommand(CatalogCommandType type, String value) {}

}

Y ahora el método que intenta analizar args. O devuelve un comando, o null (o Optional, pero no compliquemos la vida al principio).

import java.util.Arrays;

private CatalogCommand parseCatalogCommand(String[] args) {
    // Primero verificamos la longitud mínima para no encontrarnos con un ArrayIndexOutOfBoundsException.
    if (args.length < 2) return null;

    // La primera "parte" del comando debe ser estrictamente catalog.
    if (!args[0].equals("catalog")) return null;

    // catalog search <query...>
    if (args[1].equals("search") && args.length >= 3) {
        // IMPORTANTE: la consulta puede constar de varias palabras, por lo tanto unimos la "cola" de los argumentos.
        String query = String.join(" ", Arrays.copyOfRange(args, 2, args.length));
        return new CatalogCommand(CatalogCommandType.SEARCH, query);
    }

    // catalog details <externalId>
    if (args[1].equals("details") && args.length == 3) {
        // Para details hay un solo parámetro, por lo que podemos ser estrictos con la longitud.
        return new CatalogCommand(CatalogCommandType.DETAILS, args[2]);
    }

    // Si la forma no coincide con ningún comando soportado, devolvemos null.
    return null;
}

Aquí es importante que comprobamos la longitud del array antes de acceder a args[1] o args[2]. Esto es básico, pero evita el clásico ArrayIndexOutOfBoundsException, que suele aparecer en el momento más «demostrativo», cuando enseñas el código a alguien más.

4. Enrutamiento en ReadLaterApplication

En este momento tenemos una estructura agradable: run(args) se encarga de llamar al analizador, comprobar el resultado y ejecutar el escenario correcto. Es decir, hace exactamente lo que hace un «punto de entrada adulto» en cualquier aplicación: gestiona los flujos de ejecución, en lugar de «hacer todo el trabajo a mano». Aunque de momento sean solo dos comandos.

Un ejemplo del «núcleo» de run() podría ser así:

public void run(String[] args) {
    // 1) Analizamos args en un modelo de comando comprensible.
    CatalogCommand command = parseCatalogCommand(args);

    // 2) Si el comando no se reconoce, imprimimos un error y una ayuda sobre cómo ejecutar.
    if (command == null) {
        System.err.println("Comando desconocido o argumentos incorrectos.");
        printUsage();
        return;
    }

    try {
        // 3) Enrutamos la ejecución según el tipo de comando.
        switch (command.type()) {
            case SEARCH -> runCatalogSearch(command.value());    // value = query
            case DETAILS -> runCatalogDetails(command.value());  // value = externalId
        }
    } catch (RuntimeException e) {
        // La capa CLI superior finaliza el escenario con un mensaje corto, no con un stacktrace en la pantalla.
        System.err.println("El comando no se ha ejecutado.");
    }
}

Aquí es importante separar dos tipos de fallos. Una forma de comando incorrecta es un problema de la CLI, por lo tanto printUsage() vive justo aquí. Un fallo al ejecutar el comando ya es historia de la capa del cliente; el punto de entrada superior finaliza el escenario con un mensaje corto y no muestra el stacktrace al usuario. Los detalles técnicos deben vivir junto con la propia llamada HTTP, no en cada comando por separado.

Para que el panorama sea completamente claro, podemos representarlo como un pequeño pipeline:

flowchart TD
    A["String[] args"] --> B["parseCatalogCommand(args)"]
    B -->|null| C["printUsage()"]
    B -->|CatalogCommand| D["switch(type)"]
    D --> E["runCatalogSearch(query)"]
    D --> F["runCatalogDetails(externalId)"]
    E --> G["CatalogClient.search(...)"]
    F --> H["CatalogClient.details(...)"]

Ahora ReadLaterApplication se vuelve comprensible incluso para alguien que no conoce nuestro proyecto: ve qué comandos hay, dónde se analizan y a quién se delega su ejecución.

5. catalog search: consulta

El comando de búsqueda es el primer escenario que normalmente se ejecuta, porque es el más «vivo»: introducimos una frase y obtenemos una lista de libros. Por eso es especialmente importante que una consulta de varias palabras no rompa toda la aplicación. Nuestra política es simple: todo lo que viene después de catalog search lo unimos de nuevo en una cadena de consulta.

Supongamos que ya tenemos una API interna del cliente CatalogClient que devuelve DTO normalizados:

import java.util.List;

public interface CatalogClient {
    // Realiza una búsqueda de libros por una cadena de consulta y devuelve elementos de resultado normalizados.
    List<CatalogBookSearchItem> search(String query);
}

Entonces runCatalogSearch(query) se ve casi como pseudocódigo, y eso es genial:

import java.util.List;

private void runCatalogSearch(String query) {
    // Realizamos la búsqueda a través de la capa del cliente (no manualmente mediante HTTP/JSON en el punto de entrada).
    List<CatalogBookSearchItem> items = catalogClient.search(query);

    // Salida amigable: el usuario ve de inmediato si hay resultados.
    System.out.println("Encontrados: " + items.size()); // Por ejemplo: Encontrados: 2

    // Es mejor separar la impresión de la lista para que runCatalogSearch no se haga grande.
    printSearchItems(items);
}

El método de impresión es mejor mantenerlo separado para no convertir runCatalogSearch() en una «sábana»:

import java.util.List;

private void printSearchItems(List<CatalogBookSearchItem> items) {
    // Imprimimos los campos normalizados de la aplicación, no los DTO específicos del proveedor.
    for (CatalogBookSearchItem item : items) {
        System.out.println(item.externalId() + " | " + item.title() + " | " + item.author());
        // Ejemplo de línea:
        // OL12345M | Clean Code | Robert C. Martin
    }
}

Esta pieza funciona solo si la capa del cliente ha devuelto una lista lista. Si durante la llamada HTTP o el mapeo se produce un error, la salida normal no continúa: el comando finaliza con el mensaje de error superior, en lugar de imprimir datos a medio procesar.

Observa que imprimimos campos normalizados como externalId, title, author, y no cosas específicas del proveedor como key o el array authors. Esta es una consecuencia importante de nuestra arquitectura: el punto de entrada y la salida trabajan en el lenguaje de la aplicación, no en el lenguaje del servicio externo.

Si la búsqueda no encuentra nada, items.size() será 0 — y eso también es un escenario normal. Para el usuario es útil ver Encontrados: 0 y, quizá, una frase separada No se encontró nada, pero el cero en sí mismo ya es un comportamiento predecible.

6. catalog details: ficha

Si la búsqueda es una «lista», los detalles son una «ficha». El comando catalog details debe tener exactamente un parámetro. Y es un caso muy conveniente, porque podemos ser estrictos: si los argumentos no son 3, significa que el comando se ha introducido incorrectamente, y es mejor mostrar inmediatamente usage que intentar «adivinar por el usuario».

De nuevo nos basamos en la API interna del cliente:

public interface CatalogClient {
    // Devuelve la ficha detallada de un libro por su identificador externo.
    CatalogBookDetails details(String externalId);
}

Entonces la ejecución del comando es máxima directa:

private void runCatalogDetails(String externalId) {
    // Obtenemos los detalles a través de la capa del cliente.
    CatalogBookDetails details = catalogClient.details(externalId);

    // Mostramos los campos principales de la "ficha".
    System.out.println("externalId: " + details.externalId());
    System.out.println("title: " + details.title());
    System.out.println("author: " + details.author());
}

Sí, de momento es una «simple impresión en consola». Y es normal para nuestra etapa actual: no estamos construyendo una bonita interfaz de usuario, estamos construyendo un escenario reproducible tipo backend. Nuestro objetivo es que el comando funcione igual en los modos real y mock, no que la ficha sea visualmente más bonita que una publicación de LinkedIn.

Es importante además lo siguiente: el comando details es muy sensible a lo que se pasa en externalId. Si accidentalmente pasas una cadena vacía o te equivocas al escribir, el servicio externo puede devolver un 404, o un 200 con datos vacíos, o un error extraño. A nivel de CLI al menos garantizamos que el parámetro está presente, y luego que la capa del cliente decida qué hacer si no se encuentra el libro. Con los detalles la regla es la misma: o obtenemos una ficha completa y la imprimimos, o el comando termina con un breve mensaje de error, sin una salida a medio procesar.

7. Uso y errores

Una CLI mala se diferencia de una buena en una cosa simple: cuando el usuario se equivoca, la CLI mala castiga, y la buena le indica. Sinceramente, nos conviene más la segunda opción, porque el usuario somos nosotros mismos dentro de 15 minutos, cuando hayamos olvidado cómo lanzar exactamente el proyecto. Al cerebro le gusta borrar comandos de la memoria para dejar espacio a cosas importantes, como memes sobre programadores.

El printUsage() más simple podría ser así:

private void printUsage() {
    // Un bloque de texto se lee mejor que muchos println seguidos.
    System.out.print("""
            Usage:
              ./gradlew run --args="catalog search <query>"
              ./gradlew run --args="catalog details <externalId>"
            """);
}

Y en run(args) mostramos esta ayuda ante cualquier error en la forma del comando. Además, es útil diferenciar entre «salida normal» y «error». En Java para eso está System.err, y no existe solo por estética.

if (command == null) {
    // Enviamos el mensaje de error a stderr para que se pueda separar de la salida "normal".
    System.err.println("Error: comando incorrecto. Se esperaba: catalog search|details ...");

    // El usage lo imprimimos en stdout como ayuda al usuario.
    printUsage();
    return;
}

Observa que no lanzamos una excepción hacia arriba ni mostramos el stacktrace al usuario. El stacktrace nos es útil como desarrolladores, pero como mensaje de ayuda funciona aproximadamente igual que las instrucciones de un microondas escritas en antiguo élfico.

Si se quiere ser aún más humano, se pueden añadir un par de ejemplos:

System.out.println("Examples:");
System.out.println("  ./gradlew run --args=\"catalog search clean code\"");
System.out.println("  ./gradlew run --args=\"catalog details OL12345M\"");

Pero es importante no convertir el usage en una sábana. Cuanto más corta y concreta sea la ayuda, mayor será la probabilidad de que realmente se utilice.

8. Errores típicos al trabajar con CLI

Error n.º 1: acceder a args[1] y args[2] sin comprobar la longitud del array.
Este es el camino clásico hacia un ArrayIndexOutOfBoundsException. Aparece cuando ejecutas la aplicación sin argumentos o con un comando incompleto (catalog pero sin search/details). El hábito correcto es comprobar primero args.length, y solo después indexar el array.

Error n.º 2: para catalog search se toma solo args[2] y se pierden las demás palabras de la consulta.
Si escribiste String query = args[2];, entonces la consulta clean code se convertirá en clean, y te preguntarás largo rato por qué la búsqueda «parece funcionar peor». Para la búsqueda debemos unir la cola de argumentos mediante String.join(" ", ...).

Error n.º 3: intentar hacer un analizador de línea de comandos «inteligente» antes de tiempo.
A veces se quiere soportar comillas, banderas, --limit=10, alias y la mitad de las capacidades de Bash. Al final escribes medio proyecto de un framework CLI y olvidas para qué empezaste. Hoy nuestra gramática es pequeña, y eso es una ventaja. Es mejor mantener el analizador corto y predecible.

Error n.º 4: construir la URI, la llamada HTTP y la lectura de JSON directamente en ReadLaterApplication.
Así ReadLaterApplication se convierte en una «sopa de todo». Cualquier cambio en el contrato externo rompe el punto de entrada, y probarlo no es cómodo. Es mucho más tranquilo cuando ReadLaterApplication llama a catalogClient.search(...), recibe DTO normalizados y simplemente los imprime.

Error n.º 5: imprimir DTO del proveedor o trabajar con campos del proveedor en el código del comando.
Si en runCatalogSearch() de repente empiezas a imprimir doc.key() o a acceder a authors().get(0), significa que el límite se ha roto: el contrato externo se ha filtrado a donde no debe vivir. Esto no es solo «cosmética». Es el riesgo de que al cambiar el JSON externo tengas que arreglar la mitad del proyecto, en lugar de una sola capa.

1
Tarea
Java Server, nivel 17, lección 2
Bloqueada
Comando `catalog search` con consulta de múltiples palabras
Comando `catalog search` con consulta de múltiples palabras
1
Tarea
Java Server, nivel 17, lección 2
Bloqueada
Comando `catalog details` por identificador externo
Comando `catalog details` por identificador externo
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION