Статьи

Создание DSL для робота AWT

Java SDK поставляется с классом java.awt.Robot который позволяет автоматизировать ввод с клавиатуры и мыши, а также создавать снимки экрана. Если вы хотите написать небольшое тестовое приложение, которое имитирует пользовательский ввод, или вы просто хотите автоматизировать ввод некоторого повторяющегося текста, эта функция пригодится. Но вы не хотите каждый раз писать полное Java-приложение.

С другой стороны, ANTLR — это генератор синтаксических анализаторов, который позволяет нам создавать «доменные языки» (DSL). С помощью ANTLR мы можем разработать простой DSL, который предоставляет одну команду для каждого из методов java.awt.Robot . С этого момента мы можем легко написать сценарий для различных простых задач автоматизации.

Первый шаг — придумать синтаксис нашего нового «DSL»:

  • Различные «утверждения» должны быть разделены точкой с запятой.
  • Каждый оператор должен состоять из одной «команды» и нескольких параметров для этой команды.
  • Комментарии должны занимать несколько строк (используя C-подобные комментарии / *… * / или только до конца строки.

Простой файл может выглядеть так:

1
2
3
4
5
6
7
8
9
/*
* A simple example demonstrating the basic features.
*/
delay 300; // sleep for 300ms
mouseMove 20,30;
createScreenCapture 100,100,200,200 file=/home/siom/capture.png;
mouseClick button1;
keyboardInput "Test";
delay 400;

С этими требованиями мы можем начать записывать грамматику:

01
02
03
04
05
06
07
08
09
10
11
12
grammar Robot;
  
instructions:
    (instruction ';')+
    EOF;
  
instruction:
    instructionDelay |
    instructionMouseMove |
    instructionCreateScreenCapture |
    instructionMouseClick |
    instructionKeyboardInput;

Мы называем грамматику «Робот» и определяем первые правила правил таким образом, чтобы у нас была одна или несколько инструкций, за которыми следовала бы точка с запятой в качестве разделителя инструкций до достижения конца файла (EOF). Инструкции, которые мы хотим поддержать, перечислены как часть instruction правила. Канал между различными правилами обозначает логическое ИЛИ, то есть только одно из этих правил должно совпадать.

Самое простое правило — instructionDelay Delay:

1
2
3
4
5
instructionDelay:
    'delay' paramMs=INTEGER;
...
INTEGER:
    [0-9]+;

Правило начинается с команды ‘delay’, за которой следует единственный параметр, который задает количество спящих миллисекунд в виде целого числа. Токен INTEGER показан ниже правила. Это просто определяет, что мы ожидаем хотя бы одно число от нуля до девяти. Чтобы впоследствии упростить обработку параметра, мы назначаем параметр отдельному узлу дерева с именем paramMs .

Правило делать снимок экрана выглядит следующим образом:

1
2
3
4
5
6
7
instructionCreateScreenCapture:
    'createScreenCapture' x=INTEGER ',' y=INTEGER ',' w=INTEGER ',' h=INTEGER  'file=' file=FILENAME;
...
FILENAME:
    FileNameChar+;
fragment FileNameChar:
    [a-zA-Z0-9/\\:_-$~.];

После ключевого слова createScreenCapture пользователь должен createScreenCapture две координаты на экране верхней левой точки прямоугольника, который должен быть захвачен. Две следующие координаты обозначают ширину и высоту прямоугольника. Наконец, пользователь должен предоставить имя файла для захваченного изображения.

Имя файла состоит из одного или нескольких символов из фрагмента FileNameChar . Этот fragment определяет все символы, которые должны быть разрешены для имени файла.

Используя maven, мы можем теперь сохранить эту грамматику как файл Robot.g4 в папке src/main/antlr4 и использовать соответствующий плагин maven для генерации лексера и парсера Java:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<build>
    <plugins>
        <plugin>
            <groupId>org.antlr</groupId>
            <artifactId>antlr4-maven-plugin</artifactId>
            <version>${antlr.version}</version>
            <executions>
                <execution>
                    <goals>
                        <goal>antlr4</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        ...
    </plugins>
</build>
  
<dependencies>
    <dependency>
        <groupId>org.antlr</groupId>
        <artifactId>antlr4-runtime</artifactId>
        <version>${antlr.version}</version>
    </dependency>
    ...
</dependencies>

Зависимость от antlr4-runtime необходима для использования сгенерированных классов в нашем собственном коде.

Метод execute() принимает Path к входному файлу в качестве параметра, анализирует и выполняет его:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public void execute(Path inputPath) throws IOException, AWTException {
    RobotLexer lexer = new RobotLexer(new ANTLRInputStream(new FileInputStream(inputPath.toFile())));
    RobotParser parser = new RobotParser(new CommonTokenStream(lexer));
    final Robot robot = new Robot();
    parser.addParseListener(new RobotBaseListener() {
        @Override
        public void exitInstructionDelay(@NotNull RobotParser.InstructionDelayContext ctx) {
            int delayParam = Integer.parseInt(ctx.paramMs.getText());
            LOGGER.info("delay(" + delayParam + ")");
            robot.delay(delayParam);
        }
        ...
    });
    parser.instructions();
}

Содержимое файла пересылается через ANTLRInputStream в RobotLexer , созданный ANTLR. После того, как лексер проанализировал файл и сгенерировал поток токенов, этот поток может быть передан фактическому RobotParser .

Чтобы реагировать на входящие инструкции, добавлен ParseListener . К счастью, ANTLR уже создал базовый приемник, который реализует все методы обратного вызова с пустой реализацией. Следовательно, нам нужно только переопределить методы, которые мы хотим обработать. Поскольку ANTLR создает для каждого правила синтаксического анализатора один метод обратного вызова, мы можем переопределить, например, метод exitInstructionDelay() . Параметр, передаваемый сгенерированным кодом, имеет тип RobotParser.InstructionDelayContex . Этот объект контекста имеет поле paramMs как ранее мы paramMs параметр в грамматике для отдельного узла. Его getText() возвращает значение для этого параметра как String . Нам нужно только преобразовать его в целочисленное значение, а затем передать его в метод delay() экземпляра Robot .

Реализация для правила instructionCreateScreenCapture показана в следующем блоке:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
@Override
public void exitInstructionCreateScreenCapture(@NotNull
    RobotParser.InstructionCreateScreenCaptureContext ctx) {
    int x = Integer.parseInt(ctx.x.getText());
    int y = Integer.parseInt(ctx.y.getText());
    int w = Integer.parseInt(ctx.w.getText());
    int h = Integer.parseInt(ctx.h.getText());
    LOGGER.info("Rectangle rectangle = new Rectangle(" + x + "," + y +
        "," + w + "," + h + ")");
    Rectangle rectangle = new Rectangle(x, y, w, h);
    LOGGER.info("createScreenCapture(rectangle);");
    BufferedImage bufferedImage = robot.createScreenCapture(rectangle);
    File output = new File(ctx.file.getText());
    LOGGER.info("Save file to " + output.getAbsolutePath());
    try {
        ImageIO.write(bufferedImage, "png", output);
    } catch (IOException e) {
        throw new RuntimeException("Failed to write image file: " + e.getMessage(), e);
    }
}

Принцип такой же, как показано для последней инструкции. Переданный в контекстный объект имеет одно поле для каждого параметра, и эти строковые значения должны быть преобразованы в целочисленные значения. С помощью этой информации мы можем создать объект Rectangle , вызвать метод Robot createScreenCapture() и сохранить его BufferedImage .

Вывод

Создание специализированного DSL для робота AWT оказалось проще, чем ожидалось. Предоставленный плагин maven создает все необходимые классы из файла грамматики и вместе с тем плавно интегрируется в процесс сборки. Полученный DSL можно использовать для автоматизации простых задач мыши и клавиатуры, включая создание скриншотов.

  • PS: исходный код доступен на github .
Ссылка: Создание DSL для робота AWT от нашего партнера по JCG Мартина Моиса в блоге Martin’s Developer World .