Здравствуй, уважаемый %habrauser%. Около 3 лет назад я написал статью о том Как я создал систему установки принтеров на работе. Не могу не согласится с комментариями и отзывами от прошлой статьи, гласящие о том, что для установки принтеров можно воспользоваться групповыми политиками, но в мире в enterprise, больших или малых, возможны и другие случаи разного характера когда не смотря на наличие домена и групповых политик не представляется удобным\возможным разворачивать принтеры через GPO-шки. Учитывая собранный опыт и отзывы пришло время показать о моей новой переписанной системе с гораздо большим и удобным функционалом с сохранением минимализма, и я надеюсь, что кому-то она пригодится. В статье я также расскажу о том, с какими трудностями я столкнулся при создании этой системы. Кому стало интересно добро пожаловать под кат. Осторожно! Будет много картинок!

Почему надо менять то, что и так работает


Идея о том, что систему надо переписать пришла когда в очередной раз я получил жалобу от HelpDesk о том, что система не очень удобная и много возни с добавлением нового принтера. Каждый раз после установки принтера на сервер, необходимо было создавать два файла, где первый файл это VBS скрипт установки с записанным в него адресом нового принтера (Например: \\server01\printer1) и BAT файл который запускает этот VBS скрипт. Этот самый BAT файл загружался в систему установки принтеров, откуда пользователи находили нужный принтер и скачивали его. Для пользователей все просто: скачал -> запустил -> принтер поставился. Но настройка для технического отдела была нудной и долгой и эту проблему необходимо было решить.

Постановка требований


Ниже приведен список требований, которые были выполнены по мере разработки:

  1. В первую очередь должна открываться Home страница с выбором филиала
  2. Должна быть возможность поиска филиала по имени на Home странице
  3. После выбора филиала должна открываться страница со списком принтеров соответствующего филиала
  4. На странице выбора принтеров соответствующего филиала должна быть возможность поиска принтера по имени
  5. На странице выбранного принтера должна показываться следующая информация

    • Имя принтера
    • Изображение принтера
    • Отображение знака Online если принтер в сети
    • Тип принтера
    • Описание принтера
    • IP адрес
    • Местоположение принтера
    • Производитель принтера
    • Иконка производтеля принтера (опционально)
    • Количество просмотров принтера (опционально)
    • Возможность отправлять ссылку на принтер по почте по клику по ссылке mailto типа
    • Кнопка «Install» для загрузки установочного скрипта, который генерируется на лету

  6. Возможность глобального поиска как и принтеров так и филиалов
  7. Страница с мануалом текст, которого можно будет писать в админке в WYSIWYG редакторе
  8. Админ панель для управления всеми возможностями системы
  9. Должна быть минимальная API для получения какой-либо информации
  10. Возможность изменения данных для подключения к БД, если БД поднят на другом сервере без редактирования исходного кода приложения и пересобирания его в WAR
  11. Должна быть возможность изменения скрипта установки принтера на любую другую
  12. Для облегчения задачи первоначальной установки системы администратору, установка БД должна выполняться через страницу /install с сохранением введенных параметров подключения к БД (IP БД сервера с mysql (или localhost), пользователь и пароль от БД)

Выбор движка


Конечно есть большое количество возможных вариантов того, на чем можно было бы написать приложение, но я выбрал Java Server Faces Framework (JSF) так, как хотелось немного пощупать то, что это такое, да и возможность упаковать приложение в готовый WAR файл и деплоить его на Tomcat как и на Linux, так и на Windows подкупила меня.

Разработка


Введение


До того момента, я ни разу не писал на Java веб приложения, поэтому все начиналось с трудом. Имел опыт с PHP и то только умел кодить на своем велосипеде самописном движке коричневого качества, да и сейчас вряд ли, наверное, цветовой окрас качества новой системы отличается. Хотелось бы узнать мнение опытных разработчиков по этому поводу. Ссылка на гитхаб будет в конце статьи.

Разработку я начал вести сперва в NetBeans, но спустя некоторе время перешел на Eclipse потому, что Eclipse показался мне гораздо более удобным и понятным.

Проект был создан по MVC паттерну, где: JSP — это сами страницы с HTML и JSTL разметкой (views); Servlet — контроллер, который обрабатывет POST или GET запросы этой страницы; DAO класс страницы — тянет или сохраняет нужную информацию в БД. Если обобщить, то все страницы в системе состоят из JSP и своего Servlet-а, но могут не иметь своего DAO класса, если они статичны, а функции для API состоят только из Servlet-а и DAO класса. В качестве стилистики дизайна я использовал Bootstrap 4.

Структура БД


Структура БД достаточно проста и приведена ниже на картинке:



Имеются 5 таблиц для хранения информации:

  • branches — предназначен для хранения филиалов
  • printers — здесь хранятся принтеры
  • printerstype — типы принтеров
  • users — администраторы
  • systemsettings — настройки системы, где столбец «parameter» имя настройки и «value» его значение

Трудности


Первая


С первой трудностью с которой я столкнулся, это было то, как сделать так, чтобы страницу возможно было бы открыть только по его относительному пути (например /home), а не открывая JSP файл (например Home.jsp) непосредственно. Если открывать файл JSP напрямую, то вызов Servlet-а этой страницы не происходил, соответственно и нужная информация не тянулась на страницу. Решение оказалось простым. Достаточно нужно было в начало каждой JSP страницы поставить проверку того, если станицу открыли не по относительному пути, то перенаправить его туда, а в самом Servlet-е возвращать содержимое JSP файла страницы. Некоторые функции перенаправления я писал сначала не верно и страница уходила в бесконечный loop, с чем я спустя 3 дня танцев с бубном и гадания на кофейной гуще справился.

Вторая




Вторая трудность заключалась в том, как реализовать генерирование файла скрипта установки принтера на лету и чтобы для каждего принтера оно было индивидуальным, и чтобы его можно было редактировать. Решение данной проблемы пришло спустя некоторое время и оказалось следующим. Очевидно, что текст самого скрипта необходимо хранить в БД. Для этих целей создал таблицу systemsettings, добавил туда строку, где в столбец «parameter» вписал «installscript», а в «value» сам VBS скрипт из прошлой статьи. А что если у нас завтра будет не VBS, а PowerShell скрипт или любой другой? Поэтому в таблицу systemsettings добавил еще одну строку, где в столбец «parameter» вписал «installscriptextension», а в «value» значение «vbs». Далее создал Servlet download, который принимает GET значение переменной целочисленного типа printerid и выглядит следущим образом:

if(request.getParameter("printerid") != null)
{
   // Получаем ID принтера из GET запроса
   Integer printerid = 0;
   try
   {
      printerid = Integer.parseInt(request.getParameter("printerid"));
   }
   catch (NumberFormatException e) 
   {
      request.getRequestDispatcher("/home").forward(request, response);
   }
        	
   // Получаем наш класс принтера со всеми его параметрами из БД
   Printer printer = DownloadScriptDao.GetPrinter(printerid);
	
   if(printer != null)
   {
      String scriptname = "none";
      String script= "none";
      String scriptextension = "txt";
      
      // Получаем наш скрипт из БД
      script = DownloadScriptDao.GetInstallScript();
      // Получаем расширение скрипта из БД
      scriptextension = DownloadScriptDao.GetInstallScriptExtension();
		
      // Получаем имя принтера
      scriptname = printer.GetName().trim();
      
      // Если обнаруживаем в скрипте %PRINTER_NAME%, то заменяем его значение Имени принтера полученное из БД
      script = script.replace("%PRINTER_NAME%", printer.GetName());
      
      // Если обнаруживаем в скрипте %PRINTER_DESCRIPTION%, то заменяем его значение Описания принтера полученное из БД
      script = script.replace("%PRINTER_DESCRIPTION%", printer.GetDescription());
      
      // Если обнаруживаем в скрипте %PRINTER_SHARE_NAME%, то заменяем его значение Общего серверного адреса принтера полученное из БД
      script = script.replace("%PRINTER_SHARE_NAME%", printer.GetServerShareName());
      
      // Если обнаруживаем в скрипте %PRINTER_ID%, то заменяем его значение на ID принтера из БД
      script = script.replace("%PRINTER_ID%", printer.GetId().toString());
      
      // Если обнаруживаем в скрипте %PRINTER_BRANCH_ID%, то заменяем его значение на ID филиала принтера из БД
      script = script.replace("%PRINTER_BRANCH_ID%", printer.GetBranchId().toString());
      
      // Если обнаруживаем в скрипте %PRINTER_BRANCH_ID%, то заменяем его значение на IP принтера из БД
      script = script.replace("%PRINTER_IP%", printer.GetIp());
      
      // Если обнаруживаем в скрипте %PRINTER_BRANCH_ID%, то заменяем его значение на Имени производителя принтера из БД
      script = script.replace("%PRINTER_VENDOR%", printer.GetVendor());
      
      // Если обнаруживаем в скрипте %PRINTER_TYPE%, то заменяем его значение на Тип принтера из БД
      script = script.replace("%PRINTER_TYPE%", printer.GetPrinterTypeId().toString());
      
      // Если обнаруживаем в скрипте %PRINTER_CUSTOM_FIELD1%, то заменяем его значение на Свой параметр принтера из БД
      script = script.replace("%PRINTER_CUSTOM_FIELD1%", printer.GetCustomField1());
      
      // Даем браузеру понять что сейчас будет возвращен файл а не страница
      response.setContentType("application/octet-stream");
      
      // Даем браузеру понять что имя файла будет значение имени принтера и его расширение
      response.setHeader("Content-Disposition", "attachment;filename=" + scriptname + "." + scriptextension);
      
      // Задаем содержимое скачиваемого файла (скрипта) после всех изменений
      StringBuffer sb = new StringBuffer(script);
      InputStream in = new ByteArrayInputStream(sb.toString().getBytes("UTF-8"));
      ServletOutputStream out = response.getOutputStream();
      
      byte[] outputByte = new byte[1];
      while(in.read(outputByte, 0, 1) != -1)
      {
      	out.write(outputByte, 0, 1);
      }
      in.close();
      out.flush();
      out.close();
   }
}

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

Третья




И наконец пробема которую все не удавалось решить — это возможность установки БД при первоначальной настройке прямо с браузера с последующим сохранением данных подключения для работы системы в конфигурационный файл. Сперва я написал огромное полотно Java кода с SQL запросами, которое нормально не работало, но затем обнаружил гораздо более удобный вариант. Есть библиотека с классом ScriptRunner, которая позволяет читать и выполнять SQL файл. С помощью неё создание бд с нужными таблицами умещаются всего лишь в пару строк. Пример указан ниже:

// Ваша функция которая служит для подключения к БД, которую вы должны инициализировать
Connection someconnection;

// Чтение файла из папки WEB-INF\classes\YOURDUMPFILE.sql
ScriptRunner runner = new ScriptRunner(someconnection, false, false);
ClassLoader loader = Thread.currentThread().getContextClassLoader();
InputStream stream = loader.getResourceAsStream("YOURDUMPFILE.sql");
InputStreamReader reader = new InputStreamReader(stream);
runner.runScript(reader);
reader.close();
conn.close();

После импорта БД, в конфигурационный файл расположенный в WEB-INF\classes\config.properties записываются данные подключения БД. Каждый раз при обращении к БД система читает этот файл. Конечно чтобы ограничить вход на ссылку /install после установки всем, в config.properties записывается значение «db.configured=yes». Если значение yes, то открыть ссылку /install невозможно.

Отображение знака «online» если принтер в сети




Функцию отображения принтера онлайн я пытался сделать с помощью JavaScript WebSocket, но это у меня не получилось. Поэтому решение было другим. После загурзки страницы посылается AJAX запрос на API PrintDesk CheckPrinterIsOnline и возвращается результат. На основе результата отображается или не отображается знак «Онлайн». Ниже представлен код Java, который это делает:

InetAddress inet = InetAddress.getByName(printerip);
if(inet.isReachable(500))
   result = "online";
else
   result = "offline";

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

Заключение


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

Бонус


Скриншоты




















Ссылка на GitHub
Ссылка на релиз PrintDesk
Ссылка на предыдущую статью