Система установки принтеров для организации — PrintDesk — Обновленная версия
Здравствуй, уважаемый %habrauser%. Около 3 лет назад я написал статью о том Как я создал систему установки принтеров на работе. Не могу не согласится с комментариями и отзывами от прошлой статьи, гласящие о том, что для установки принтеров можно воспользоваться групповыми политиками, но в мире в enterprise, больших или малых, возможны и другие случаи разного характера когда не смотря на наличие домена и групповых политик не представляется удобным\возможным разворачивать принтеры через GPO-шки. Учитывая собранный опыт и отзывы пришло время показать о моей новой переписанной системе с гораздо большим и удобным функционалом с сохранением минимализма, и я надеюсь, что кому-то она пригодится. В статье я также расскажу о том, с какими трудностями я столкнулся при создании этой системы. Кому стало интересно добро пожаловать под кат. Осторожно! Будет много картинок!
Почему надо менять то, что и так работает
Идея о том, что систему надо переписать пришла когда в очередной раз я получил жалобу от HelpDesk о том, что система не очень удобная и много возни с добавлением нового принтера. Каждый раз после установки принтера на сервер, необходимо было создавать два файла, где первый файл это VBS скрипт установки с записанным в него адресом нового принтера (Например: \\server01\printer1) и BAT файл который запускает этот VBS скрипт. Этот самый BAT файл загружался в систему установки принтеров, откуда пользователи находили нужный принтер и скачивали его. Для пользователей все просто: скачал → запустил → принтер поставился. Но настройка для технического отдела была нудной и долгой и эту проблему необходимо было решить.
Постановка требований
Ниже приведен список требований, которые были выполнены по мере разработки:
- В первую очередь должна открываться Home страница с выбором филиала
- Должна быть возможность поиска филиала по имени на Home странице
- После выбора филиала должна открываться страница со списком принтеров соответствующего филиала
- На странице выбора принтеров соответствующего филиала должна быть возможность поиска принтера по имени
- На странице выбранного принтера должна показываться следующая информация
- Имя принтера
- Изображение принтера
- Отображение знака Online если принтер в сети
- Тип принтера
- Описание принтера
- IP адрес
- Местоположение принтера
- Производитель принтера
- Иконка производтеля принтера (опционально)
- Количество просмотров принтера (опционально)
- Возможность отправлять ссылку на принтер по почте по клику по ссылке mailto типа
- Кнопка «Install» для загрузки установочного скрипта, который генерируется на лету
- Возможность глобального поиска как и принтеров так и филиалов
- Страница с мануалом текст, которого можно будет писать в админке в WYSIWYG редакторе
- Админ панель для управления всеми возможностями системы
- Должна быть минимальная API для получения какой-либо информации
- Возможность изменения данных для подключения к БД, если БД поднят на другом сервере без редактирования исходного кода приложения и пересобирания его в WAR
- Должна быть возможность изменения скрипта установки принтера на любую другую
- Для облегчения задачи первоначальной установки системы администратору, установка БД должна выполняться через страницу /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
Ссылка на предыдущую статью