Создаем шаблоны электронных писем с помощью XML
На сегодняшний день возможность посылать электронные сообщения является неотъемлемой частью любого web-приложения. В основном, это очень специфические виды сообщений - например, сообщения, которые шлются для напоминания пользовательского пароля, приветственные сообщения, сообщения, подтверждающие заказы, и пр. Хотя содержание электронных сообщений и меняется от приложения к приложению, но процесс их отправки редко различается. Вы просто создаете письмо, отсылаете его на почтовый сервер, и позже получатель его оттуда забирает.
Когда вы программируете на Java, обычно для выполнения всей черной работы, связанной с соединением с почтовым сервером и отправкой письма, используется JavaMail API ( http://java.sun.com/products/javamail/ ). К сожалению, этим API крайне неудобно пользоваться (в основном, из-за гибкости электронной почты как таковой), поэтому, если вы собираетесь использовать его достаточно часто, будет куда удобнее и разумнее написать для него специальную оболочку. В зависимости от того, как его использовать, оболочка может быть нацелена на работу с каким-нибудь конкретным видом писем (для отправки писем, подтверждающих пароль, например), или же она будет работать, как обычно: принимая тему сообщения, список получателей и тело письма в качестве аргументов.
Однажды создав подобную оболочку, вам необходимо иметь также и систему для создания самих сообщений. Возьмем, например, сообщения, напоминающие пользователю его пароль, если он его случайно забыл. Почти все электронные письма имеют поле для темы сообщения, список получателей и тело письма. Когда мы посылаем письмо, напоминающее пароль, электронный адрес получателя и сам пароль обычно извлекаются из какого-нибудь хранилища, в котором находится информация о пользовательских аккаунтах. Поле темы и само тело сообщения должны сливаться с данными из базы данных и где-то сохраняться. Одна из основных проблем при проектировании таких приложений - решить, где же все-таки лучше хранить такого рода данные. Во многих случаях эти строки сохраняются в файлах свойств, которые содержатся отдельно от вашего исходного кода и предоставляют удобную возможность локализации, если необходимо. Такой подход используется в очень многих web-приложениях для хранения шаблонов электронных писем для отправки, однако подход этот не совсем правильный.
Вот список основных причин, по которым использование файлов свойств не является оптимальным способом хранения строк, составляющих шаблон электронных писем:
Файлы свойств отображаются в очень простую структуру данных - пары ключей и их значений. Это совсем не подходит в случае, если нужно привязать множество значений одному и тому же ключу. Например, электронное сообщение может иметь четырех человек в поле To: и троих в поле Cc:. Это невозможно реализовать просто с помощью файлов свойств.
Файлы свойств имеют очень строгое форматирование своего содержимого. Каждый ключ и его значение должны находиться на одной и той же строке. Таким образом, строки могут доставить много ненужных проблем, связанных с редактированием этого файла. Например, будет очень проблематично поместить все тело электронного сообщения в одном свойстве (пара ключ=значение). И если вам нужно будет включить переносы на другую строку в пределах значения этого свойства, вы будете вынуждены заменять их специальным символом \n.
Существует альтернативный подход к созданию шаблонов электронных сообщений. Он заключается в использовании XML, и именно этот подход мы и будем рассматривать в данной статье. XML предоставляет возможность конструирования шаблонов электронных сообщений с очень гибкой структурой, и, помимо этого, он не имеет таких ограничений на форматирование, какие имеют место в файлах свойств. Поэтому с его помощью мы можем очень просто сохранять достаточно большие строки. Единственное достоинство файлов свойств в том, что с ними чуть проще работать, чем с XML-документами. В случае с файлами свойств гораздо проще загрузить файл и после этого получить доступ к свойствам, уже не обращаясь к данному файлу. С другой стороны, в случае XML-файла уходит куда больше времени на загрузку и обработку. Для этого также необходимо использовать одну из многочисленных библиотек для работы с XML-файлами, некоторые из которых поставляются вместе с Java.
Эта статья и сопровождающие ее примеры призваны попытаться как можно больше упростить для вас этот процесс, предоставляя общий фреймуорк для создания шаблонов электронных писем с помощью XML-документов и их отправки. В этом фреймуорке мы использовали пакет Common Digester из проекта Jakarta для обработки XML и JavaMail API для отправки фактических сообщений.
Шаблоны электронных сообщений
Давайте посмотрим на формат самих шаблонов электронных сообщений. Шаблоны представляют собой обычные XML-файлы, которые содержат корневой элемент и дочерние элементы этого элемента. Корневой элемент - это
<email></p> <p><from>[email protected]</from></p> <p><to>[email protected]</to></p> <p><cc>[email protected]</cc></p> <p><bcc>[email protected]</bcc></p> <p><subject>Это тема письма</subject></p> <p><body>Это тело нашего электронного сообщения.</body></p> <p></email>
Настраиваемые шаблоны
Файлы свойств предоставляют одну очень полезную возможность. Она заключается в том, что вы можете использовать класс MessageFormat для замены идентификаторов-заменителей в файле свойств на реальные значения во время работы приложения. Например, если вы храните сообщения об ошибках в файле свойств, и одно из этих сообщений - "файл не найден", можно внести в этот файл следующее свойство:
file.not.found.error=Ошибка, невозможно найти файл {0}.
После этого используем класс MessageFormat следующим образом:
ResourceBundle bundle = ResourceBun dle.getBundle("MyProperties", current Locale);</p> <p>Object[] arguments = { "some_file. txt" };</p> <p>String newString = MessageFormat.format(</p> <p>bundle.getString("file.not.found. error"), arguments);
В результате переменная newString будет содержать строку "Ошибка, невозможно найти файл some_file.txt.". Подобную же функциональность мы привнесли и в нашу систему. Поскольку класс MessageFormat может работать с любыми строками, вы можете очень просто вставлять такие же идентификаторы-заменители в элементы subject и body XML-шаблона электронного сообщения.
Иногда может возникнуть необходимость вставлять персональную информацию в ваши шаблоны перед посылкой писем. Например, вам может понадобиться включить имя получателя в тело письма или, быть может, даже детальную информацию о произведенном им заказе.
Наша система легко справляется с такой задачей путем обработки содержимого элементов subject и body с помощью MessageFormat. Уловка в том, что этот класс принимает только один массив аргументов, который будет использоваться для обработки как поля subject, так и тела сообщения. Так, в содержимом элемента subject могут иметь место идентификаторы {0}, {2} и {3}, а в содержимом элемента body - {0}, {1} и {4}. Мы выбрали такой подход потому, что очень часто одни и те же аргументы используются в обоих полях, body и subject, и это также упрощает список параметров, передаваемый в EmailSender.
Обработка шаблона
Теперь, когда мы уже создали шаблон, можно приступать к его обработке. Как вы уже знаете, существует множество библиотек, предназначенных для работы с XML-документами. Одна из них - Commons Digester, часть проекта Jakarta Commons - создавалась изначально как часть проекта Struts, для того чтобы предоставить быстрый и простой путь к разбору конфигурационного файла Struts. Этот инструмент дает простой подход к отображению элементов XML-файла в структуру данных с использованием синтаксиса, схожего с XPath (http://www.w3.org/TR/xpath). Основное его достоинство в том, что он позволяет выдирать нужные элементы из XML-документов без необходимости в их разборе узел за узлом с помощью SAX или создании древовидной структуры данных, как это делает DOM.
Ниже приведен метод, который считывает данные из XML-файла и копирует их в объект EmailTemplate:
public static EmailTemplate getEmail Template(InputStream aStream)</p> <p>{</p> <p>Digester digester = new Digester();</p> <p>digester.setValidating(false);</p> <p>digester.addObjectCreate("email", EmailTemplate.class);</p> <p>digester.addBeanPropertySetter ("email/subject", "subject");</p> <p>digester.addBeanPropertySetter ("email/body", "body");</p> <p>digester.addBeanPropertySetter ("email/from", "from");</p> <p>digester.addCallMethod("email/to", "addTo", 0);</p> <p>digester.addCallMethod("email/cc", "addCc", 0);</p> <p>digester.addCallMethod("email/bcc", "addBcc", 0);</p> <p>try</p> <p>{</p> <p>return (EmailTemplate)digester. parse(aStream);</p> <p>}</p> <p>catch (IOException e)</p> <p>{</p> <p>logger.error("Ошибка: ", e);</p> <p>return null;</p> <p>}</p> <p>catch (SAXException e)</p> <p>{</p> <p>logger.error("Ошибка: ", e);</p> <p>return null;</p> <p>}</p> <p>}
Теперь давайте рассмотрим каждую из строк этого примера. Работа с Commons Digester заключается в создании некоторого набора правил, которые впоследствии будут применены к файлу, который будет обрабатываться. Прежде чем приступить к заданию этих правил, мы предварительно установили флаг проверки на валидность XML-документа в false, поскольку не создавали и не привязывали к нашему XML-файлу никаких DTD-файлов для проверки на валидность структуры нашего шаблона. Чтобы начать обработку файла, создаем объект класса Digester и после этого вызываем методы для установки правил отображения данных. Сперва мы вызываем метод addObjectCreate(), который устанавливает правило создания объекта EmailTemplate, как только нам встретится элемент email. Элемент email является корневым элементом нашего шаблона, поэтому каждый файл шаблона будет отображаться в один экземпляр класса EmailTemplate.
Для элементов, которые появляются в нашем шаблоне всего единожды, мы использовали метод addBeanPropertySetter(). Он принимает два аргумента: путь к элементу, который будет обрабатываться, и set-метод, который будет отображать содержимое этого элемента в объект EmailTemplate. В первом вызове мы обозначили, что содержимое элементов, совпадающих с заданным шаблоном ("email/subject"), должно быть передано set-методу поля subject объекта класса EmailTemplate. Заданный шаблон определяет порядок вложенностей элементов, разделяемых символами / и по которому следует искать элемент. В нашем случае заданный шаблон соответствует элементу subject, который является дочерним по отношению к элементам email. При задании подобных шаблонов поиска также можно использовать символы замены (wildcards), которые могут обеспечить более гибкие возможности поиска. Для ознакомления с подробным описанием использования и создания этих шаблонов посмотрите JavaDoc ( http://jakarta.apache.org/commons/digester/api/index.html ) для Commons Digester.
Что касается элементов, которые могут неоднократно встречаться в шаблоне электронного сообщения, то для этих свойств вызов set-методов не подходит. Вместо этого мы использовали метод addCallMethod(), который принимает содержимое элемента и вызывает специальный метод. Мы использовали версию этого метода, которая принимает три аргумента. Это шаблон на соответствие, метод, который следует вызывать, и количество аргументов, которое будет передано этому методу. Во всех трех случаях мы указали 0 в качестве третьего аргумента, поскольку при этом методу будет передано только лишь содержимое найденного элемента. В классе EmailTemplate мы написали три метода: addTo(), addCc() и addBcc(), которые добавляют список получателей сообщения из файла шаблона в коллекции класса EmailTemplate.
Как только установлены правила для всех шести типов дочерних элементов XML-шаблона электронного сообщения, мы можем приступать непосредственно к разбору нашего файла. Для этого используется InputStream, связанный с файлом XML-документа, который передается в качестве аргумента методу getEmailTemplate(). Метод parse() может принимать в качестве аргумента объект File, InputSource из SAX, InputStream, Reader или строку URI, которая определяет место расположения файла, который нужно обработать. Мы выбрали версию метода parse(), которая принимает объект InputStream в качестве аргумента.
Метод parse() может выбрасывать исключения IOException или SAXException. Если возникает какое-либо из этих исключений, мы его ловим, журналируем с помощью log4j и возвращаем null. Если не возникнет никаких исключений, то метод getEmailTemplate() возвратит новый экземпляр класса EmailTemplate, который будет сгенерирован с помощью класса Digester.
Остальная часть класса EmailTemplate
Самой значимой частью класса EmailTemplate, несомненно, является метод getEmailTemplate(). Все остальное - просто различные свойства и методы, предназначенные скорее для того, чтобы всего-навсего сделать работу с классом удобнее. Итак, этот класс имеет три свойства класса String: тема, тело письма, адрес отправителя, - а также другие свойства, которые хранятся в структурном классе ArrayList: списки адресатов, полей CC и BCC. Для каждого из этих свойств в классе EmailTemplate предусмотрены set- и get-методы: getToAddresses(), getCcAddresses() и getBccAddresses(). JavaMail API ожидает, что вы будете передавать ему адреса в старом стиле в виде массива объектов класса InternetAddress. Эти методы заботятся также и о том, чтобы конвертировать объекты ArrayList в массив объектов, которые требует JavaMail API.
EmailSender
Теперь, когда мы успешно разобрали файл шаблона и получили готовый объект класса EmailTemplate, следующим шагом приступим к отправке электронного сообщения. Класс EmailSender включает один статический перегруженный метод - sendEmail(). Вот его сигнатура:
public static void sendEmail(</p> <p>String aTo,</p> <p>EmailTemplate aTemplate,</p> <p>String[] aArgs)
Аргументы этого метода наверняка не требуют долгих разъяснений.
Первый из них - поле To: (Кому) электронного сообщения. В принципе, вы можете задать это поле непосредственно в самом шаблоне сообщения, однако очень часто получатель сообщения определяется на этапе работы приложения. Например, если вы шлете сообщение - напоминание о пароле, то оно должно быть адресовано именно пользователю, сделавшему соответствующий запрос. Адрес получателя выгодно жестко прописывать в шаблоне электронного сообщения, например, тогда, когда системе необходимо послать сообщение для тестирования или для каких-нибудь системных нужд. Например, предположим, системе нужно сгенерировать и послать электронное сообщение, которое переключало бы последовательность выполняемых действий каждый раз, когда послан соответствующий запрос. В этом случае, конечно, адрес получателя лучше жестко прописать в шаблоне сообщения.
Второй аргумент - это сам объект EmailTemplate.
А третий - список аргументов, которые будут переданы классу MessageFormat, когда он будет обрабатывать поле темы и тело сообщения. Для этого существует специальная часть кода, создающая массив информации, которая используется для персонализации шаблона электронного сообщения. Там имеется также несколько других объявленных методов, которые служат для того, чтобы упростить вызов этого метода. Таким образом, вы можете вызывать его без указания получателей или вообще без каких-либо аргументов.
Тело метода sendEmail(), в основном, состоит из вызовов методов JavaMail API для настройки необходимых параметров и отправки сообщения. Сначала мы проверяем, не равен ли объект EmailTemplate null. Если равен, то ничего не делаем. В противном случае первым шагом установки параметров создаем объект свойств Properties (улучшенный Hashtable) с установками SMTP-сервера.
После этого создаем объект Session из пакета JavaMail API и передаем его конструктору созданный ранее объект Properties с настройками SMTP-сервера. Объект Session нужен для того, чтобы создать объект MimeMessage, что мы и делаем. Теперь устанавливаем поле адреса From: равным адресу, определенному в объекте EmailTemplate, который мы передаем этому методу в качестве аргумента. Следующим шагом устанавливаем поле To:.
Поскольку все адреса CC: и BCC определены в пределах шаблона, их обработка не будет доставлять никаких проблем. Нужно просто используя подходящие методы класса EmailTemplate добавить дополнительных получателей сообщения в само сообщение.
Как было упомянуто ранее, мы используем MessageFormat для того, чтобы применить все аргументы, переданные методу к теме и телу сообщения.
После этого нужно просто скопировать полученные тему и тело сообщения в объект сообщения. Теперь все, что осталось сделать, - вызвать метод Transport.send() и передать ему объект класса MimeMessage.
Использование фреймуорка
Теперь узнаем, как нужно пользоваться этой системой. Мы рассмотрим вариант работы сервлета, хотя это должно работать и в любой другой нормальной программе. Нижеприведенный код наглядно показывает работу с нашей системой:
// Грабим шаблон электронного сообщения</p> <p>InputStream template =</p> <p>getServlet()</p> <p>.getServletConfig()</p> <p>.getServletContext()</p> <p>.getResourceAsStream(</p> <p>"/WEB-INF/email/registration Notification.xml");</p> <p>EmailTemplate notification = Email Template.getEmailTemplate(template);</p> <p>//Создаем секцию электронного сообщения, содержащую фактические данные о пользователе</p> <p>String[] args = { "Rafe" };</p> <p>EmailSender.sendEmail ("rafe@rafe. us", notification, args);
Сначала используя системные функции получаем InputStream, связанный с файлом шаблона, представленный в виде XML-документа. Поскольку мы используем сервлет, то файл получаем из ServletContext. Существует, конечно, и множество других способов получить Input-Stream, связанный с этим файлом, но в случае со средой сервлета этот вариант подходит как никакой другой. После этого все, что нам нужно сделать, - передать полученный объект класса InputStream методу EmailTemplate. getEmailTemplate(), который мы описывали ранее. Потом мы просто определяем массив с аргументами для настройки электронного сообщения и вызываем метод EmailSender. sendEmail().
Вместо заключения
Существует ряд улучшений, которые можно привнести в данную систему. Два самых очевидных - добавление возможности отправки как HTML, так и обычных электронных сообщений, а также добавление возможности отправки вложенных файлов вместе с сообщением.
Чтобы создать такого рода сообщения, достаточно воспользоваться сообщениями типа javax.mail.MimeMultipart.
Web Дизайнер (designer)