My favorites | Sign in
nop
Project Home Downloads Wiki Issues Source
Search
for
ExtendingFirstApp  
В этой статье рассказывается, что можно сделать
Updated Sep 14, 2011 by konsolet...@gmail.com

для улучшения первого приложения.

Введение

В предыдущей статье мы написали простейшее приложение для 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


Sign in to add a comment
Powered by Google Project Hosting