Wykorzystanie adnotacji w praktyce – picocli

To znowu ja. Kontynuujemy dziś dwa tematy, o których pisałem ostatnio. Czyli zobaczymy jak można wykorzystać adnotacje do wygodnego przekazywania wartości do programu w chwili jego uruchomienia. W jednym z ostatni artykułów opisywałem ideę przekazywania wartości do programu metodą przełączników. Więc dzisiaj przejdziemy od razu do rzeczy.

Biblioteka picocli

Dziś powtórzę kroki, które opisałem w tym wpisie, tylko dziś do tego użyję biblioteki picocli. Na ich stronie znajduje się bardzo dobrze napisana dokumentacja. Więc w razie jakichkolwiek niejasności zachęcam do zajrzenia do nich. Oczywiście komentarz będzie bardzo dobrze widziany – konstruktywna krytyka zawsze ma pozytywną wartość. Na potrzeby tego wpisu stworzyłem projekt pictures-manipulation-5. Więc wszystkie kody, które zamieszczam we wpisie znajdują się również na githubie.

Plik pom.xml będzie wyglądał bardzo podobnie do tego, co było w projekcie pictures-manipulation-4:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>pl.backlog.green</groupId>
  <artifactId>pictures-manipulation-5</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>pictures-manipulation-5</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>14</maven.compiler.source>
    <maven.compiler.target>14</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.openpnp</groupId>
      <artifactId>opencv</artifactId>
      <version>4.3.0-2</version>
    </dependency>
    <dependency>
      <groupId>info.picocli</groupId>
      <artifactId>picocli</artifactId>
      <version>4.5.2</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-assembly-plugin</artifactId>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
            <configuration>
              <archive>
                <manifest>
                  <mainClass>
                    pl.backlog.green.Watermak
                  </mainClass>
                </manifest>
              </archive>
              <descriptorRefs>
                <descriptorRef>jar-with-dependencies</descriptorRef>
              </descriptorRefs>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Widzimy, że zmieniły się współrzędne projektu oraz zastąpiliśmy zależność do commons-cli przez picocli. Teraz przejdźmy do konkretnych przykładów.

Test01 – prosta opcja bez wartości

Zacznijmy od klasy Test01, która pokazuje w jaki sposób możemy zdefiniować prosty przełącznik. Najpierw kod, a potem kilka uwag do kodu:

@CommandLine.Command(
        name = "Test01",
        mixinStandardHelpOptions = true,
        version = "1.0",
        description = "Simple program to show capabilities of picocli library"
)
public class Test01 implements Callable<Integer> {
    @CommandLine.Option(names = "-a", description = "first option without value, not mandatory")
    boolean simpleClusteredOption;

    @Override
    public Integer call() {
        if (simpleClusteredOption) {
            System.out.println("Podałeś opcję 'a'");
        } else {
            System.out.println("Nie podałeś opcji 'a'");
        }
        return 0;
    }

    public static void main(String[] args) {
        int exitCode = new CommandLine(new Test01()).execute(args);
        System.exit(exitCode);
    }
}

W metodzie main tworzymy obiekt klasy CommandLine i w konstruktorze tej klasy przekazujemy obiekt naszej klasy Test01, która implementuje interfejs Callable. Na świeżo utworzonym obiekcie CommandLine wywołujemy metodę execute, do której przekazujemy tablicę Stringów, które są argumentami uruchomienia naszej aplikacji. Wartość zwrócona przez metodę execute (a tak naprawdę wartość zwróconą przez metodę call w klasie Test01) zostanie ustawiona jako kod zakończenia aplikacji (0 oznacza, że program zakończył się sukcesem, każda inna wartość może oznaczać jakiś ściśle określony problem.

Czyli z powyższego wynika, że cała magia programu znajduje się w klasie Test01. Jak już wspomniałem klasa ta ma implementować interfejs Callable (bo inaczej nie będziemy mogli jej przekazać w konstruktorze klasy CommandLine). Metoda call w klasie Test01 definiuje, co ma zrobić program.

Dodatkową rzeczą którą wymagamy od naszej klasy jest użycie adnotacji @Command. Chciałem wkleić tutaj definicję tej adnotacji, ale okazuje się, że definicja ma ponad 300 linii i zawiera całe mnóstwo atrybutów. W naszym przykładzie ustawiamy tylko kilka z nich. Wydaje mi się, że najciekawszym z użytych tutaj atrybutów jest mixinStandardHelpOptions. Gdy go ustawimy na true, to picocli wygeneruje nam podstawową pomoc na temat uruchomienia programu po uruchomieniu z przełącznikiem -h (lub --help) oraz doda przełącznik -V (--version), który wypisze wartość atrybutu version.

Cała magia z dodawaniem przełączników odbywa się przez zdefiniowanie pól i położenie na nich adnotacji @Option. Znów kod samej adnotacji jest bardzo długi i zawiera wiele różnych atrybutów wraz z opisem, a my używamy tylko kilku najważniejszych, czyli nazwy i opisu.

Zobacz, jakie jest wyjście z programu dla kilka różnych uruchomień:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/pictures-manipulation-5 (master)
$ java -cp target/pictures-manipulation-5-1.0-SNAPSHOT-jar-with-dependencies.jar pl.backlog.green.Test01
Nie podałeś opcji 'a'

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/pictures-manipulation-5 (master)
$ java -cp target/pictures-manipulation-5-1.0-SNAPSHOT-jar-with-dependencies.jar pl.backlog.green.Test01 -a
Podałeś opcję 'a'

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/pictures-manipulation-5 (master)
$ java -cp target/pictures-manipulation-5-1.0-SNAPSHOT-jar-with-dependencies.jar pl.backlog.green.Test01 -h
Usage: Test01 [-ahV]
Simple program to show capabilities of picocli library
  -a              first option without value, not mandatory
  -h, --help      Show this help message and exit.
  -V, --version   Print version information and exit.

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/pictures-manipulation-5 (master)
$ java -cp target/pictures-manipulation-5-1.0-SNAPSHOT-jar-with-dependencies.jar pl.backlog.green.Test01 --help
Usage: Test01 [-ahV]
Simple program to show capabilities of picocli library
  -a              first option without value, not mandatory
  -h, --help      Show this help message and exit.
  -V, --version   Print version information and exit.

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/pictures-manipulation-5 (master)
$ java -cp target/pictures-manipulation-5-1.0-SNAPSHOT-jar-with-dependencies.jar pl.backlog.green.Test01 -V
1.0

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/pictures-manipulation-5 (master)
$ java -cp target/pictures-manipulation-5-1.0-SNAPSHOT-jar-with-dependencies.jar pl.backlog.green.Test01 --version
1.0

Widać, że klasa CommandLine robi za nas mnóstwo roboty, a my musimy tylko położyć odpowiednią adnotację na polu.

Test02 – opcja z wartością

W jaki sposób zrobić opcję z wartością? Po prostu deklarujemy pole o innym typie niż boolean. I naprawdę możemy poszaleć z typami pól. Tutaj jest pełna lista typów, które są wspierane automatycznie. Jeżeli wciąż brakuje jakiegoś typu, to możesz dodać swój własny konwerter. Jak? Chętnych odsyłam do dokumentacji. Zobaczmy, więc jak można dodać opcje z wartościami:

@CommandLine.Command(
        name = "Test02",
        mixinStandardHelpOptions = true,
        version = "1.0",
        description = "Simple program to show capabilities of picocli library"
)
public class Test02 implements Callable<Integer> {
    @CommandLine.Option(names = "-a", description = "first option without value, not mandatory")
    boolean simpleClusteredOption;

    @CommandLine.Option(names = "-b", description = "second option with value, not mandatory")
    String value01;

    @CommandLine.Option(names = "-c", paramLabel = "INT", description = "second option with value, not mandatory")
    int value02;


    @Override
    public Integer call() {
        if (simpleClusteredOption) {
            System.out.println("Podałeś opcję 'a'");
        } else {
            System.out.println("Nie podałeś opcji 'a'");
        }
        System.out.println("Wartość 'b': " + value01);
        System.out.println("Wartość 'c': " + value02);
        return 0;
    }

    public static void main(String[] args) {
        int exitCode = new CommandLine(new Test02()).execute(args);
        System.exit(exitCode);
    }
}

Teraz zobaczmy wyjście z programu:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/pictures-manipulation-5 (master)
$ java -cp target/pictures-manipulation-5-1.0-SNAPSHOT-jar-with-dependencies.jar pl.backlog.green.Test02 -a -b blabla -c 5
Podałeś opcję 'a'
Wartość 'b': blabla
Wartość 'c': 5

Zobaczmy jeszcze tekst pomocy wygenerowany przez picocli:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/pictures-manipulation-5 (master)
$ java -cp target/pictures-manipulation-5-1.0-SNAPSHOT-jar-with-dependencies.jar pl.backlog.green.Test02 -h
Usage: Test02 [-ahV] [-b=<value01>] [-c=INT]

Simple program to show capabilities of picocli library
  -a              first option without value, not mandatory
  -b=<value01>    second option with value, not mandatory
  -c=INT          second option with value, not mandatory
  -h, --help      Show this help message and exit.
  -V, --version   Print version information and exit.

Zobacz, w pomocy widać wyraźnie, że przełączniki do przekazywania wartości są wyraźnie zaznaczone. Widać, że te przełączniki oczekują na wartość. Jeżeli nie sprecyzujemy co, chcemy tu wyświetlić to pojawi się nazwa pola. Doprecyzować to możemy, używając atrybutu paramLabel w adnotacji @Option.

Test03 – opcja w wersji długiej

Zwróć uwagę, że adnotacja @Option ma atrybut names, nie name. Jak zajrzymy do definicji tej adnotacji, to zauważymy, że atrybut names przyjmuje tablicę Stringów. Więc tak naprawdę możemy zdefiniować dowolnie wiele sposób wykonania danej opcji.

@CommandLine.Command(
        name = "Test03",
        mixinStandardHelpOptions = true,
        version = "1.0",
        description = "Simple program to show capabilities of picocli library"
)
public class Test03 implements Callable<Integer> {
    @CommandLine.Option(names = "-a", description = "first option without value, not mandatory")
    boolean simpleClusteredOption;

    @CommandLine.Option(names = "-b", description = "second option with value, not mandatory")
    String value01;

    @CommandLine.Option(names = "-c", paramLabel = "INT", description = "second option with value, not mandatory")
    String value02;

    @CommandLine.Option(names = {"-p", "--path"}, paramLabel = "PATH", description = "second option with value, not mandatory")
    Path path;

    @Override
    public Integer call() {
        if (simpleClusteredOption) {
            System.out.println("Podałeś opcję 'a'");
        } else {
            System.out.println("Nie podałeś opcji 'a'");
        }
        System.out.println("Wartość 'b': " + value01);
        System.out.println("Wartość 'c': " + value02);
        System.out.println("Wartość 'p': " + path);
        return 0;
    }

    public static void main(String[] args) {
        int exitCode = new CommandLine(new Test03()).execute(args);
        System.exit(exitCode);
    }
}

I możemy uruchomić program:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/pictures-manipulation-5 (master)
$ java -cp target/pictures-manipulation-5-1.0-SNAPSHOT-jar-with-dependencies.jar pl.backlog.green.Test03 -a -b blabla -c 5 --path "c:\Users\pawel"
Podałeś opcję 'a'
Wartość 'b': blabla
Wartość 'c': 5
Wartość 'p': c:\Users\pawel

jak również możemy użyć krótszej wersji:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/pictures-manipulation-5 (master)
$ java -cp target/pictures-manipulation-5-1.0-SNAPSHOT-jar-with-dependencies.jar pl.backlog.green.Test03 -a -b blabla -c 5 -p "c:\Users\pawel"
Podałeś opcję 'a'
Wartość 'b': blabla
Wartość 'c': 5
Wartość 'p': c:\Users\pawel

Test04 – opcja obowiązkowa

Żeby uczynić opcję obowiązkową do uruchomienia programu, wystarczy użyć atrybutu required w adnotacji @Option:

@CommandLine.Command(
        name = "Test04",
        mixinStandardHelpOptions = true,
        version = "1.0",
        description = "Simple program to show capabilities of picocli library"
)
public class Test04 implements Callable<Integer> {
    @CommandLine.Option(names = "-a", required = true, description = "first option without value, mandatory")
    boolean simpleClusteredOption;

    @CommandLine.Option(names = "-b", description = "second option with value, not mandatory")
    String value01;

    @CommandLine.Option(names = "-c", required = true, paramLabel = "INT", description = "second option with value, mandatory")
    String value02;

    @CommandLine.Option(names = {"-p", "--path"}, required = true, paramLabel = "PATH", description = "second option with value, not mandatory")
    Path path;

    @Override
    public Integer call() {
        if (simpleClusteredOption) {
            System.out.println("Podałeś opcję 'a'");
        } else {
            System.out.println("Nie podałeś opcji 'a'");
        }
        System.out.println("Wartość 'b': " + value01);
        System.out.println("Wartość 'c': " + value02);
        System.out.println("Wartość 'p': " + path.toString());
        return 0;
    }

    public static void main(String[] args) {
        int exitCode = new CommandLine(new Test04()).execute(args);
        System.exit(exitCode);
    }
}

I jeżeli nie podamy opcji w momencie uruchamiania picocli wygeneruje informację o błędzie:

$ java -cp target/pictures-manipulation-5-1.0-SNAPSHOT-jar-with-dependencies.jar pl.backlog.green.Test04 -b blabla -c 5 -p "c:\Users\pawel"
Missing required option: '-a'
Usage: Test04 -a [-hV] [-b=<value01>] -c=INT -p=PATH
Simple program to show capabilities of picocli library
  -a                first option without value, mandatory
  -b=<value01>      second option with value, not mandatory
  -c=INT            second option with value, mandatory
  -h, --help        Show this help message and exit.
  -p, --path=PATH   second option with value, not mandatory
  -V, --version     Print version information and exit.

Test05 – dodatkowe argumenty programu

Biblioteka picocli pozwala na wygodna obsługę argumentów programu, których nie przekazujemy za pomocą przełączników. Obsługa tego wymaga zdefiniowanie kolejnego pola z adnotacją @Parameters:

@CommandLine.Command(
        name = "Test05",
        mixinStandardHelpOptions = true,
        version = "1.0",
        description = "Simple program to show capabilities of picocli library"
)
public class Test05 implements Callable<Integer> {
    @CommandLine.Option(names = "-a", description = "first option without value, not mandatory")
    boolean simpleClusteredOption;

    @CommandLine.Option(names = "-b", required = true, description = "second option with value, mandatory")
    String value01;

    @CommandLine.Option(names = "-c", paramLabel = "INT", description = "second option with value, not mandatory")
    String value02;

    @CommandLine.Parameters(index = "0", paramLabel = "FILE", description = "argument of program")
    String value03;

    @Override
    public Integer call() {
        if (simpleClusteredOption) {
            System.out.println("Podałeś opcję 'a'");
        } else {
            System.out.println("Nie podałeś opcji 'a'");
        }
        System.out.println("Wartość 'b': " + value01);
        System.out.println("Wartość 'c': " + value02);
        System.out.println("Argument: " + value03);
        return 0;
    }

    public static void main(String[] args) {
        int exitCode = new CommandLine(new Test05()).execute(args);
        System.exit(exitCode);
    }
}

Teraz uruchomienie programu wygląda następująco:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/pictures-manipulation-5 (master)
$ java -cp target/pictures-manipulation-5-1.0-SNAPSHOT-jar-with-dependencies.jar pl.backlog.green.Test05 -b blabla -c 5 "c:\Users\pawel"
Nie podałeś opcji 'a'
Wartość 'b': blabla
Wartość 'c': 5
Argument: c:\Users\pawel

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/pictures-manipulation-5 (master)
$ java -cp target/pictures-manipulation-5-1.0-SNAPSHOT-jar-with-dependencies.jar pl.backlog.green.Test05 -h
Usage: Test05 [-ahV] -b=<value01> [-c=INT] FILE
Simple program to show capabilities of picocli library
      FILE        argument of program
  -a              first option without value, not mandatory
  -b=<value01>    second option with value, mandatory
  -c=INT          second option with value, not mandatory
  -h, --help      Show this help message and exit.
  -V, --version   Print version information and exit.

Nowa wersja programu Watermark

Zanim przejdziesz dalej, spróbuj sam się zastanowić, jak możemy dodać parsowanie argumentów uruchomienia do programu Watermark używając biblioteki picocli. Oczywiście moje rozwiazanie znajduje się poniżej:

@CommandLine.Command(
        name = "watermark.sh",
        mixinStandardHelpOptions = true,
        version = "1.0",
        description = "Simple program to put watermark into image"
)
public class Watermak implements Callable<Integer> {

    @CommandLine.Option(names = {"-i", "--input"}, required = true, paramLabel = "PATH", description = "path to input image - to put image in")
    String imagePath;

    @CommandLine.Option(names = {"-o", "--output"}, required = true, paramLabel = "PATH", description = "path to output image")
    String outputImagePath;

    @CommandLine.Option(names = {"-w", "--watermark"}, required = true, paramLabel = "PATH", description = "path to watermark image")
    String watermarkPath;

    private Mat addWatermarkToImage(Mat image, Mat watermark) {
        Size inSize = image.size();
        Size outSize = new Size(inSize.width/2.5, inSize.height/2.5);

        Mat scaledImage = new Mat();
        resize(image, scaledImage, outSize);
        cvtColor(scaledImage, scaledImage, COLOR_BGR2BGRA);

        Mat transparentLayer = new Mat(outSize, CvType.CV_8UC4);
        Rect roi = new Rect(
                scaledImage.cols() - watermark.cols() - 10,
                scaledImage.rows() - watermark.rows() - 10,
                watermark.cols(),
                watermark.rows());
        watermark.copyTo(transparentLayer.submat(roi));

        Mat result = new Mat();
        addWeighted(scaledImage, 1, transparentLayer, 0.35, 0, result);

        return result;
    }

    @Override
    public Integer call() throws Exception {
        Mat image = Imgcodecs.imread(imagePath, IMREAD_COLOR);
        Mat watermark = imread(watermarkPath, IMREAD_UNCHANGED);
        imwrite(outputImagePath, addWatermarkToImage(image, watermark));
        return 0;
    }

    public static void main(String[] args) {
        nu.pattern.OpenCV.loadLocally();

        int exitCode = new CommandLine(new Watermak()).execute(args);
        System.exit(exitCode);
    }
}

Teraz możemy zbudować projekt:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/pictures-manipulation-5 (master)
$ mvn clean package

Skopiować go do katalogu ~/bin:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/pictures-manipulation-5 (master)
$ cp target/pictures-manipulation-5-1.0-SNAPSHOT-jar-with-dependencies.jar /c/Users/pawel/bin/pictures-manipulation-5.jar

Zaktualizować zawartość pliku ~/bin/watermark.sh:

#!/bin/bash

java -jar /c/Users/pawel/bin/pictures-manipulation-5.jar $@

I teraz możemy uruchomić nasz program, na przykład w taki sposób:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Pictures/picts
$ watermark.sh --input=IMG_0601.JPG -o mak.jpg --watermark=watermarkWhite.png

Podsumowanie

To był przykład praktycznego wykorzystania adnotacji do rozwiązania problemu, który wcześniej rozwiązaliśmy bardziej tradycyjnymi metodami. Sam zdecyduj, która opcja bardziej Ci się podoba.

W następnych wpisach, skupię się bardziej na technicznych aspektach przetwarzania plików graficznych za pomocą biblioteki OpenCV.

Aspirujący twórca internetowy, który zna się na programowaniu i chce się dzielić wiedzą

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Scroll to top