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

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

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

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

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

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

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

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

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

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

Читать далее

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

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

Читать далее

Применение JNA (Java Nati­ve Access) в Java-проектах для доступа к функциональности и объектам, так называемых «нативных» библиотек — COM-DLL Micro­soft Win­dows, представляет собой большой интерес.
Основным преимуществом является сокращение времени разработки проекта, если вся необходимая функциональность уже содержится в какой-то стандартной библиотеке Micro­soft Win­dows, либо есть сторонняя COM-DLL с необходимым набором решений, либо это уже применяемая клиентом COM-DLL бизнес-логики. Также невозможно переоценить возможность использования COM-DLL, работающей с объектами Micro­soft .NET Frame­work, написанная, например, на C#.
Вторым, но не меньшим по значимости преимуществом является то, что в отличие от предыдущей технологии JNI (Java Nati­ve Inter­face), здесь не придется писать библиотеку-оболочку на C, а это и в правду сомнительное удовольствие.
Но обо всем по порядку. Рассмотрим задачу, которая и подвигла на изучение технологии JNA.
Для нужд проекта потребовалось получить список принтеров в системе, но также название их драйверов, т.к. имя принтера можно задать вручную какое угодно, хоть «MyS­u­per­Pu­per­Prin­ter», а проекту требовалось что-то более надежное и стабильное. Результат решения, которое предлагает 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));
        }
    }
}

Моя система показывает следующий результат:
Prin­ter­Na­me = Sam­sung SCX-4x28 Seri­es PCL6, Prin­ter­Ma­ke­And­Mo­del = null
Prin­ter­Na­me = Sam­sung CLX-3180 Seri­es, Prin­ter­Ma­ke­And­Mo­del = null
Prin­ter­Na­me = PDF Creator, Prin­ter­Ma­ke­And­Mo­del = null
Prin­ter­Na­me = Fine­Print, Prin­ter­Ma­ke­And­Mo­del = null
Prin­ter­Na­me = Fax, Prin­ter­Ma­ke­And­Mo­del = null

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

For attri­bu­tes like
javax.print.attribute.standard.PrinterMakeAndModel
javax.print.attribute.standard.PrinterLocation
javax.print.attribute.standard.PrinterInfo
javax.print.PrintService.getAttribute should return mea­ning­ful infor­ma­ti­on obtai­ned from the Win­dows OS, ins­tead of empty strings.
This bug can be repro­du­ced always.
EVA­LUA­TI­ON 2003-02-18
In Win­dows, this infor­ma­ti­on can be retrie­ved from PRINTER_INFO_X structure.

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

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

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

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

Начнем с самого оригинального примера «Hel­lo 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.

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

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

Теперь вернемся к задаче проекта – получить имена принтеров и их драйверов в Micro­soft Win­dows. Как было сказано выше, нужно обратиться к Winspool.drv и посредством ее функции Enum­Prin­ters получить набор структуры 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 теперь расширяет StdCall­Li­bra­ry, а также содержит структуру PRINTER_INFO_2, наследницу Struc­tu­re. Это необходимость, т.к. вызов Native.loadLibrary посредством reflec­tion строит карту интерфейса и его структур и, если вынести эту структуру из интерфейса, то карта будет неправильной. Ладно, если бы была ошибка, так структура просто будет ошибочно заполнена методом Enum­Prin­ters – например, вместо имени принтера в поле pPrin­ter­Na­me будет только первая буква имени.

У структуры PRINTER_INFO_2 есть еще пара особенностей: она должна содержать конструктор без параметров и переопределять метод pro­tec­ted List get­Fiel­dOr­der() – строгая последовательность и именование полей.

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

Prin­ter­Na­me = Sam­sung SCX-4x28 Seri­es PCL6, Dri­ver­Na­me = Sam­sung SCX-4x28 Seri­es PCL6
Prin­ter­Na­me = Sam­sung CLX-3180 Seri­es, Dri­ver­Na­me = Sam­sung CLX-3180 Series
Prin­ter­Na­me = PDF Creator, Dri­ver­Na­me = CUSTPDF Writer
Prin­ter­Na­me = Fine­Print, Dri­ver­Na­me = Fine­Print 7
Prin­ter­Na­me = Fax, Dri­ver­Na­me = Micro­soft Shared Fax Driver

Это именно то, что требовалось.
Как же можно еще упростить эту задачу? Выглядит все это все равно чересчур громоздко.
Например, нам не нужны все 21 поля структуры PRINTER_INFO_2, а только два – Prin­ter­Na­me и Dri­ver­Na­me.
Посмотрим, как справляется с этим .NET – ведь это неотъемлемая часть Micro­soft 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();
    }
}

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

Prin­ter­Na­me = Sam­sung SCX-4x28 Seri­es PCL6, Dri­ver­Na­me = Sam­sung SCX-4x28 Seri­es PCL6
Prin­ter­Na­me = Sam­sung CLX-3180 Seri­es, Dri­ver­Na­me = Sam­sung CLX-3180 Series
Prin­ter­Na­me = PDF Creator, Dri­ver­Na­me = CUSTPDF Writer
Prin­ter­Na­me = Fine­Print, Dri­ver­Na­me = Fine­Print 7
Prin­ter­Na­me = Fax, Dri­ver­Na­me = Micro­soft Shared Fax Driver

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

Как теперь обернуть этот код в «нативную» библиотеку, получить к ней доступ через JNA и забрать результат в удобном для нашего проекта виде?
Начнем с того, что .NET не «нативная» среда — у нее свои CLR (Com­mon Lan­guage Run­ti­me), CIL (Com­mon Inter­me­dia­te Lan­guage) и CLI (Com­mon Lan­guage Infra­st­ruc­tu­re-Stan­dard), и .NET-DLL будет отлично линковаться другой библиотекой или программой .NET, но никак не как «нативная» библиотека к С- и Java-программам.

Как же быть?

На помощь приходит Unma­na­ged Exports (Dll­Ex­port for .Net).

Ее можно подключить как nuget-плагин через Packa­ge Mana­ger Con­so­le с помощью команды:

PM> Install-Packa­ge UnmanagedExports

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

Обычно это My Docu­ments­Vi­su­al Stu­dio 20**TemplatesProjectTemplates.

Потом создать новый проект C# на базе шаблона Unma­na­gedEx­port­Li­bra­ry и наша «нативная» библиотека Winspool.dll почти готова.
Добавим в нее статический класс Export и статическую функцию Get­Prin­ter­In­fo:

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;
        }
    }
}

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

Функция Get­Prin­ter­In­fo собирает информацию о принтерах и их драйверах в строку с разделителем «;», т.е. если потом преобразовать эту строку по разделителю в массив строк, то по нечетным индексам будет имя принтера, а по четным – имя драйвера. Очень полезным для нас тут будет то, что Dll­Ex­port может передавать простые типы в качестве параметров и возвращаемых значений функций, и к счастью для нас, что в этот список входит 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 «Ille­gal cha­rac­ters 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;
        }
    }
}

Здесь можно видеть корневой класс Prin­t­In­fo­Li­st, который является списком объектов Prin­t­In­fo. Стоит обратить внимание на использование атрибутов библиотеки System.Runtime.SerializationDat­a­Con­tract и Data­Mem­ber. Их применение должно быть совершенно очевидным – Dat­a­Con­tract описывает объект, а Data­Mem­ber – данные объекта, в данном случае это открытые поля. Кроме того, в конце метода десериализации в полученной XML-строке применена замена секции <Prin­t­In­fo­Li­st… со всеми пространствами имен Name­spaces на — просто секцию <Prin­t­In­fo­Li­st>. Это нужно для упрощения примера, иначе придется обеспечивать обработку всех Name­spaces на стороне Java.

Далее в класс Export добавим новую функцию GetPrinterInfo2, возвращающую заполненный и сериализованный объект Prin­t­In­fo­Li­st в виде 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-строка десериализуется в экземпляр Prin­t­In­fo­Li­st, который представляет собой список объектов Prin­t­In­fo, и который теперь гораздо удобнее и нагляднее использовать в любых частях проекта.

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

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

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

ПОСМОТРИТЕ НАШЕ СЕРВИС ПРЕДЛОЖЕНИЕ 

В мире Java библиотека JAXB уже давно (как сообщает нам wiki­pe­dia с 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 – некий корневой элемент, контейнер для других элементов. Он может содержать Para­graph, который также является контейнером для элементов Text. В свою очередь все элементы являются контейнерами атрибутов, например у Para­graph это Align­ment и Indent, у Text – Bold, Ita­lic, Font­Na­me, Font­Si­ze и Color. Конечно, это неполный перечень, но в качестве примера достаточный. Кроме того, условием для контейнеров элементов – Root и Para­graph, является то, что они могут содержать неограниченное количество подэлементов, т.е. Root содержит много Para­graph, а Para­graph — много Text. Но контейнеры атрибутов могут содержать каждый атрибут в единственном экземпляре. Например, зачем элементу Text несколько Font­Na­me или 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 может содержать только Para­graph, в свою очередь Para­graph – только Text, Text – только текстовые атрибуты, Para­graph – свои, 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;
}

Название метода getXmlVa­lue не играет роли, главное, что мы делаем его pro­tec­ted и он невидим извне и не влияет на существующую бизнес-логику, а @XmlElement сообщает JAXB, что в XML у атрибута должен быть элемент с именем name = “Value”, в котором будет value атрибута. Причем для JAXB важно иметь пару get­ter-set­ter, чтобы он мог сохранять/читать XML.

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

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

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

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

@XmlAccessorType(XmlAccessType.NONE) позволит избежать маппинга по умолчанию, когда все найденные поля, пары get­ter-set­ter и т.д. будут принудительно пытаться записаться в XML.
@XmlTransient позволит в свою очередь избежать так называемой коллизии имен. Возникает такая коллизия из-за value – это и поле класса, и элемент в XML @XmlElement(name = “Value”), причем для JAXB регистр не имеет значения – value и Value одинаковы в XML. Чтобы точно сказать JAXB, что за Value пойдет в XML, мы и используем комбинацию @XmlAccessorType(XmlAccessType.NONE) и @XmlTransient – таким образом класс «закрывается» от JAXB, и открывает только get­ter-set­ter через @XmlElement(name = “Value”).
Конечно, напрашивается подозрения в искусственности проблемы – что мешает назвать @XmlElement(name = “MyS­u­per­Pu­perVa­lue”) и избежать @XmlTransient? Да ничто, просто мне приятнее видеть в XML “Value”.
Идем дальше – что нам надо теперь сделать с атрибутами-наследниками? Да практически ничего! Атрибуты, у которых value – простые типы (boolean, int …) или String не требуют вообще никаких доработок. JAXB сериализует их в XML автоматически. Это не может не радовать, т.к. таких атрибутов в реальной модели действительно очень много и подобная полная переработка вызвала бы сомнения в целесообразности затрат.
В данном случае оставим без изменений Bol­d­At­tri­bu­te, Ita­li­c­At­tri­bu­te, Font­Na­me и Font­Si­ze у текста, и Inden­t­At­tri­bu­te у параграфа.
А что с Color­At­tri­bu­te? Вот он как раз не простой тип, а java.awt.Color, поэтому требует конвертации в некий простой. Для этого JAXB предлагает абстракцию Xml­Ad­ap­ter. Приведу тут простую реализацию:

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 значения.
Переопределим в Color­At­tri­bu­te @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)
Идем дальше – почему Align­men­t­At­tri­bu­te нужно доработать?
У него value – Enum, а это тоже не простой тип, хотя вроде бы должно быть наоборот – номер и все тут. Но JAXB нужно научить работать с ним. Есть два способа – либо написать адаптер, аналогично Color, либо применить аннотации. Я принял второе решение. Объявляем enum как тип в определенном namespace:

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

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

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

По атрибутам все. Теперь мы имеем представление, как доработать остальные для JAXB – атрибуты с простыми типами value остаются без изменений, enum-атрибуты – нуждаются в name­space типа, а атрибуты сложных типов нуждаются в адаптерах.
Переходим к элементам.
Аннотируем класс Ter­mi­nal­Ele­ment через @XmlAccessorType(XmlAccessType.NONE) – как уже описывалось выше, чтобы исключить ненужный маппинг всех полей и методов по умолчанию. Вот Bran­ch­Ele­ment, благодаря этому, уже не нуждается в такой аннотации — она есть в супер-классе Ter­mi­nal­Ele­ment, и этого достаточно.
Да, еще одним важным условием для XML-cериализации является наличие конструктора без параметров, поэтому Bran­ch­Ele­ment как раз содержит такой пустой конструктор.
Далее интереснее – добавим в Text­Ele­ment следующее:

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

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

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

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

А что с атрибутами? JAXB может сериализовать автоматически не только простые типы и String, но и простые наборы данных типа массив и List, где T – любой тип поддерживающий XML-сериализацию, т.е. подготовленный для JAXB.
Что делать с нашей Lin­ked­Hash­Map из Ter­mi­nal­Ele­ment? Правильно – писать адаптер, например 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 – все просто.
А в Text­Ele­ment теперь нужно сделать оболочку (wrap­per) для списка атрибутов:

@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 список будет находиться в элементе с именем “Attri­bu­tes”, а каждый встреченный атрибут так же получает определенное имя: Bol­d­At­tri­bu­te – Bold, Font­Na­me­At­tri­bu­te – Font­Na­me и т.д.
Аналогично описываем атрибуты ParagraphElement:

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

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

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

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

@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-файл и загружать его впоследствии обратно, нам не пришлось делать полную переделку модели, все незначительные изменения находятся в базовых абстрактных классах, благодаря pro­tec­ted являются скрытыми извне, мы полностью контролируем и можем легко менять именование и структуру элементов в XML-файле.

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

ПОСМОТРИТЕ НАШЕ СЕРВИС ПРЕДЛОЖЕНИЕ 

Одним из важных преимуществ библиотеки Java­FX является использование стилей, принесенное из мира 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;
}

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

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

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

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

Инструкция .but­ton в 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

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

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

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

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

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

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

avafx and css styles 9

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

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

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

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

Забегая вперед скажу, что задача достаточно непростая, да еще и решение для версий Java­FX 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);
                }
            }
        }
    }
}

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

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», а когда у них будет срабатывать триггер «inRan­ge», по аналогии «hover» или «pres­sed» на кнопке, то цвет должен измениться соответствующим образом.
Как «научить» ячейки запускать триггер?
Т.к. в качестве ячеек в нашем компоненте используется But­ton , то необходимо переопределить его поведение в нашем «наследнике», а точнее переопределить поведение так называемого «псевдо-класса», для Java­FX 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;
        }
    }

Как видно, все методы «…Pseu­do­Class…» — deprecated.
Теперь вместо But­ton используем наш Ran­ge­But­ton и смотрите, как теперь преобразится метод selectCell:

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

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

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);
        }

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

ПОСМОТРИТЕ НАШЕ СЕРВИС ПРЕДЛОЖЕНИЕ 

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

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

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

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