|
ExtendingFirstApp
В этой статье рассказывается, что можно сделать
для улучшения первого приложения. ВведениеВ предыдущей статье мы написали простейшее приложение для nop. Чтобы побыстрее получить ощутимый результат, мы пренебрегли многими моментами. Попытаемся восстановить справедливость и создать нечто, более близкое к полноценному веб-приложению. В процессе мы познакомимся с различными возможностями фреймворка. База данныхПриложение в том виде, в котором мы его написали, обладает существенным недостатком - после перезапуска все заметки теряются. Обычно принято хранить данные приложения не в памяти, а в базах данных. nop содержит свою удобную надстройку над JDBC, упрощающую решение типовых задач, связанных с БД: создание таблиц в пустой БД и выполнение запросов к БД для выборки и модификации данных. МиграцииМиграции предназначены для большего, чем просто создание таблиц в пустой БД. Миграции позволяют автоматически менять схему БД под нужды последней версии приложения. В нашем случае требуется из пустой БД сделать БД, содержащую таблицу с заметками. Покажем, как эту задачу решает механизм миграций. Созайте пакет org.nop.examples.notes.data. В нём создайте класс Migration0: package org.nop.examples.notes.data;
import org.nop.migration.AbstractMigration;
import org.nop.migration.ChangeSetBuilder;
import org.nop.migration.Nonreversible;
@Nonreversible
public class Migration0 extends AbstractMigration {
@Override
protected void apply(ChangeSetBuilder cb) {
cb.createSequence("Notes_sequence", 1);
cb.createTable("Notes")
.with().column("id").integer().primaryKey()
.with().column("title").varchar(100)
.with().column("content").varchar(1000)
.with().column("creationDate").timestamp().indexed();
}
}При старте nop автоматически обнаружит эту миграцию и выполнит метод apply. Аннотация Nonreversible указывает, что откатить миграцию невозможно. Код метода apply создаёт последовательсноть Notes_sequence, из которой будут браться идентификаторы новых заметок, и таблицу Notes, где будут храниться сами заметки. Если теперь скомпилировать модуль и запустить nop, то среди текста, который он выплёвывает в консоль, можно увидеть два SQL-запроса. Впрочем, результаты применения миграции можно увидеть и по-другому. По умолчанию nop использует HSQLDB. Можно сконфигурировать фреймворк так, чтобы он подключался к другой СУБД, например, PostgreSQL. Для этого в каталоге, из которого стартует nop, находим директорию config и в ней файл org.nop.core.ConnectionConfig.xml. Вносим в него примерно следующий текст (подставьте подходящие имя БД, адрес сервера, логин и пароль): <?xml version="1.0" encoding="UTF-8" standalone="no"?> <config class="org.nop.core.ConnectionConfig"> <driver>org.postgresql.Driver</driver> <url>jdbc:postgresql://localhost/nop-notes</url> <user>postgres</user> <password>123</password> <migrationDriver>org.nop.migration.drivers.PostgreSQLDatabase</migrationDriver> <sqlDriver>org.nop.sql.drivers.PostgreSQLDriver</sqlDriver> </config> Теперь если запустить nop а затем посмотреть в БД, то можно увидеть, что действительно были созданы таблица и последовательность. Работа с БД из приложенияТаблицы в БД - это сущности, чужеродные Java. Java работает с объектами. Прежде всего требуется описать объект, представляющий таблицу Notes. Создайте класс NoteSource: package org.nop.examples.notes.data;
import org.nop.sql.ExprBuilder;
import org.nop.sql.QuerySource;
import org.nop.sql.Table;
@Table(name = "Notes")
public interface NoteSource extends QuerySource {
static final String ID_SEQUENCE = "Notes_sequence";
ExprBuilder id();
ExprBuilder title();
ExprBuilder content();
ExprBuilder creationDate();
}Теперь можно делать запросы к БД из класса, реализующего логику. Напомним, что вся логика приложения реализована в классе NoteRepositoryImpl. Перепишем этот класс так, чтобы он сохранял данные не в памяти, а в БД. package org.nop.examples.notes;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.nop.core.ManagedBean;
import org.nop.examples.notes.api.Note;
import org.nop.examples.notes.api.NoteRepository;
import org.nop.examples.notes.data.NoteSource;
import org.nop.sql.DataManager;
import org.nop.sql.DataResult;
import org.nop.sql.QueryBuilder;
import org.nop.util.Injected;
@ManagedBean(iface = NoteRepository.class)
public class NoteRepositoryImpl implements NoteRepository {
private DataManager dataManager;
private QueryBuilder qb;
@Injected
public NoteRepositoryImpl(DataManager dataManager) {
this.dataManager = dataManager;
this.qb = dataManager.getQueryBuilder();
}
@Override
public List<Note> getLastNotes(int limit) {
NoteSource note = qb.get(NoteSource.class);
DataResult result = dataManager.exec(qb.with(note)
.sortDesc(note.creationDate())
.take(limit)
.fetch(note.id(), note.title(), note.creationDate()));
List<Note> dtos = new ArrayList<Note>();
while (result.next()) {
Note dto = new Note();
dto.setId(result.getInt(1));
dto.setTitle(result.getString(2));
dto.setCreationDate(result.getDate(3));
dtos.add(dto);
}
return dtos;
}
@Override
public Note getNote(int noteId) {
NoteSource note = qb.get(NoteSource.class);
DataResult result = dataManager.exec(qb.with(note)
.filter(note.id().eq(noteId))
.fetch(note.id(), note.title(), note.creationDate()));
if (!result.next()) {
return null;
}
Note dto = new Note();
dto.setId(result.getInt(1));
dto.setTitle(result.getString(2));
dto.setCreationDate(result.getDate(3));
return dto;
}
@Override
public String getNoteContent(int noteId) throws IllegalArgumentException {
NoteSource note = qb.get(NoteSource.class);
DataResult result = dataManager.exec(qb.with(note)
.filter(note.id().eq(noteId))
.fetch(note.content()));
if (!result.next()) {
throw new IllegalArgumentException("Note #" + noteId + " not found");
}
return result.getString(1);
}
@Override
public int createNote(String title, String content) {
NoteSource note = qb.get(NoteSource.class);
int id = dataManager.nextInt(NoteSource.ID_SEQUENCE);
dataManager.exec(qb.insertInto(note)
.field(note.id(), id)
.field(note.title(), title)
.field(note.content(), content)
.field(note.creationDate(), new Date()));
return id;
}
}В целом, работа с БД в nop очень похожа на JDBC. Однако, nop сам берёт на себя управление соединениями и транзакциями, достаточно просто внедрить в свой класс DataManager. От JDBC подход отличается тем, что запросы оформляются не в виде строки с SQL, а через специальный небольшой язык, основанный на fluent-интерфейсе. Если теперь скомпилировать модуль и запустить nop, то работа с заметками будет идти на первый взгляд так же, как и раньше. Однако, после перезапуска nop созданные заметки останутся. Валидация формВ нашем приложении форма всего одна - форма создания заметки. Если ввести некорректные данные и нажать на кнопку "Create note", то приложение поведёт себя непредсказуемым образом (скорее всего - выдаст статус 500) вместо того, чтобы пояснить пользователю, что ему необходимо исправить. Избавимся от этого недостатка, добавив логику валидации к классу AddNoteForm. Для начала импортируйте ещё два класса: import org.nop.forms.AbstractForm; import org.nop.forms.CustomValidator; после чего строчку public class AddNoteForm {допишите до public class AddNoteForm extends AbstractForm {Теперь наша форма наследует свойство validation от класса AbstractForm и можно метод для валидации: @CustomValidator
public boolean validate() {
if (title == null || title.isEmpty()) {
validation.add("Please, enter title");
} else if (title.length() > 100)
validation.add("Title is too long");
if (content == null || content.isEmpty()) {
validation.add("Please, enter content");
} else if (content.length() > 1000) {
validation.add("Content is too long");
}
return validation.isEmpty();
}Наконец, необходимо показать пользователю сообщения от валидатора. Для этого потребуется внести изменения в шаблон формы. В файле AddNoteView.xml допишите открывающий тег первого элемента: <t:template xmlns:t="http://nop.org/schemas/templating/core"
xmlns:f="http://nop.org/schemas/templating/forms">Эта конструкция даёт указание использовать библиотеку тегов и привязывает теги из этой библиотеки к префиксу f. В том же файле сразу после открывающего тега <form> добавьте строчку: <f:validation messages="form.validation"/> Теперь при попытке добавить заметку, не заполнив форму, вы получите два сообщения об ошибке. Удалённые вызовыРаботать с приложением может не только пользователь, но и другие приложения. Для пользователя предназначен веб-интерфейс. Приложения могут вызывать методы логики через XML-RPC. В nop это делается совсем просто. Для начала откройте интерфейс NoteRepository и каждый метод пометьте аннотацией org.nop.rpc.WebMethod. Затем в интерфейс NoteRoute внесите следующие строки: @RoutePattern("service")
@WebService(NoteRepository.class)
String service();Теперь интерфейс NoteRepository можно вызывать через XML-RPC по адресу http://localhost:8080/notes/service. Если позволяют навыки, можете самостоятельно написать клиент. Вот пример простейшего клиента, использующего возможности nop для удалённого вызова XML-RPC: package org.nop.examples.notes;
import org.nop.examples.notes.api.Note;
import org.nop.examples.notes.api.NoteRepository;
import org.nop.rpc.RpcClient;
import org.nop.rpc.xmlrpc.XmlRpcClientFactory;
class ConsoleApp {
public static void main(String[] args) {
String base = "http://localhost:8080";
if (args.length == 1) {
base = args[0];
}
XmlRpcClientFactory clientFactory = new XmlRpcClientFactory();
clientFactory.addApi(NoteRepository.class, "/notes/service");
RpcClient client = clientFactory.create(base);
NoteRepository repos = client.get(NoteRepository.class);
for (Note note : repos.getLastNotes(100)) {
System.out.println(note.getCreationDate() + ": " + note.getTitle());
}
}
}РесурсыВеб-страницы принято украшать: добавлять стили с помощью CSS, вставлять пиктограмки для оформления кнопок и ссылок. Покажем, как можно вставить картинку на страницу с помощью nop. Создайте пакет org.nop.examples.notes.resources. В него скопируйте какую-нибудь небольшую PNG-картинку и назовите её plus.png. В интерфейс NoteRoute внесите следующие строки: @RoutePattern(value = "res/{p1}", variableLength = true)
@StaticFiles("resources")
String resource(String name);В шаблоне NoteListView.xml строчку: <td colspan="2"><a href="${route.add()}">Create new</a></td>замените на <td colspan="2">
<a href="${route.add()}"><img src="${route.resource('plus.png')}"/>Create new</a>
</td>Теперь перед ссылкой "Create new" будет стоять картинка. Подобным образом можно добавлять и другие файлы. Например, можно по аналогии добавить CSS-файл. РазметкаСоздавать заметки, состоящие из простого текста - это скучно. Часто требуется разбить заметку на абзацы, выделить важные мысли и т.д. nop поддерживает свой язык вики-разметки. Добавим в наше прложение возможность использовать этот язык. В шаблоне NoteView поменяйте тип свойства Content наTemplate. Для этого откройте интерфейс NoteView и замените тип значения, передаваемого методу setContent, на Template. Должно получиться так: NoteView setContent(Template content); В файле NoteView.xml строчку <p>${content}</p>замените на <t:include eval="content"/> Наконец, в контроллер необходимо добавить код, который бы парсил текст и создавал бы из него шаблон. Для этого у контроллера метод view следует переписать так: public Content view(int noteId) {
Note note = repository.getNote(noteId);
if (note == null) {
return null;
}
String content = repository.getNoteContent(noteId);
MarkupParser parser = new MarkupParser();
MarkupRenderer renderer = new MarkupRenderer(parser.parse(content));
return html(createView(NoteView.class)
.setTitle(note.getTitle())
.setCreationDate(note.getCreationDate())
.setContent(renderer));
}Теперь можно поэкспериментировать, добавляя заметки: абзацы разделяются одной пустой строкой, курсив делается с помощью _подчёркиваний_, полужирный шрифт - с помощью *звёздочек*. У языка разметки множество других возможностей, но здесь мы их рассматривать не будем. ЗаключениеВ этой статье мы рассмотрели разные интересные возможности nop, улучшили приложение, написанное в предыдущей статье. Некоторые темы остались нерассмотренными, либо по причине их излишней "продвинутости", либо ввиду того, что для их иллюстрации требуются значительные изменения в существующем коде. Для дальнейшено изучения nop читайте руководства, разбирайте "живой" код (например, этого проекта). Получившийся код можете скачать по ссылке http://nop.googlecode.com/files/nop.notes.ext.zip |