Это вторая статья из цикла “Разра­ба­тываем свой язык програм­ми­ро­вания на Java”, первую статью можно прочитать по ссылке.

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

Класси­фи­кация ошибок

Ошибки бывают разные, но в целом их можно разделить на три категории: синтак­си­ческиесеман­ти­ческие и ошибки времени испол­нения.

Синтак­си­ческие ошибки возникают из-за нарушения правил синтаксиса, установ­ленных для конкретного языка програм­ми­ро­вания. Синтак­си­ческие правила определяют, как должны быть органи­зованы инструкции и выражения в коде.

Пример синтак­си­ческой ошибки (отсут­ствует закры­вающая кавычка):

println("Hello, World!)

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

var a = 1 + 2 * 3

Ожидая, что переменная a будет равна 9, но на самом деле она будет равна 7. Это проис­ходит из-за того, что оператор умножения имеет более высокий приоритет, чем оператор сложения. Семан­ти­ческая ошибка обычно может быть обнаружена во время отладки или обширного тести­ро­вания программы.

Ошибки времени испол­нения, также известные как исклю­чения (Exceptions), возникают во время выпол­нения программы. Такие ошибки могут возникнуть из-за непра­вильного ввода данных, попытки доступа к несуще­ству­ющему файлу и многих других сценариев. Некоторые ошибки времени испол­нения могут быть обработаны в программе, но если этого не сделать, то обычно программа будет аварийно завершена.

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

JimpleBaseVisitor

Для выявления ошибок и преду­пре­ждений нам понадо­бится, знакомый из первой статьи абстрактный класс JimpleBaseVisitor (сгене­ри­рован ANTLR), который по-умолчанию реализует интерфейс JimleVisitor. Он позволяет обходить AST-дерево (Abstract Syntax Tree) и на основе анализа его узлов мы будем решать ошибка, преду­пре­ждение или нормальная часть кода. По сути, диагно­стика ошибок почти не отличается от интер­пре­тации кода, кроме случаев когда нам нужно выполнять ввод/вывод или обращаться к внешним ресурсам. Например, если выпол­няется команда вывода в консоль, то наша задача проверить допустимый ли тип данных передается в качестве аргумента, без непосред­ственного вывода в консоль.

Создадим класс JimpleDiagnosticTool, который наследует JimleBaseVisitor и будет инкап­су­ли­ровать в себе всю логику поиска и хранения ошибок:

class JimpleDiagnosticTool extends JimpleBaseVisitor<ValidationInfo> {
    private Set<Issue> issues = new LinkedHashSet<>();
}

record Issue(IssueType type, String message, int lineNumber, int lineOffset, String details) {}

Данный класс содержит в себе список типа Issue, который представляет инфор­мацию о конкретной ошибке.

Известно, что каждый метод данного класса должен возвращать значение опреде­ленного типа. В нашем случае мы будем возвращать инфор­мацию о типе узла в дереве — ValidationInfo. Также данный класс содержит инфор­мацию о возможном значении, это поможет нам выявлять некоторые семан­ти­ческие ошибки или ошибки времени выполнения.

record ValidationInfo(ValidationType type, Object value) {}

enum ValidationType {
    /**
     * Expression returns nothing.
     */
    VOID,

    /**
     * Expression is String
     */
    STRING,

    /**
     * Expression is double
     */
    DOUBLE,

    /**
     * Expression is long
     */
    NUMBER,

    /**
     * Expression is boolean
     */
    BOOL,

    /**
     * Expression contains error and analysing in another context no makes sense.
     */
    SKIP,

    /**
     * When object can be any type. Used only in Check function definition mode.
     */
    ANY,

    /**
     * Tree part is function declaration
     */
    FUNCTION_DEFINITION
}

Следует обратить внимание на значение ValidationType.SKIP. Оно будет исполь­зо­ваться в случае если в части дерева была найдена и уже зареги­стри­рована ошибка, и дальнейший анализ этого узла дерева не имеет смысла. Например, если в выражении суммы один аргумент содержит ошибку, то анализ второго аргумента выражения не будет проводиться.

ValidationInfo checkBinaryOperatorCommon(ParseTree leftExp, ParseTree rightExp, Token operator) {
    ValidationInfo left = visit(leftExp);
    if (left.isSkip()) {
        return ValidationInfo.SKIP;
    }
    ValidationInfo right = visit(rightExp);
    if (right.isSkip()) {
        return ValidationInfo.SKIP;
    }
    // code omitted
}

Listeners vs Visitors

Перед тем как двигаться дальше, давайте посмотрим на еще один сгене­ри­ро­ванный ANTLR-ом интерфейс JimpleListener (шаблон Observer), который тоже может быть исполь­зован, если нам нужно обходить AST-дерево. В чем разница между ними? Самое большое различие между этими механизмами в том, что методы listener вызываются ANTLR-ом для каждого узла всегда, тогда как методы visitor должны обходить свои дочерние элементы явными вызовами. И если программист не вызывает visit() на дочерних узлах, то эти узлы не посещаются, т.е. у нас есть возмож­ность управлять обходом дерева. Например, в нашей реали­зации тело функции посещается сначала один раз полностью (режим checkFuncDefinition==true) для выявления ошибок во всей функции (все блоки if и else), и несколько раз с конкретными значе­ниями аргументов:

@Override
ValidationInfo visitIfStatement(IfStatementContext ctx) {
    // calc expression in "if" condition
    ValidationInfo condition = visit(ctx.expression());

    if (checkFuncDefinition) {
        visit(ctx.statement());
        // as it's just function definition check, check else statement as well
        JimpleParser.ElseStatementContext elseStatement = ctx.elseStatement();
        if (elseStatement != null) {
            visit(elseStatement);
        }
        return ValidationInfo.VOID;
    }

    // it's not check function definition, it's checking of certain function call
    if (condition.isBool() && condition.hasValue()) {
        if (condition.asBoolean()) {
            visit(ctx.statement());
        } else {
            JimpleParser.ElseStatementContext elseStatement = ctx.elseStatement();
            if (elseStatement != null) {
                visit(elseStatement);
            }
        }
    }

    return ValidationInfo.VOID;
}

Шаблон Visitor работает очень хорошо, если нам необходимо спроеци­ровать опреде­ленное значение для каждого узла дерева. Это именно то, что нам нужно.

Отлов синтак­си­ческих ошибок

Для того чтобы найти в коде некоторые синтак­си­ческие ошибки, нам необходимо реали­зовать интерфейс ANTLRErrorListener. Данный интерфейс содержит четыре метода, которые будут вызываться (парсером и/или лексером) в случае ошибки или неопре­де­ленного поведения:

interface ANTLRErrorListener {
    void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e);
    void reportAmbiguity(Parser recognizer, DFA dfa, int startIndex, int stopIndex, boolean exact, BitSet ambigAlts, ATNConfigSet configs);
    void reportAttemptingFullContext(Parser recognizer, DFA dfa, int startIndex, int stopIndex, BitSet conflictingAlts, ATNConfigSet configs);
    void reportContextSensitivity(Parser recognizer, DFA dfa, int startIndex, int stopIndex, int prediction, ATNConfigSet configs);
} 

Название первого метода (syntaxError) говорит само за себя, он будет вызываться в случае синтак­си­ческой ошибки. Реали­зация довольно простая: нам нужно преоб­ра­зовать инфор­мацию об ошибке в объект типа Issue и добавить его в список ошибок:

@Override
void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) {
    int offset = charPositionInLine + 1;
    issues.add(new Issue(IssueType.ERROR, msg, line, offset, makeDetails(line, offset)));
}

Остальные три метода можно игнори­ровать. Также ANTLR сам реализует этот интерфейс (класс ConsoleErrorListener) и отправляет ошибки в стандартный поток ошибок (System.err). Чтобы отключить его и другие стандартные обработчики, нам необходимо вызвать метод removeErrorListeners у парсера и лексера:

    // убираем стандартные обработчики ошибок
    lexer.removeErrorListeners();
    parser.removeErrorListeners();

Другой тип синтак­си­ческих ошибок базируется на правилах конкретного языка. Например, в нашем языке функция иденти­фи­ци­руется по имени и количеству аргументов. Когда анали­затор встречает вызов функции, то он проверяет, существует ли функция с таким именем и количе­ством аргументов. Если нет, то он выдает ошибку. Для этого нам необходимо переопре­делить метод visitFunctionCall:

@Override
ValidationInfo visitFunctionCall(FunctionCallContext ctx) {
    String funName = ctx.IDENTIFIER().getText();
    int argumentsCount = ctx.expression().size();
    var funSignature = new FunctionSignature(funName, argumentsCount, ctx.getParent());
    // ищем функцию в контексте по сигнатуре (имя+количество аргументов)
    var handler = context.getFunction(funSignature);

    if (handler == null) {
        addIssue(IssueType.ERROR, ctx.start, "Function with such signature not found: " + funName);
        return ValidationInfo.SKIP;
    }

    // code omitted
}

Давайте проверим конструкцию if. Jimple требует, чтобы выражение в условии if было типа boolean:

@Override
ValidationInfo visitIfStatement(IfStatementContext ctx) {
    // visit expression
    ValidationInfo condition = visit(ctx.expression());
    // skip if expression contains error
    if (condition.isSkip()) {
        return ValidationInfo.SKIP;
    }

    if (!condition.isBool()) {
        addIssue(IssueType.WARNING, ctx.expression().start, "The \"if\" condition must be of boolean type only. But found: " + condition.type());
    }

    // code omitted
}

Внима­тельный читатель заметит, что в данном случае мы добавили преду­пре­ждение, а не ошибку. Это сделано из-за того, что наш язык является динами­ческим и нам не всегда известна точная инфор­мация о типе выражения.

Поиск семан­ти­ческих ошибок

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

Пример детек­ти­ро­вания ошибки “Деление на ноль”: сраба­тывает в случае когда в качестве делителя исполь­зуется выражение, которое всегда возвращает значение 0:

ValidationInfo checkBinaryOperatorForNumeric(ValidationInfo left, ValidationInfo right, Token operator) {
    if (operator.getType() == JimpleParser.SLASH && right.hasValue() && ((Number) right.value()).longValue() == 0) {
        // if we have value of right's part of division expression and it's zero
        addIssue(IssueType.WARNING, operator, "Division by zero");
    }

    // code omitted
}

Ошибки времени исполнения

Ошибки времени испол­нения также тяжело или даже невоз­можно обнаружить на этапе компиляции/интерпретации. Однако, некоторые подобные ошибки всё же можно выявить. Например, если функция вызывает сама себя (напрямую или через другую функцию), то это может привести к ошибке перепол­нения стека (StackOverflow). Первое что нам нужно сделать – это объявить список (Set), где мы будем сохранять функции, которые находятся в процессе вызова в данной момент. Саму проверку можно (и нужно) разме­стить в методе handleFuncInternal обработки вызова функции. В начале этого метода находится проверка наличия текущего FunctionDefinitionContext (контекст объяв­ления функции) в списке уже вызванных функций, и если да, то регистрируем преду­пре­ждение (Warning) и прерываем дальнейшую обработку функции. Если нет, то добавляем текущий контекст в наш список, и далее следует остальная логика. При выходе из handleFuncInternal нужно удалить из списка текущий контекст функции. Здест следует обратить внимание, что в данном случае мы не только выявили потен­ци­альный StackOverflow, но и избавились от этой же ошибки во время проверки кода, а именно при выполении зацик­ли­вания метода handleFuncInternal.

Set<FunctionDefinitionContext> calledFuncs = new HashSet<>();

ValidationInfo handleFuncInternal(List<String> parameters, List<Object> arguments, FunctionDefinitionContext ctx) {
    if (calledFuncs.contains(ctx)) {
        addIssue(IssueType.WARNING, ctx.name, String.format("Recursive call of function '%s' can lead to StackOverflow", ctx.name.getText()));
        return ValidationInfo.SKIP;
    }
    calledFuncs.add(ctx);
    
    // other checkings

    calledFuncs.remove(ctx);

    // return resulting ValidationInfo
}

Анализ потока управления/данных

Для более глубокого иссле­до­вания программного кода, оптими­зации и выявления сложных ошибок также используют Анализ потока управ­ления (Control-flow analysis) и Анализ потока данных (Data-flow analysis).

Анализ потока управ­ления фокуси­руется на понимании того, какие части программы выпол­няются в зависи­мости от различных условий и управ­ляющих структур, таких как условные операторы (if-else), циклы и переходы. Он позволяет выявить пути выпол­нения программы и иденти­фи­ци­ровать потен­ци­альные ошибки, связанные с непра­вильной логикой управ­ления потоком. Например, недости­жимый код или потен­ци­альные точки зависания программы.

С другой стороны, анализ потока данных сосре­до­та­чи­вается на том, как данные распро­стра­няются и исполь­зуются внутри программы. Он помогает выявить потен­ци­альные проблемы с данными, такие как исполь­зо­вание неини­ци­а­ли­зи­ро­ванных переменных, зависи­мости данных и возможные утечки памяти. Анализ потока данных может также обнару­живать ошибки, связанные с непра­вильными опера­циями над данными, такими как исполь­зо­вание некор­ректных типов или непра­вильных (бессмыс­ленных) вычислений.

Резюме

В этой статье мы рассмотрели, как добавить диагно­стику ошибок и преду­пре­ждений в свой язык програм­ми­ро­вания. Узнали, какие инстру­менты из коробки предо­ставляет ANTLR для регистрации синтак­си­ческих ошибок. Реали­зовали обработку некоторых ошибок и потен­ци­альных проблем во время выпол­нения программы.

Весь исходный код интер­пре­татора можно посмотреть по ссылке.

Ссылки

В мире Java библиотека JAXB уже давно (как сообщает нам wikipedia с 2006 года) и по праву является распро­стра­ненным и очень удобным инстру­ментом для XML-cериа­ли­зации объектов и даже объектных моделей. В интернете есть масса примеров и целые сторонние библиотеки, постро­енные на JAXB — взять хотя бы docx4j (http://www.docx4java.org/trac/docx4j), которая работает с моделью документа в Open-XML формате. Так же много инфор­мации можно почерпнуть у самих авторов JAXBhttps://jaxb.java.net/. Без труда найдутся в интернете прекрасные готовые исходники для генерации классов Java по XML-модели. Примеров масса и можно увлека­тельно провести время, изучая возмож­ности JAXB.

Но практи­чески все примеры приво­дятся для одного простого класса, который сериа­лизуют в XML, что называется, с нуля. А что делать, если уже есть некая готовая объектная модель со сложной струк­турой насле­до­вания и зависи­мостей, которые запрещены к изменениям? Где следует применить JAXB-аннотации? Куда их добавить — непосред­ственно в каждый из доброй полторы сотни классов, либо в один базовый? Если в каждый класс иерархии, то целесо­об­разна ли такая переделка с точки зрения объема выпол­ненной работы? Если в один-два базовых, то будет ли вообще работать XML-сериа­ли­зация? Да, действи­тельно много вопросов, и посмотрим, как JAXB с этим справ­ляется для конкретной задачи проекта.

Например, есть модель документа: Root – некий корневой элемент, контейнер для других элементов. Он может содержать Paragraph, который также является контей­нером для элементов Text. В свою очередь все элементы являются контей­нерами атрибутов, например у Paragraph это Alignment и Indent, у Text – Bold, Italic, FontName, FontSize и Color. Конечно, это неполный перечень, но в качестве примера доста­точный. Кроме того, условием для контей­неров элементов – Root и Paragraph, является то, что они могут содержать неогра­ни­ченное количество подэле­ментов, т.е. Root содержит много Paragraph, а Paragraph — много Text. Но контейнеры атрибутов могут содержать каждый атрибут в единственном экзем­пляре. Например, зачем элементу Text несколько FontName или Bold?

Таким образом, есть уже готовая модель элементов и атрибутов, со своей специ­фи­ческой бизнес-логикой, полями, методами и т.д.

Начнем с базового абстрактного атрибута:

 

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

  • Заказное ПО. Продукт разра­ботки предна­зна­чается опреде­ленному заказчику, который финан­сирует процесс разра­ботки и определяет требо­вания к программному продукту. По испол­нении заказа все права на продукт переходят к заказчику, который в будущем для совер­шен­ство­вания данного программного продукта может привлечь либо прежнего, либо любого другого исполнителя.
  • Инвести­ци­онное ПО. Отличается от заказного отсут­ствием конкретного заказчика (хотя разра­ботка может вестись и на средства, выделенные внешним инвестором). Обычно в этом случае права на программный код сохраняет испол­нитель. Благодаря этому создаются условия для работы по посто­янному совер­шен­ство­ванию данного ПО, выпуска его новых версий и обнов­лений для действующих. Вероятна и органи­зация непре­рывно работающей службы техни­ческой поддержки. Коммер­ческий эффект дости­гается обычно путем продажи программного продукта заинте­ре­со­ванным пользо­ва­телям. Почти все программные продукты, представ­ленные на свободном рынке относятся к данному типу ПО.
  • Игровое ПО. Это условное название, поскольку большинство компью­терных игр можно отнести скорее к инвести­ци­онному типу ПО. Но в целом данный тип ПО харак­те­ри­зуется тем, что в процессе его разра­ботки отсут­ствует (или до минимума ограничена по сроку) фаза сопро­вож­дения, что сильно влияет на экономику процесса разра­ботки и его организацию.
  • Встро­енное ПО. Это прикладное ПО, созда­ваемое под конкретные техни­ческие изделия и постав­ляемое вместе с ними. Сопро­вож­дению, как правило, не подлежит, но харак­те­ри­зуется повышенными требо­ва­ниями к качеству, надеж­ности и безот­каз­ности. Объяс­няется это тем, что отзыв партии изделий произ­во­ди­телем – ситуация очень непри­ятная, несущая серьезные прямые коммер­ческие и репута­ци­онные потери для произ­во­дителя изделий.

Этапы разра­ботки ПО

Любой процесс, в том числе и разра­ботка ПО состоит из какого-то количества опреде­ленных этапов. Процесс разра­ботки ПО отличается тем, что обычно к работам над следующим этапом приступают, когда уровень готов­ности преды­дущего достигает около 80–90%. В наибольшей степени данное обсто­я­тельство относится к разра­ботке требо­ваний, поскольку именно в них какой-то уровень неопре­де­лен­ности иногда сохра­няется практи­чески до завер­шения проекта. Этот момент является серьезным фактором риска, и должен непре­рывно контролироваться.

Далее перечислим основные этапы разра­ботки инвести­ци­онного ПО. Практика показывает, что данный процесс обладает наиболее сложной струк­турой, поскольку здесь довольно часто ведутся парал­лельные работы над несколькими версиями программного продукта.

  1. Подго­товка. Опреде­ление общей концепции системы, востре­бо­ван­ности, реали­зу­е­мости и т.д.
  2. Разра­ботка конкретных требо­ваний к программному продукту.
  3. Разра­ботка архитектуры. Целью данного этапа является создание физической и логической архитектуры верхнего уровня, способной выполнить все требо­вания к разра­ба­ты­ва­емому программному продукту. На данном этапе возможна коррекция концепции, перво­на­чального техни­че­ского решения и требо­ваний. По его завер­шении, возможно начало парал­лельных работ над созданием следующей версии данного программного продукта.
  4. Этап поставки. Может включать несколько проме­жу­точных этапов, в зависи­мости от масштаб­ности проекта. На данной стадии проект приоб­ретает форму относи­тельно готового программного продукта.
  5. Опытная эксплу­а­тация. Продол­жи­тель­ность данного этапа также зависит от масштаб­ности проекта. На этом этапе выявляются и устра­няются недочеты, баги, уязви­мости и т.д. Прове­ряется качество работы ПО в различных условиях эксплу­а­тации, совме­сти­мость с другими программными продуктами и т.д. Данный этап можно также назвать этапом основной отладки и оптими­зации программного продукта.
  6. Этап коммер­ческой реали­зации, промыш­ленной эксплу­а­тации и т.д. Сопро­вож­дение и техни­ческая поддержка.
  7. Вывод из эксплу­а­тации. Прекра­щение техни­ческой поддержки и выпуска обновлений.
  8. Завер­шение работ.

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

Что касается Встро­енного ПО, то громадное значение приоб­ретает этап отладки, поскольку осуществ­ление техпод­держки, выпуск и поставка обнов­лений обычно бывают крайне затруднены, если вообще возможны.

 Под термином «корпо­рация» обычно понимают очень крупную компанию, количество сотруд­ников которой исчис­ляется в тысячах, разбро­санную геогра­фи­чески и занима­ю­щуюся бизнесом в нескольких отраслях. Понятно, что к инфор­ма­ци­онным системам (ИС), созда­ваемым для обслу­жи­вания нужд подобных бизнес-структур, предъ­яв­ляются особые требования.

Целью создание КИС является обеспе­чение инфор­ма­ци­онной поддержки всех видов деятель­ности, которыми занимается данная корпо­рация, на всех уровнях: учета, управ­ления, плани­ро­вания, прогно­зи­ро­вания и т.д.

Читать далее

Ответ на вопрос, для чего нужно создавать высоко­ка­че­ственное программное обеспе­чение, является логичным и законо­мерным для всех сторон процесса разработки.

Очевидно, что любой заказчик хочет получить высоко­ка­че­ственный программный продукт, удовле­тво­ряющий всем постав­ленным требо­ваниям, по возмож­ности дешевле и в отведённый для этого срок, чтобы в будущем предот­вратить расходы на обслу­жи­вание, поиск проблем, ошибок и т.д.

Читать далее

Приме­нение JNA (Java Native Access) в Java-проектах для доступа к функци­о­наль­ности и объектам, так называемых «нативных» библиотек — COM-DLL Microsoft Windows, представляет собой большой интерес.
Основным преиму­ще­ством является сокра­щение времени разра­ботки проекта, если вся необхо­димая функци­о­наль­ность уже содер­жится в какой-то стандартной библиотеке Microsoft Windows, либо есть сторонняя COM-DLL с необхо­димым набором решений, либо это уже приме­няемая клиентом COM-DLL бизнес-логики. Также невоз­можно переоценить возмож­ность исполь­зо­вания COM-DLL, работающей с объектами Microsoft .NET Framework, написанная, например, на C#.
Вторым, но не меньшим по значи­мости преиму­ще­ством является то, что в отличие от преды­дущей техно­логии JNI (Java Native Interface), здесь не придется писать библиотеку-оболочку на C, а это и в правду сомни­тельное удовольствие.
Но обо всем по порядку. Рассмотрим задачу, которая и подвигла на изучение техно­логии JNA.
Для нужд проекта потре­бо­валось получить список принтеров в системе, но также название их драйверов, т.к. имя принтера можно задать вручную какое угодно, хоть «MySuperPuperPrinter», а проекту требо­валось что-то более надежное и стабильное. Результат решения, которое предлагает Java, оказался неправильным…
Восполь­зуемся библио­текой javax.print:

import javax.print.PrintService;
import javax.print.PrintServiceLookup;
import javax.print.attribute.standard.PrinterMakeAndModel;
 
public class Tester {
    public static void main(String[] args) {
        final PrintService[] printServices = PrintServiceLookup.lookupPrintServices(null, null);
        for (PrintService printService : printServices) {
            System.out.println("PrinterName = " + printService.getName() +
                    ", PrinterMakeAndModel = " + printService.getAttribute(PrinterMakeAndModel.class));
        }
    }
}

Моя система показывает следующий результат:
PrinterName = Samsung SCX-4x28 Series PCL6, PrinterMakeAndModel = null
PrinterName = Samsung CLX-3180 Series, PrinterMakeAndModel = null
PrinterName = PDF Creator, PrinterMakeAndModel = null
PrinterName = FinePrint, PrinterMakeAndModel = null
PrinterName = Fax, PrinterMakeAndModel = null

Где инфор­мация о драйвере или модели принтера? Ее нет.
Посмотрим, что говорит об этом Oracle:

For attributes like
javax.print.attribute.standard.PrinterMakeAndModel
javax.print.attribute.standard.PrinterLocation
javax.print.attribute.standard.PrinterInfo
javax.print.PrintService.getAttribute should return meaningful information obtained from the Windows OS, instead of empty strings.
This bug can be reproduced always.
EVALUATION 2003-02-18
In Windows, this information can be retrieved from PRINTER_INFO_X structure.

ОК, проверим последнюю версию Java 1.7.0_17 – результат тот же, т.е. проблема 2003 не исправлена.

Ну да ладно, в принципе Java не предна­значена знать все тонкости опера­ци­онной системы, на которой она работает. Поэтому, как там Microsoft Windows разби­рается со своими принтерами, должен был бы знать какой-нибудь Microsoft Windows сервис. Он есть – это библиотека c:WindowsSystem32Winspool.drv и ее функция EnumPrinters. Функция перечисляет принтеры системы и складывает инфор­мацию как раз в структуру PRINTER_INFO_2.

Чтобы получить доступ к библиотеке Winspool.drv можно исполь­зовать JNI – для этого нам придется писать оболочку на С, h‑файл со стороны JNI и т.д. Для неспе­ци­а­листа в этой техно­логии доста­точно трудо­емкий и время затратный процесс, а уж о трудностях отладки я вообще молчу.

Посмотрим, что нам может предложить JNA.

Начнем с самого ориги­нального примера «Hello World», что мне прихо­дилось видеть, от разра­ботчика JNA.

import com.sun.jna.Library;
import com.sun.jna.Native;
public class Tester {
    public interface Kernel32 extends Library {
        boolean Beep(int frequency, int duration);
    }
    private static Kernel32 kernel32 = (Kernel32) Native.loadLibrary("kernel32", Kernel32.class);
    private static void toMorseCode(String letter) throws Exception {
        for (byte b : letter.getBytes()) {
            kernel32.Beep(1200, ((b == '.') ? 50 : 150));
            Thread.sleep(50);
        }
    }
    public static void main(String[] args) throws Exception {
        String helloWorld[][] = {
                {"....", ".", ".-..", ".-..", "---"}, // HELLO
                {".--", "---", ".-.", ".-..", "-.."}  // WORLD
        };
        for (String word[] : helloWorld) {
            for (String letter : word) {
                toMorseCode(letter);
                Thread.sleep(150);
            }
            Thread.sleep(350);
        }
    }
}

Т.е. доста­точно объявить интерфейс, расши­ряющий com.sun.jna.Library, объявить в нем необхо­димый метод с именем и сигна­турой, соответ­ству­ющими функции из библиотеки (в данном случае это библиотека c:WindowsSystem32kernel32.dll и функция Beep), и загрузить библиотеку, используя этот интерфейс с помощью com.sun.jna.Native. Первым параметром выступает имя библиотеки, которое может содержать полный путь к ней в файловой системе, а вторым – класс интер­фейса, который исполь­зуется для связы­вания интер­фейса и библиотеки посред­ством reflection.

Впечатляет, насколько просто можно услышать привет­ствие компьютера «Hello World», которое он проби­бикает азбукой Морзе.

Также интересные примеры можно найти тут.

Теперь вернемся к задаче проекта – получить имена принтеров и их драйверов в Microsoft Windows. Как было сказано выше, нужно обратиться к Winspool.drv и посред­ством ее функции EnumPrinters получить набор структуры PRINTER_INFO_2 для каждого принтера в системе. К счастью JNA позабо­тился об обертке структур «нативных» библиотек и предо­ставила класс com.sun.jna.Structure.

Тестовый код выглядит так:

import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.Win32Exception;
import com.sun.jna.platform.win32.WinDef;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.win32.StdCallLibrary;
import com.sun.jna.win32.W32APIOptions;
import java.util.Arrays;
import java.util.List;
 
public class WinSpoolTest {
     
    public interface Winspool extends StdCallLibrary {
         
        boolean EnumPrinters(int flags, String name, int level, Pointer pPrinterEnum, int cbBuf, IntByReference pcbNeeded, IntByReference pcReturned);
        public static final int PRINTER_ENUM_LOCAL = 0x00000002;
        public static final int LEVEL2 = 2;
         
        public static class PRINTER_INFO_2 extends Structure {
            public String pServerName;
            public String pPrinterName;
            public String pShareName;
            public String pPortName;
            public String pDriverName;
            public String pComment;
            public String pLocation;
            public WinDef.INT_PTR pDevMode;
            public String pSepFile;
            public String pPrintProcessor;
            public String pDatatype;
            public String pParameters;
            public WinDef.INT_PTR pSecurityDescriptor;
            public int Attributes;
            public int Priority;
            public int DefaultPriority;
            public int StartTime;
            public int UntilTime;
            public int Status;
            public int cJobs;
            public int AveragePPM;
             
            @Override
            protected List<String> getFieldOrder() {
                return Arrays.asList(
                        "pServerName",
                        "pPrinterName",
                        "pShareName",
                        "pPortName",
                        "pDriverName",
                        "pComment",
                        "pLocation",
                        "pDevMode",
                        "pSepFile",
                        "pPrintProcessor",
                        "pDatatype",
                        "pParameters",
                        "pSecurityDescriptor",
                        "Attributes",
                        "Priority",
                        "DefaultPriority",
                        "StartTime",
                        "UntilTime",
                        "Status",
                        "cJobs",
                        "AveragePPM");
            }
            public PRINTER_INFO_2() {
            }
             
            public PRINTER_INFO_2(int size) {
                super(new Memory(size));
            }
        }
    }
     
    final static Winspool winspool = (Winspool) Native.loadLibrary("Winspool.drv", Winspool.class, W32APIOptions.UNICODE_OPTIONS);
     
    public static void main(String[] args) throws Exception {
        final IntByReference pcbNeeded = new IntByReference();
        final IntByReference pcReturned = new IntByReference();
        winspool.EnumPrinters(Winspool.PRINTER_ENUM_LOCAL, null, Winspool.LEVEL2, null, 0, pcbNeeded, pcReturned);
        if (pcbNeeded.getValue() <= 0) {
            return;
        }
        final Winspool.PRINTER_INFO_2 pPrinterEnum = new Winspool.PRINTER_INFO_2(pcbNeeded.getValue());
        if (!winspool.EnumPrinters(Winspool.PRINTER_ENUM_LOCAL, null, Winspool.LEVEL2, pPrinterEnum.getPointer(),
                pcbNeeded.getValue(), pcbNeeded, pcReturned)) {
            throw new Win32Exception(Kernel32.INSTANCE.GetLastError());
        }
        pPrinterEnum.read();
        final Winspool.PRINTER_INFO_2[] result = (Winspool.PRINTER_INFO_2[]) pPrinterEnum.toArray(pcReturned.getValue());
        for (Winspool.PRINTER_INFO_2 pi : result) {
            System.out.printf("PrinterName = %1$2s, DriverName = %2$2sn", pi.pPrinterName, pi.pDriverName);
        }
    }
}

Что мы видим тут? Интерфейс Winspool теперь расширяет StdCallLibrary, а также содержит структуру PRINTER_INFO_2, наследницу Structure. Это необхо­ди­мость, т.к. вызов Native.loadLibrary посред­ством reflection строит карту интер­фейса и его структур и, если вынести эту структуру из интер­фейса, то карта будет непра­вильной. Ладно, если бы была ошибка, так структура просто будет ошибочно заполнена методом EnumPrinters – например, вместо имени принтера в поле pPrinterName будет только первая буква имени.

У структуры PRINTER_INFO_2 есть еще пара особен­ностей: она должна содержать конструктор без параметров и переопре­делять метод protected List getFieldOrder() – строгая после­до­ва­тель­ность и имено­вание полей.

Вот и все. Запустив тест, получим в консоли список принтеров и их драйверов:

PrinterName = Samsung SCX-4x28 Series PCL6, DriverName = Samsung SCX-4x28 Series PCL6
PrinterName = Samsung CLX-3180 Series, DriverName = Samsung CLX-3180 Series
PrinterName = PDF Creator, DriverName = CUSTPDF Writer
PrinterName = FinePrint, DriverName = FinePrint 7
PrinterName = Fax, DriverName = Microsoft Shared Fax Driver

Это именно то, что требовалось.
Как же можно еще упростить эту задачу? Выглядит все это все равно чересчур громоздко.
Например, нам не нужны все 21 поля структуры PRINTER_INFO_2, а только два – PrinterName и DriverName.
Посмотрим, как справ­ляется с этим .NET – ведь это неотъ­ем­лемая часть Microsoft Windows.
Код для консольного прило­жения .NET будет выглядеть так:

public class Tester
{
    public static void Main()
    {
        System.Printing.LocalPrintServer localPrintServer = new
        System.Printing.LocalPrintServer(System.Printing.PrintSystemDesiredAccess.AdministrateServer);
        foreach (var printer in localPrintServer.GetPrintQueues())
        {
            System.Console.WriteLine("PrinterName = {0}, DriverName = {1}", printer.FullName, printer.QueueDriver.Name);
        }
        System.Console.Read();
    }
}

Результат выводится на консоль:

PrinterName = Samsung SCX-4x28 Series PCL6, DriverName = Samsung SCX-4x28 Series PCL6
PrinterName = Samsung CLX-3180 Series, DriverName = Samsung CLX-3180 Series
PrinterName = PDF Creator, DriverName = CUSTPDF Writer
PrinterName = FinePrint, DriverName = FinePrint 7
PrinterName = Fax, DriverName = Microsoft Shared Fax Driver

Хм… То же самое. Значит мы на правильном пути.

Как теперь обернуть этот код в «нативную» библиотеку, получить к ней доступ через JNA и забрать результат в удобном для нашего проекта виде?
Начнем с того, что .NET не «нативная» среда — у нее свои CLR (Common Language Runtime), CIL (Common Intermediate Language) и CLI (Common Language Infrastructure-Standard), и .NET-DLL будет отлично линко­ваться другой библио­текой или программой .NET, но никак не как «нативная» библиотека к С- и Java-программам.

Как же быть?

На помощь приходит Unmanaged Exports (DllExport for .Net).

Ее можно подключить как nuget-плагин через Package Manager Console с помощью команды:

PM> Install-Package UnmanagedExports

Либо просто взять у произ­во­дителя архив с плагином UnmanagedExportLibrary.zip и скопи­ровать его в каталог шаблонов проек­ти­ро­вания Microsoft Visual Studio.

Обычно это My DocumentsVisual Studio 20**TemplatesProjectTemplates.

Потом создать новый проект C# на базе шаблона UnmanagedExportLibrary и наша «нативная» библиотека Winspool.dll почти готова.
Добавим в нее стати­ческий класс Export и стати­ческую функцию GetPrinterInfo:

using System;
using System.Text;
using RGiesecke.DllExport;
 
public static class Export
{
    [DllExport("GetPrinterInfo")]
    public static string GetPrinterInfo()
    {
        try
        {
            System.Text.StringBuilder sb = new System.Text.StringBuilder();
            System.Printing.LocalPrintServer localPrintServer = new System.Printing.LocalPrintServer(System.Printing.PrintSystemDesiredAccess.AdministrateServer);
            foreach (var printer in localPrintServer.GetPrintQueues())
            {
                sb.Append(printer.FullName).Append(";").Append(printer.QueueDriver.Name).Append(";");
            }
            return sb.ToString();
        }
        catch (Exception ex)
        {
            return ex.Message;
        }
    }
}

Стати­ческий класс и стати­ческая функция – это необхо­димое условие для приме­нения атрибута DllExport из библиотеки RGiesecke.DllExport. Таким образом, функция станет «нативной» в библиотеке и к ней можно будет непосред­ственно обращаться через JNA.

Функция GetPrinterInfo собирает инфор­мацию о принтерах и их драйверах в строку с разде­ли­телем «;», т.е. если потом преоб­ра­зовать эту строку по разде­лителю в массив строк, то по нечетным индексам будет имя принтера, а по четным – имя драйвера. Очень полезным для нас тут будет то, что DllExport может передавать простые типы в качестве параметров и возвра­щаемых значений функций, и к счастью для нас, что в этот список входит string.

Скомпи­лируем библиотеку Winspool.dll для платформы х86.

Код на Java выглядит так:

import com.sun.jna.Native;
import java.io.File;
 
public class WinSpoolTest {
 
    public interface Winspool extends com.sun.jna.Library {
        public String GetPrinterInfo();
    };
 
    public static void main(String[] args){
        final String path = new File("").getAbsolutePath() + "\lib\Winspool.dll";
        final Winspool lib = (Winspool) Native.loadLibrary(path, Winspool.class);
 
        final String res = lib.GetPrinterInfo();
        final String[] arr = res.split(";");
 
        for (int i = 0; i < arr.length; i += 2){
            System.out.printf("PrinterName = %1$2s, DriverName = %2$2sn", arr[i], arr[i+1]);
        }
    }
}

Выглядит значи­тельно проще, не правда ли?

Но что-то все равно бросается в глаза своим несовер­шен­ством… Да, это распо­ло­жение библиотеки Winspool.dll в папке lib, рядом с классом, и ее поиск по пути в файловой системе:

final String path = new File(“”).getAbsolutePath() + “\lib\Winspool.dll”;

В данном случае это не критично, но что делать, если проект как-то хитро компи­ли­руется, использует специ­альные папки для ресурсов, или динами­чески ориен­ти­рован на Java опреде­ленной разряд­ности – 64 бита или 32? Как сделать так, чтобы нам упростить поиск место­рас­по­ло­жения библиотеки? И как подго­товить «нативную» библиотеку для разной разряд­ности, да еще отследить в Java проекте, когда и какую использовать?

Вот тут выступает еще одно досто­инство JNA – автома­ти­ческое скани­ро­вание и нахож­дение «нативных» библиотек.

Если сделать так:
1 – где-нибудь создать две папки — win32-x86 и win32-x86-64
2 — скомпи­ли­ровать «нативную» библиотеку с атрибутом х86, и положить ее в папку win32-x86
3 — скомпи­ли­ровать с атрибутом х64, и положить в win32-x86-64
4 — создать из этих двух папок zip-архив
5 — переиме­новать его в jar
6 — подключить его к Java-проекту
то JNA автома­ти­чески подключает версию «нативной» библиотеки соответ­ственно разряд­ности Java.

Код станет еще проще, и нам не придется устра­ивать поиск библиотеки Winspool.dll, контроль соответ­ствия разряд­ности Java и библиотеки Winspool.dll.

import com.sun.jna.Native;
 
public class WinSpoolTest2 {
 
    public interface Winspool extends com.sun.jna.Library {
        public String GetPrinterInfo();
    };
 
    public static void main(String[] args){
        final Winspool lib = (Winspool) Native.loadLibrary("Winspool", Winspool.class);
 
        final String res = lib.GetPrinterInfo();
        final String[] arr = res.split(";");
 
        for (int i = 0; i < arr.length; i += 2){
            System.out.printf("PrinterName = %1$2s, DriverName = %2$2sn", arr[i], arr[i+1]);
        }
    }
}

Ну и напоследок, можно сделать еще красивее. Благодаря тому, что «нативная» библиотека .NET может возвращать строку неогра­ни­ченной длинны, то исполь­зо­вание результата с разде­ли­телем выглядит не очень дружественно.

Что более всего подходит для передачи данных в виде строки? Правильно – XML. Попробуем передать в качестве результата «нативной» библиотеки XML-сериа­ли­зо­ванный объект, а на стороне Java десери­а­лизуем в свой точно такой же объект.

На C# есть несколько возмож­ностей XML-сериа­ли­зации объектов, но сразу сделаю замечание, что распро­стра­ненная сериа­ли­зация из библиотеки System.Xml.Serialization не работает, если исполь­зовать ее в «нативной» библиотеке.

Будет ошибка System.IO «Illegal characters in path», хотя никакого IO при этом не исполь­зуется – это какой-то баг .NET, который пока не исправлен и в .NET 4.5.

К счастью есть другой механизм XML-сериа­ли­зации в библиотеке System.Runtime.Serialization.

Добавим к .NET-проекту класс PrintInfoList:

using System;
using System.Runtime.Serialization;
using System.Text;
using System.IO;
using System.Collections.Generic;
 
[DataContract(Name = "PrintInfoList")]
public class PrintInfoList
{
    [DataContract(Name = "PrintInfo")]
    public class PrintInfo
    {
        [DataMember(Name = "PrinterName")]
        public string PrinterName
        { get; set; }
 
        [DataMember(Name = "DriverName")]
        public string DriverName
        { get; set; }
    }
 
    private List<PrintInfo> list = new List<PrintInfo>();
 
    [DataMember(Name = "List")]
    public List<PrintInfo> List
    { get { return list; } }
 
    public String Serialize()
    {
        DataContractSerializer serializer = new DataContractSerializer(typeof(PrintInfoList));
        using (MemoryStream memoryStream = new MemoryStream())
        {
            serializer.WriteObject(memoryStream, this);
            memoryStream.Flush();
            string result = Encoding.UTF8.GetString(memoryStream.ToArray());
            result = System.Text.RegularExpressions.Regex.Replace(result, "<PrintInfoList.*?>", "<PrintInfoList>");
            return result;
        }
    }
}

Здесь можно видеть корневой класс PrintInfoList, который является списком объектов PrintInfo. Стоит обратить внимание на исполь­зо­вание атрибутов библиотеки System.Runtime.SerializationDataContract и DataMember. Их приме­нение должно быть совер­шенно очевидным – DataContract описывает объект, а DataMember – данные объекта, в данном случае это открытые поля. Кроме того, в конце метода десери­а­ли­зации в полученной XML-строке применена замена секции <PrintInfoList… со всеми простран­ствами имен Namespaces на — просто секцию <PrintInfoList>. Это нужно для упрощения примера, иначе придется обеспе­чивать обработку всех Namespaces на стороне Java.

Далее в класс Export добавим новую функцию GetPrinterInfo2, возвра­щающую запол­ненный и сериа­ли­зо­ванный объект PrintInfoList в виде XML-строки:

[DllExport("GetPrinterInfo2")]
public static string GetPrinterInfo2()
{
    try
    {
        PrintInfoList pil = new PrintInfoList();
        System.Printing.LocalPrintServer localPrintServer = new System.Printing.LocalPrintServer(System.Printing.PrintSystemDesiredAccess.AdministrateServer);
        foreach (var printer in localPrintServer.GetPrintQueues())
        {
            pil.List.Add(new PrintInfoList.PrintInfo
            {
                PrinterName = printer.FullName,
                DriverName = printer.QueueDriver.Name
            });
        }
        return pil.Serialize();
    }
    catch (Exception ex)
    {
        return ex.Message;
    }
}

Теперь вернемся к Java. Чтобы десери­а­ли­зовать XML-строку — результат метода GetPrinterInfo2 библиотеки Winspool.dll, создадим полный аналог объектов на Java:

import com.sun.jna.Native;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.*;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.List;
 
public class WinSpoolTest3 {
 
    @XmlAccessorType(XmlAccessType.NONE)
    @XmlRootElement(name = "PrintInfo")
    public static class PrintInfo {
         
        @XmlElement(name = "PrinterName")
        public String printerName;
 
        @XmlElement(name = "DriverName")
        public String driverName;
    }
 
    @XmlAccessorType(XmlAccessType.NONE)
    @XmlRootElement(name = "PrintInfoList")
    public static class PrintInfoList {
 
        @XmlElementWrapper(name = "List")
        @XmlElements({@XmlElement(name = "PrintInfo", type = PrintInfo.class)})
        public List<PrintInfo> list;
 
        public static PrintInfoList deserialize(String xml) {
            try {
                final InputStream inputStream = new ByteArrayInputStream(xml.getBytes("UTF-8"));
                try {
                    final JAXBContext context = JAXBContext.newInstance(PrintInfoList.class);
                    final Unmarshaller unmarshaller = context.createUnmarshaller();
                    return (PrintInfoList)unmarshaller.unmarshal(inputStream);
                } finally {
                    inputStream.close();
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            return null;
        }
    }
 
    public interface Winspool extends com.sun.jna.Library {
        public String GetPrinterInfo2();
    };
 
    public static void main(String[] args){
        final Winspool lib = (Winspool) Native.loadLibrary("Winspool", Winspool.class);
        final String res = lib.GetPrinterInfo2();
 
        final PrintInfoList pil = PrintInfoList.deserialize(res);
 
        for(final PrintInfo pi : pil.list){
            System.out.printf("PrinterName = %1$2s, DriverName = %2$2sn", pi.printerName, pi.driverName);
        }
    }
}

Т.е. XML-строка десери­а­ли­зуется в экземпляр PrintInfoList, который представляет собой список объектов PrintInfo, и который теперь гораздо удобнее и нагляднее исполь­зовать в любых частях проекта.

На базе полученного шаблона «нативная» DLL -> JNA -> Java можно создавать много разных и интересных межплат­фор­менных проектов. На C# можно писать библиотеки-оболочки, упрощающие работу с «внутрен­но­стями» Microsoft Windows, которые трудно реали­зуемы или вообще недоступны для Java, а качестве обмена между .NET и Java процессами исполь­зовать XML-сериа­ли­зо­ванные объекты. Ведь этот принцип можно исполь­зовать и в обратном направ­лении, т.е. от Java к .NET, и таким способом передавая параметры и прочие данные для обработки в «нативной» DLL через XML-сериа­ли­зо­ванные объекты.

Исходные коды и прочие ресурсы:

Все исходные коды можно найти здесь:
src-jna-for-access-to-native-com-dll-library.zip

В мире Java библиотека JAXB уже давно (как сообщает нам wikipedia с 2006 года) и по праву является распро­стра­ненным и очень удобным инстру­ментом для XML-cериа­ли­зации объектов и даже объектных моделей. В интернете есть масса примеров и целые сторонние библиотеки, постро­енные на JAXB — взять хотя бы docx4j (http://www.docx4java.org/trac/docx4j), которая работает с моделью документа в Open-XML формате. Так же много инфор­мации можно почерпнуть у самих авторов JAXB — https://jaxb.java.net/. Без труда найдутся в интернете прекрасные готовые исходники для генерации классов Java по XML-модели. Примеров масса и можно увлека­тельно провести время, изучая возмож­ности JAXB.

Но практи­чески все примеры приво­дятся для одного простого класса, который сериа­лизуют в XML, что называется, с нуля. А что делать, если уже есть некая готовая объектная модель со сложной струк­турой насле­до­вания и зависи­мостей, которые запрещены к изменениям? Где следует применить JAXB-аннотации? Куда их добавить — непосред­ственно в каждый из доброй полторы сотни классов, либо в один базовый? Если в каждый класс иерархии, то целесо­об­разна ли такая переделка с точки зрения объема выпол­ненной работы? Если в один-два базовых, то будет ли вообще работать XML-сериа­ли­зация? Да, действи­тельно много вопросов, и посмотрим, как JAXB с этим справ­ляется для конкретной задачи проекта.

Например, есть модель документа: Root – некий корневой элемент, контейнер для других элементов. Он может содержать Paragraph, который также является контей­нером для элементов Text. В свою очередь все элементы являются контей­нерами атрибутов, например у Paragraph это Alignment и Indent, у Text – Bold, Italic, FontName, FontSize и Color. Конечно, это неполный перечень, но в качестве примера доста­точный. Кроме того, условием для контей­неров элементов – Root и Paragraph, является то, что они могут содержать неогра­ни­ченное количество подэле­ментов, т.е. Root содержит много Paragraph, а Paragraph — много Text. Но контейнеры атрибутов могут содержать каждый атрибут в единственном экзем­пляре. Например, зачем элементу Text несколько FontName или Bold?
Таким образом, есть уже готовая модель элементов и атрибутов, со своей специ­фи­ческой бизнес-логикой, полями, методами и т.д.

//Начнем с базового абстрактного атрибута:

public abstract class Attribute<T> {
    protected T value;

    public Attribute(){
        this(null);
    }
    public Attribute(T value){
        this.value = value;
    }
    public T getValue() {
        return value;
    }
    public void setValue(T value) {
        this.value = value;
    }
}

//Его наследники для TextElement:


public class BoldAttribute extends Attribute<Boolean> {
    public BoldAttribute(){
        super(Boolean.FALSE);
    }
    public BoldAttribute(Boolean value){
        super(value);
    }
}
 
public class ItalicAttribute extends Attribute<Boolean> {
    public ItalicAttribute(){
        super(Boolean.FALSE);
    }
    public ItalicAttribute(Boolean value){
        super(value);
    }
}
 
public class FontNameAttribute extends Attribute<String> {
    public FontNameAttribute (){
        super("");
    }
    public FontNameAttribute (String value){
        super(value);
    }
}
 
public class FontSizeAttribute extends Attribute<Integer> {
    public FontSizeAttribute (){
        super(10);
    }
    public FontSizeAttribute (Integer value){
        super(value);
    }
}
 
public class ColorAttribute extends Attribute<java.awt.Color> {
    public ColorAttribute(){
        super(null);
    }
    public ColorAttribute(java.awt.Color value){
        super(value);
    }
} 

//Наследники Attribute для ParagraphElement:

public class AlignmentAttribute extends Attribute<AlignmentAttribute.VALUE> {
    public enum VALUE {
        NONE, LEFT, RIGHT, CENTER
    }
    public AlignmentAttribute() {
        super(VALUE.NONE);
    }
    public AlignmentAttribute(VALUE value) {
        super(value);
    }
}
 
public class IndentAttribute extends Attribute<Integer> {
    public IndentAttribute(){
        super(0);
    }
    public IndentAttribute(Integer value){
        super(value);
    }
}

//Теперь абстрактный терминальный элемент – контейнер атрибутов:

public abstract class TerminalElement {
    public TerminalElement(){
    }
    protected Map<Class, Attribute> attributes = new LinkedHashMap<Class, Attribute>();
    public List<Attribute> getAttributes() {
        return new ArrayList<Attribute>(attributes.values());
    }
    public void addAttribute(Attribute attribute){
        if (attribute == null){
            return;
        }
        attributes.put(attribute.getClass(), attribute);
    }
}

//Как видно, набор атрибутов содержит контейнер LinkedHashMap, который позволяет поддержать уникальность атрибутов и сохранить их последовательность по мере добавления.
//Простым наследником этого класса является Text:

public class TextElement extends TerminalElement {
    private String text;
    public TextElement(String text){
        this.text = text;
    }
    public TextElement(){
        this(null);
    }
    public String getText(){
        return text;
    }
    public void setText(String text){
        this.text = text;
    }
}

//Далее имеем абстракцию контейнера элементов:
public abstract class BranchElement extends TerminalElement {
    public BranchElement(){
    }
    protected List<TerminalElement> elements = new ArrayList<TerminalElement>();
    public void addElement(TerminalElement element){
        elements.add(element);
    }
    public List<TerminalElement> getElements(){
        return elements;
    }
}

//Как уже указывалось, контейнер элементов не ограничен в их количестве по типу, поэтому тут простой ArrayList.
//Ну и наследники абстрактного контейнера очень просты:
public class ParagraphElement extends BranchElement {
    //
}
public class RootElement extends BranchElement{
    //
}

Это все, хотя реальная модель содержит гораздо больше элементов, атрибутов, ограни­чения по содер­жимому: например, Root может содержать только Paragraph, в свою очередь Paragraph – только Text, Text – только текстовые атрибуты, Paragraph – свои, Root – свои. Сейчас же в этом примере можно сделать все, что угодно и заполнить модель ошибочными данными, но речь тут о JAXB, поэтому перейдем уже к XML-cериа­ли­зации этой модели, ее трудностям и их решениям.
Чтобы создать тестовую модель в памяти восполь­зуемся методом-стартером main(), например в RootElement:

public static void main(String[] args) throws Exception {
        final RootElement re = new RootElement();
        final ParagraphElement pe = new ParagraphElement();
        pe.addAttribute(new IndentAttribute(1000));
        pe.addAttribute(new AlignmentAttribute(AlignmentAttribute.VALUE.CENTER));
        final TextElement te = new TextElement("test!!!");
        pe.addElement(te);
        re.addElement(pe);
        te.addAttribute(new BoldAttribute(Boolean.TRUE));
        te.addAttribute(new ItalicAttribute(Boolean.TRUE));
        te.addAttribute(new ColorAttribute(java.awt.Color.RED));
        te.addAttribute(new FontName("Arial"));
        te.addAttribute(new FontSize(14));
    }

Вот теперь хотелось бы иметь возмож­ность сохранить эту структуру в XML-файл и зачитать обратно. Причем надо это сделать так, чтобы не были затронуты уже исполь­зуемые в бизнес-логике механизмы – т.е. готовые поля и методы должны остаться нетро­нутыми, и мы применим JAXB только чтобы незаметно для внешнего мира расширить возмож­ности модели сохранять/читать себя в/из XML-файла.

JAXB работает через аннотации: @XmlElement, @XmlType и т.д. Классы с такими аннота­циями через механизм рефлекшена проходят так называемый маппинг, и проис­ходит магия связы­вания объекта и его полей с XML.

Начнем с того, что корневой элемент надо обозначить аннотацией @XmlRootElement.

В данном случае, очевидно, что это сам RootElement:

@XmlRootElement(name = "RootElement")
public class RootElement extends BranchElement {
…

Атрибут name исполь­зуется для того, чтобы определить, как будет назван элемент в XML-файле.
Добавим в абстрактный атрибут специ­альные методы:

@XmlElement(name = "Value")
protected T getXmlValue(){
    return value;
}
protected void setXmlValue(T value){
    this.value = value;
}

Название метода getXmlValue не играет роли, главное, что мы делаем его protected и он невидим извне и не влияет на существующую бизнес-логику, а @XmlElement сообщает JAXB, что в XML у атрибута должен быть элемент с именем name = “Value”, в котором будет value атрибута. Причем для JAXB важно иметь пару getter-setter, чтобы он мог сохранять/читать XML.

Да, конечно, можно сделать проще и без getter-setter – повесить @XmlElement прямо на value атрибута:

@XmlElement(name = "Value")
protected T value;

Но мы догово­рились не трогать содер­жимое модели, а ее просто чуть-чуть и незаметно расширить.
Это все? Нет! Сам класс абстрактного атрибута нужно обозначить так:

@XmlAccessorType(XmlAccessType.NONE)
@XmlTransient
public abstract class Attribute<T> {
…

@XmlAccessorType(XmlAccessType.NONE) позволит избежать маппинга по умолчанию, когда все найденные поля, пары getter-setter и т.д. будут прину­ди­тельно пытаться записаться в XML.
@XmlTransient позволит в свою очередь избежать так называемой коллизии имен. Возникает такая коллизия из-за value – это и поле класса, и элемент в XML @XmlElement(name = “Value”), причем для JAXB регистр не имеет значения – value и Value одинаковы в XML. Чтобы точно сказать JAXB, что за Value пойдет в XML, мы и используем комби­нацию @XmlAccessorType(XmlAccessType.NONE) и @XmlTransient – таким образом класс «закры­вается» от JAXB, и открывает только getter-setter через @XmlElement(name = “Value”).
Конечно, напра­ши­вается подозрения в искус­ствен­ности проблемы – что мешает назвать @XmlElement(name = “MySuperPuperValue”) и избежать @XmlTransient? Да ничто, просто мне приятнее видеть в XML “Value”.
Идем дальше – что нам надо теперь сделать с атрибутами-наслед­никами? Да практи­чески ничего! Атрибуты, у которых value – простые типы (boolean, int …) или String не требуют вообще никаких доработок. JAXB сериа­лизует их в XML автома­ти­чески. Это не может не радовать, т.к. таких атрибутов в реальной модели действи­тельно очень много и подобная полная перера­ботка вызвала бы сомнения в целесо­об­раз­ности затрат.
В данном случае оставим без изменений BoldAttribute, ItalicAttribute, FontName и FontSize у текста, и IndentAttribute у параграфа.
А что с ColorAttribute? Вот он как раз не простой тип, а java.awt.Color, поэтому требует конвер­тации в некий простой. Для этого JAXB предлагает абстракцию XmlAdapter. Приведу тут простую реализацию:

public static class ColorAdapter extends XmlAdapter<Integer, java.awt.Color> {
        @Override
        public java.awt.Color unmarshal(Integer v) throws Exception {
            if (v == null){
                return null;
            }
            return new Color(v);
        }
 
        @Override
        public Integer marshal(java.awt.Color v) throws Exception {
            if(v == null) {
                return null;
            }
            return v.getRGB();
        }
}

Что делает этот адаптер? Получает java.awt.Color и преоб­разует его в число из его RGB значения.
Переопре­делим в ColorAttribute @XmlElement:

@XmlJavaTypeAdapter(ColorAdapter.class)
@XmlElement(name = "Value")
@Override
protected java.awt.Color getXmlValue(){
    return value;
}
 
@Override
protected void setXmlValue(java.awt.Color value){
    this.value = value;
}

Теперь JAXB сможет сохранить java.awt.Color.RED из примера в main() так:
‑65536.
Да, есть еще замечание по java.awt.Color – в реальной модели я исполь­зовал другой адаптер, т.к. color.getRGB() теряет alpha-состав­ляющую цвета и пришлось хранить цвет в XML в специ­альной строке RGBA:

protected static class ColorAdapter extends XmlAdapter<String, Color> {
        private final static Pattern REGEX_RGBA = Pattern.compile("rgba\((\d+),(\d+),(\d+),(\d+)\)");
 
        @Override
        public java.awt.Color unmarshal(String v) throws Exception {
            if (v == null || v.length() == 0) {
                return null;
            }
            final Matcher m = REGEX_RGBA.matcher(v);
            if (m.find() && m.groupCount() == 4) {
                final int r = Integer.valueOf(m.group(1));
                final int g = Integer.valueOf(m.group(2));
                final int b = Integer.valueOf(m.group(3));
                final int a = Integer.valueOf(m.group(4));
                return new java.awt.Color(r, g, b, a);
            }
            return null;
        }
 
    @Override
    public String marshal(java.awt.Color v) throws Exception {
        if (v == null) {
            return null;
        }
        return "rgba(" + v.getRed() + "," + v.getGreen() + "," + v.getBlue() + "," + v.getAlpha() + ")";
    }
}

При исполь­зо­вании этого адаптера в XML мы увидим следующее:
rgba(255,0,0,255)
Идем дальше – почему AlignmentAttribute нужно доработать?
У него value – Enum, а это тоже не простой тип, хотя вроде бы должно быть наоборот – номер и все тут. Но JAXB нужно научить работать с ним. Есть два способа – либо написать адаптер, анало­гично Color, либо применить аннотации. Я принял второе решение. Объявляем enum как тип в опреде­ленном namespace:

@XmlType(namespace = "AlignmentAttribute")
public enum VALUE {
    NONE, LEFT, RIGHT, CENTER
}

Это нужно для того, что если у нас много разных атрибутов, у которых enum называется одинаково – VALUE, как в имеющейся модели, и JAXB не поймет кто чей.
Переопре­делим у AlignmentAttribute @XmlElement:

@XmlElement(name = "Value")
@Override
protected VALUE getXmlValue() {
    return value;
}
 
@Override
protected void setXmlValue(VALUE value) {
    this.value = value;
}

По атрибутам все. Теперь мы имеем представ­ление, как доработать остальные для JAXB – атрибуты с простыми типами value остаются без изменений, enum-атрибуты – нуждаются в namespace типа, а атрибуты сложных типов нуждаются в адаптерах.
Переходим к элементам.
Аннотируем класс TerminalElement через @XmlAccessorType(XmlAccessType.NONE) – как уже описы­валось выше, чтобы исключить ненужный маппинг всех полей и методов по умолчанию. Вот BranchElement, благодаря этому, уже не нуждается в такой аннотации — она есть в супер-классе TerminalElement, и этого достаточно.
Да, еще одним важным условием для XML-cериа­ли­зации является наличие конструктора без параметров, поэтому BranchElement как раз содержит такой пустой конструктор.
Далее интереснее – добавим в TextElement следующее:

@XmlElement(name = "Text")
protected String getXmlText(){
    return text;
}
 
protected void setXmlText(String text){
    this.text = text;
}

Это значит, что в XML будет элемент с именем “Text”, содер­жащий текст TextElement – все просто:
test!!!
Для экспе­ри­мента вместо элемента можно исполь­зовать атрибут:

@ XmlAttribute (name = "Text")
protected String getXmlText(){
    return text;
}

И в XML мы увидим:

А что с атрибутами? JAXB может сериа­ли­зовать автома­ти­чески не только простые типы и String, но и простые наборы данных типа массив и List, где T – любой тип поддер­жи­вающий XML-сериа­ли­зацию, т.е. подго­тов­ленный для JAXB.
Что делать с нашей LinkedHashMap из TerminalElement? Правильно – писать адаптер, например AttributeListAdapter:

public static class AttributeListAdapter extends AbstractList {
        protected final Map<Class, Attribute> map;
 
        public AttributeListAdapter(Map<Class, Attribute> map){
            this.map = map;
        }
 
        @Override
        public boolean add(Object o) {
            if (o == null){
                return false;
            }
            return this.map.put(o.getClass(), (Attribute)o) != null;
        }
 
        @Override
        public Attribute get(int index) {
            final Iterator iterator = map.values().iterator();
            int n = 0;
            while(iterator.hasNext()){
                final Attribute attribute = (Attribute) iterator.next();
                if(n == index){
                    return attribute;
                }
                n++;
            }
            return null;
        }
 
        @Override
        public Iterator iterator() {
            return this.map.values().iterator();
        }
 
        @Override
        public int size() {
            return this.map.values().size();
        }
    }

Адаптер умеет передавать элементы из Map в виде простого списка элементов в JAXB, и наоборот принимать элементы из простого списка JAXB и записывать их в Map – все просто.
А в TextElement теперь нужно сделать оболочку (wrapper) для списка атрибутов:

@XmlElementWrapper(name = "Attributes")
@XmlElements({
        @XmlElement(name = "Bold", type = BoldAttribute.class),
        @XmlElement(name = "Italic", type = ItalicAttribute.class),
        @XmlElement(name = "Color", type = ColorAttribute.class),
        @XmlElement(name = "FontName", type = FontNameAttribute.class),
        @XmlElement(name = "FontSize", type = FontSizeAttribute.class)
    })
protected List<Attribute> getXmlAttributes() {
    return new AttributeListAdapter(attributes);
}

Т.е. мы указываем, что в XML список будет находиться в элементе с именем “Attributes”, а каждый встре­ченный атрибут так же получает опреде­ленное имя: BoldAttribute – Bold, FontNameAttribute – FontName и т.д.
Анало­гично описываем атрибуты ParagraphElement:

@XmlElementWrapper(name = "Attributes")
@XmlElements({
        @XmlElement(name = "Indent", type = IndentAttribute.class),
        @XmlElement(name = "Alignment", type = AlignmentAttribute.class)
})
protected List<Attribute> getXmlAttributes() {
    return newAttributeListAdapter(attributes);
}

И последнее – нужна оболочка (wrapper) для списка элементов для элемента-контейнера. Например, для ParagraphElement:

@XmlElementWrapper(name = "Elements")
@XmlElements({
        @XmlElement(name = "TextElement", type = TextElement.class)
})
protected List<TerminalElement> getXmlElements(){
    return elements;
}

В XML список подэле­ментов будет в элементе с именем “Elements”.
Т.к. elements уже простой список List<TerminalElement>, то писать адаптер не нужно – JAXB справится с ним автоматически.
Для RootElement аналогично:

@XmlElementWrapper(name = "Elements")
@XmlElements({
        @XmlElement(name = "ParagraphElement", type = ParagraphElement.class)
})
protected List<TerminalElement> getXmlElements(){
    return elements;
}

Теперь осталось написать методы сохранения/загрузки модели в/из XML-файла, добавим их прямо в тест в main():
Сохра­нение в XML-файл:

final Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(re, new File("c:\test_jaxb.xml"));

Загрузка из XML-файла:

final Unmarshaller unmarshaller = jc.createUnmarshaller();
final RootElement re2 = (RootElement) unmarshaller.unmarshal(new File("c:\ test_jaxb.xml"));

Модель из примера в XML-файле будет выглядеть так:


    
        
            
                
                    1000
                
                
                    CENTER
                
            
            
                
                    
                        
                            true
                        
                        
                            true
                        
                        
                            rgba(255,0,0,255)
                        
                        
                            Arial
                        
                        
                            14
                        
                    
                    test!!!
                
            
        
    

Файл прекрасно зачиты­вается обратно в модель.
Наша цель достигнута – мы можем сохранять текущее состояние документа в XML-файл и загружать его впослед­ствии обратно, нам не пришлось делать полную переделку модели, все незна­чи­тельные изменения находятся в базовых абстрактных классах, благодаря protected являются скрытыми извне, мы полностью контро­лируем и можем легко менять имено­вание и структуру элементов в XML-файле.

Файлы с исходным кодом: src / Приме­нение JAXB для XML-сериа­ли­зации объектной модели с иерархией наследования.

Одним из важных преиму­ществ библиотеки JavaFX является исполь­зо­вание стилей, прине­сенное из мира Web. Т.е. вместо того, чтобы быть зависимым от библиотек Look&Feel, пользо­ватель сам определяет внешний вид, причем это действи­тельно гибко и красиво, а также может изменяться динами­чески, «на лету», содержать анимацию и 3D графику. Используя концепцию стилей можно делать прило­жения с так называ­емыми «скинами», когда внешний вид полностью отделен от бизнес-логики, и даже разра­ба­ты­вается отдельно, например, профес­си­о­нальными дизайнерами.
Создадим простой пример диалога с кнопкой:

public class JavaFXDialog1 extends Application {
    @Override
    public void start(Stage stage) {
        final VBox vbox = new VBox();
        final Button button = new Button("test");
        vbox.getChildren().addAll(button);
        final Scene scene = new Scene(vbox, 150, 100);
        stage.setScene(scene);
        stage.show();
    }
  
    public static void main(String[] args) {
        launch(args);
    }

avafx and css styles 1
Применять стили можно несколькими способами:
1) Непосред­ственно в коде, например, изменить цвет фонта кнопки:

button.setStyle("-fx-text-fill: red");

avafx and css styles 2

2) Посред­ством CSS-файла, на который должен быть настроен класс Scene:
Для этого нужно создать файл с раширением .css и положить его в папку проекта, например — /css/styles.css.
Содер­жимое файла:

.button {
    -fx-text-fill: blue;
}

Да, тут главное не забыть настроить среду разра­ботки, чтобы она копировала эти файлы в сборку, иначе можно долго разби­раться, почему стиль не подключается
Например, в IntelliJ IDEA это делается так:
avafx and css styles 3

Теперь все готово, чтобы подключить к сцене стиль:

scene.getStylesheets().add((getClass().getResource("/css/styles.css")).toExternalForm());

Далее запустим проект и получим такой диалог:
avafx and css styles 4

Инструкция .button в CSS-файле говорит, что теперь все кнопки будут с голубым цветом:

final Button button1 = new Button("button1");
final Button button2 = new Button("button2");
vbox.getChildren().addAll(button1, button2);

avafx and css styles 5

А что если это не то, что нам надо? Что если нужно определить конкретную кнопку?
3) На помощь приходит опреде­ление пользо­ва­тель­ского стиля кнопки:
В styles.css пишем:

.button1 {
    -fx-text-fill: green;
}

А в коде:

button1.getStyleClass().add("button1");

Получаемый диалог выглядит так:
avafx and css styles 6

Теперь все кнопки, присо­еди­ненные к этому классу стиля, будут с зеленым текстом, причем метод add() говорит, что подобных стилей можно добавлять несколько, тем самым расширяя, переопре­деляя или перекрывая разные свойства элемента.
4) Определить пользо­ва­тельский стиль можно также через, так называемый, ID:
В styles.css пишем:

#button2 {
    -fx-text-fill: yellow;
}

А в коде:

button2.setId("button2");

Получаемый диалог:

avafx and css styles 7

Т.е. все элементы с одина­ковым ID будут выглядеть одинаково.
Что же еще интересного можно сделать с помощью стилей?

Стили могут обраба­тывать так называемые триггеры поведения, привне­сенные из мира XAML.
На примере кнопки к ним относятся такие события интер­фейса пользо­вателя, как получение фокуса, выделение, нажатие, пронос курсора мыши над кнопкой, и пр. – все то, что так хочется вынести из кода диалога, чтобы разгрузить его, оставив лишь бизнес-логику. Тем более, если клиент хочет не то, и не так, то это не отразиться на коде диалога – все будет определять динами­чески подклю­чаемый CSS-файл.
Например с помощью следующей CSS-инструкции можно изменить цвет кнопки, когда пользо­ватель проносит над ней курсор мыши:

.button:hover {
    -fx-background-color: orange;
}

Получаемый диалог при наведении курсора мыши:
avafx and css styles 8

Причем, как уже указы­валось ранее, это приведет к одина­ковому поведению всех классов button, а такой код:

.button1:hover {
    -fx-background-color: orange;
}

будет применять триггер только для тех, кто ссылается на класс «button1».

Подобный подход можно применять для многих триггеров, например для кнопок кроме hover есть еще foused, selected, pressed.
Жаль, что подобное нельзя применить непосред­ственно в коде, например:

button.setStyle(":hover -fx-text-fill: red");

Может быть в дальнейшем разра­ботчики JavaFX предо­ставят нам такую возможность.
Для чего мы все это изучаем? Ведь в интернете можно найти массу гораздо более мощных примеров, и цель статьи была не в их копиро­вании. Дело в том, что концепцию стилей и триггеров можно так же расширять для своих нужд, а вот это уже представляет интерес.
Мне нужно было сделать визуальный компонент на JavaFX, который бы служил для выбора размер­ности встав­ляемой в текст таблицы, причем внешний вид компо­нента, его цветовая схема, размеры и пр. не должны были бы быть его частью, а настра­и­вались бы внешними CSS-файлами. Выглядеть он должен примерно так:

avafx and css styles 9

Пользо­ватель проносит курсор мыши над сеткой и видит выделение, в данном случае для таблиц 7х8 ячеек, при клике на компо­ненте это выделение должно переда­ваться в программу, чтобы вставилась таблица 7х8 ячеек.
Конечно, можно отсле­живать выделение в коде и окрашивать ячейки опреде­ленным цветом, но что если одному клиенту нравится один цвет, другому – другой, либо цветовая схема опреде­ляется общим механизмом «скина», или еще как-нибудь – что делать тогда?

Тут нужен простой компонент для выделения ячеек, а внешний вид – не его забота.

Очевидно, напра­ши­вается взять преды­дущий пример триггером «hover» для кнопки, но он сработает для каждой ячейки при проносе над ней курсора мыши, те же ячейки, что теряют курсор мыши, перестают соответ­ствовать триггеру и теряют цвет выделения, как же оставить выделенным весь диапазон?
Для решения задачи нужно сделать свой триггер, сраба­ты­вающий на какое-то свойство объекта, например – «нахожусь в диапазоне выделения», а короче – inRage, которое можно было бы исполь­зовать в CSS-файле подобным образом:

.MyCell:inRange {
    -fx-border-width: 0.5;
    -fx-border-color: #ffffff;
    -fx-background-color: lightskyblue
}

Забегая вперед скажу, что задача доста­точно непростая, да еще и решение для версий JavaFX 1.7 и 1.8 – принци­пи­ально различные, и, например, для 1.7 придется исполь­зовать массу deprecated-методов.
Для начала рассмотрим сам компонент:

public class JavaFXDialog2  extends Application {
    @Override
    public void start(Stage stage) {
        final VBox vbox = new VBox();
        final GridPaneEx table = new GridPaneEx();
        table.init(10, 10);
        final Label label = new Label();
        label.setMaxWidth(Double.MAX_VALUE);
        label.setAlignment(Pos.CENTER);
        label.setTextAlignment(TextAlignment.CENTER);
        label.setStyle("-fx-padding: 3 0 5 0");
        label.textProperty().bind(table.text);
        vbox.getChildren().addAll(label, table);
        final Scene scene = new Scene(vbox, 350, 300);
        scene.getStylesheets().add((getClass().getResource("/css/styles.css")).toExternalForm());
        scene.setFill(null);
        stage.setScene(scene);
        stage.show();
    }
 
 
    public static void main(String[] args) {
        launch(args);
    }
 
 
    private void fireCreateTable(final int cols, final int rows){
        System.out.println("cols = " + cols + ", rows = " + rows);
    }
 
 
    protected class GridPaneEx extends GridPane {
  
        public final StringProperty text = new SimpleStringProperty("cancel");
        private int cols;
        private int rows;
  
        public GridPaneEx(){
            this.setOnMouseExited(new EventHandler() {
                @Override
                public void handle(MouseEvent mouseEvent) {
                    text.setValue("cancel");
                    deselectAll();
                }
            });
        }
  
        public void init(final int cols, final int rows){
            getChildren().clear();
            this.cols = cols;
            this.rows = rows;
            for (int col = 0; col < cols; col++){
                for (int row = 0; row < rows; row++){
                    final Button rect = new Button();
                    rect.setMinSize(30, 10);
                    add(rect, col, row);
                    final int selectedCol = col;
                    final int selectedRow = row;
                    rect.setOnMouseMoved(new EventHandler() {
                        @Override
                        public void handle(MouseEvent mouseEvent) {
                            selectRange(selectedCol, selectedRow);
                            text.setValue((selectedCol + 1) + " x " + (selectedRow + 1));
                        }
                    });
                    rect.setOnAction(new EventHandler() {
                        @Override
                        public void handle(ActionEvent actionEvent) {
                            fireCreateTable(selectedCol + 1, selectedRow + 1);
                            deselectAll();
                        }
                    });
                }
            }
            deselectAll();
        }
  
        private Node getNodeFromGridPane(int col, int row) {
            for (Node node : getChildren()) {
                if (GridPane.getColumnIndex(node) == col && GridPane.getRowIndex(node) == row) {
                    return node;
                }
            }
            return null;
        }
  
        private void selectCell(int col, int row, final boolean select){
            final Node node = getNodeFromGridPane(col, row);
            if (select){
                node.setStyle("-fx-border-width: 0.5; -fx-border-color: #ffffff; -fx-background-color: lightskyblue");
            } else {
                node.setStyle("-fx-border-width: 0.5; -fx-border-color: #000000; -fx-background-color: #ffffff");
            }
        }
  
        public void deselectAll(){
            for (int col = 0; col < cols; col++){
                for (int row = 0; row < rows; row++){
                    selectCell(col, row, false);
                }
            }
        }
        private void selectRange(int selectedCol, int selectedRow){
            deselectAll();
            for (int col = 0; col <= selectedCol; col++){
                for (int row = 0; row <= selectedRow; row++){
                    selectCell(col, row, true);
                }
            }
        }
    }
}

Интерес представляет метод selectCell, в котором окраска ячеек осуществ­ляется непосред­ственно в коде:
для обычных ячеек:

node.setStyle("-fx-border-width: 0.5; -fx-border-color: #000000; -fx-background-color: #ffffff");

для ячеек в выбранном диапазоне:

node.setStyle("-fx-border-width: 0.5; -fx-border-color: #ffffff; -fx-background-color: lightskyblue");

Т.к. в описании задачи указано, что чётко прописать цвета никак нельзя, то попробуем задать их с помощью собственного стиля, пометим в styles.css такой код:

#MyCellNormal {
    -fx-border-width: 0.5;
    -fx-border-color: #000000;
    -fx-background-color: #ffffff;
}
 
 
#MyCellInRange {
    -fx-border-width: 0.5;
    -fx-border-color: #ffffff;
    -fx-background-color: lightskyblue
}

А в метод selectCell:

private void selectCell(int col, int row, final boolean select){
            final Node node = getNodeFromGridPane(col, row);
            if (select){
                                node.setId("MyCellNormal");
            } else {
                                node.setId("MyCellInRange");
            }
        }

Уже лучше, не правда ли? Т.е. если клиента не устра­ивают цвета, то он меняет их прямо в styles.css, компонент же остается неизменным.
Но оказы­вается, что есть еще более элегантное решение – поместим в styles.css два стиля:

.MyCell {
    -fx-border-width: 0.5;
    -fx-border-color: #000000;
    -fx-background-color: #ffffff;
}
 
 
.MyCell:inRange {
    -fx-border-width: 0.5;
    -fx-border-color: #ffffff;
    -fx-background-color: lightskyblue
}

Это значит, что ячейки будут ориен­ти­ро­ваться на стиль «MyCell», а когда у них будет сраба­тывать триггер «inRange», по аналогии «hover» или «pressed» на кнопке, то цвет должен измениться соответ­ствующим образом.
Как «научить» ячейки запускать триггер?
Т.к. в качестве ячеек в нашем компо­ненте исполь­зуется Button , то необходимо переопре­делить его поведение в нашем «наследнике», а точнее переопре­делить поведение так называ­емого «псевдо-класса», для JavaFX 1.7 это делается так:

protected static class RangeButton extends Button {
        public RangeButton(){
            getStyleClass().add("MyCell");
        }
 
 
        private BooleanProperty inRange = new BooleanPropertyBase() {
 
 
            @Override
            protected void invalidated() {
                impl_pseudoClassStateChanged("inRange");
            }
 
 
            @Override
            public Object getBean() {
                return RangeButton.this;
            }
 
 
            @Override
            public String getName() {
                return "inRange";
            }
        };
 
 
        public boolean isInRange() {
            return inRange.get();
        }
 
 
        public void setInRange(boolean value) {
            inRange.set(value);
        }
 
 
        private static final long IN_RANGE_PSEUDOCLASS_STATE = StyleManager.getInstance().getPseudoclassMask("inRange");
 
 
        @Override
        public long impl_getPseudoClassState() {
            long mask = super.impl_getPseudoClassState();
            if (isInRange()) mask |= IN_RANGE_PSEUDOCLASS_STATE;
            return mask;
        }
    }

Как видно, все методы «…PseudoClass…» — deprecated.
Теперь вместо Button используем наш RangeButton и смотрите, как теперь преоб­ра­зится метод selectCell:

private void selectCell(int col, int row, final boolean select){
            final Node node = getNodeFromGridPane(col, row);
            ((RangeButton)node).setInRange(select);
        }

Т.е. изменение состояния поля «InRange» приводит к сраба­ты­ванию триггера из стиля и цвет выделенных в диапазоне ячеек изменится соответ­ствующим образом.
Это именно то, что нам нужно!
Для JavaFX 1.7 это работает, но в JavaFX 1.8 – запрещено, поэтому такой код перестанет компи­ли­ро­ваться, как только будет подключена JVM 1.8.
Что же предлагает нам новая версия? Как и ожидалось deprecated-методы были убраны и упрощена архитектура в целом. Теперь доста­точно сделать так:

protected static class RangeButton extends Button {        protected final PseudoClass pcInRange = PseudoClass.getPseudoClass("inRange");
 
 
        public RangeButton(){
            getStyleClass().add("MyCell");
        }
 
 
        protected final BooleanProperty inRange = new BooleanPropertyBase() {
 
 
            @Override
            protected void invalidated() {
                pseudoClassStateChanged(pcInRange, getValue());
            }
 
 
            @Override
            public Object getBean() {
                return RangeButton.this;
            }
 
 
            @Override
            public String getName() {
                return "inRange";
            }
        };
 
 
        public boolean isInRange() {
            return inRange.get();
        }
 
 
        public void setInRange(boolean value) {
            inRange.set(value);
        }
    }

Все работает, как и прежде.
Можно еще упростить код:

protected static class RangeButton extends Button {
        protected final BooleanProperty inRange;
        public RangeButton(){
            getStyleClass().add("MyCell");
            final PseudoClass pcInRange = PseudoClass.getPseudoClass("inRange");
            inRange = new SimpleBooleanProperty();
            inRange.addListener(new ChangeListener() {
                @Override
                public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
                    pseudoClassStateChanged(pcInRange, newValue);
                }
            });
        }

И изменить метод соответ­ствующим образом:

private void selectCell(int col, int row, final boolean select){
            final Node node = getNodeFromGridPane(col, row);
            ((RangeButton)node).inRange.setValue(select);
        }

Резюме: с помощью описанного способа на JavaFX можно создавать специ­альные пользо­ва­тельские свойства компо­нентов и связывать их со стилями, что очень удобно, если нужно, чтобы бизнес-логика была полностью абстра­ги­ро­ванной от внешнего вида.

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

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