|
FirstApp
Building your first web application with Nop.
Application descriptionLet's see how we can create a 'notes' application. The application allows user to create notes and view list of notes. The application is highly simplifiled - there is no any DB interaction, form validation etc. We just see how to create application without using some of Nop advantages. We will use Eclipse as an IDE. So you are supposed to have Eclipse installed and configured. Installing NopDownload Nop distribution from here, extract archive. The nop.all.jar file is executable, so open console, change directory to that, where Nop have been extracted, and type: java -jar nop.all.jar As a result you'll see a lot of output logs. But for not Nop doesn't do anything useful. So just break an appication using CTRL+C. Creating projectStart Eclipse and select from the menu File -> New -> Java Project. Enter a project name (nop.notes) and press the Finish button. Now you have an empty project. Choose the Project -> Properties menu item, select Java Build Path from the tree in the left side. In the right side choose the Libraries tab and press the Add External JARs... button. Find file 'nop.all.jar' and press ok the 'OK' button. Now the Nop classes are in your project's class path. Show somethingWe are not prepaired to make a fully functional application. But we want to have some result for now. So let's create a little useless page. In the 'Project' window expand project and find the 'src' folder. Click on it with the right mouse button and choose the 'New -> Package' item. Enter the package name (org.nop.examples.notes) and press the Finish button. Similarly create the NoteView interface in the same package, with the following content: package org.nop.examples.notes;
import org.nop.core.Route;
import org.nop.core.RoutePattern;
@Route(prefix = "notes")
public interface NoteRoute {
@RoutePattern("hello/{p1}")
String hello(String userName);
}We have just created a route. The route is used to map URLs to methods which generate pages' content. Here we bound the 'notes/hello/{user name}' pattern to the method named 'hello'. Further you will see where this hello method is. Логика генерации страницы подразумевает вставку данных (которыми у нас является имя пользователя) в шаблон и отображение этого шаблона. Займёмся созданием этого шаблона. Создайте файл HelloView.xml в пакете org.nop.examples.notes: New -> Other, найдите папку XML и выбирите XML File, далее введите имя файла и нажмите Finish. Введите следующий текст: <?xml version="1.0" encoding="UTF-8"?>
<t:template xmlns:t="http://nop.org/schemas/templating/core">
<t:head>
<t:parameter name="userName"/>
</t:head>
<t:body>
<p>Hello, ${userName}!</p>
</t:body>
</t:template>XML чужероден по отношению к Java, ведь Java - это объектно-ориентированный язык и работает в терминах объектов, а не элементов и атрибутов. Сделаем так, чтобы шаблон стал объектом Java. Создайте интерфейс HelloView в том же пакете и со следующим содержимым: package org.nop.examples.notes;
import org.nop.templating.Template;
public interface HelloView extends Template {
void setUserName(String userName);
}Фреймворк сам свяжет шаблон и интерфейс по имени и позволит через интерфейс задавать параметры шаблона. Это будет проиллюстрировано в контроллере. Итак, создайте класс NoteController со следующим содержимым: package org.nop.examples.notes;
import org.nop.core.AbstractController;
import org.nop.core.Content;
import org.nop.core.RouteBinding;
@RouteBinding(NoteRoute.class)
public class NoteController extends AbstractController {
public Content hello(String userName) {
HelloView view = createView(HelloView.class);
view.setUserName(userName);
return html(view, "Hello");
}
}Здесь мы описали действие, которое необходимо предпринять при запросе страницы /notes/hello/{имя_пользователя}. Дело в том, класс NoteController с помощью аннотации RouteBinding связан с маршрутом NoteRoute. У NoteRoute тоже есть метод hello, для которого установлена привязка к адресу страницы. Таким образом действие в контроллере опосредовано (через маршрут) связано с адресом. Действие такое: создаём шаблон, заполняем его данными (т.е. присваиваем имя пользователя, полученное в URL) и выводим пользователю. Последнее, что осталось сделать - это описать действия, выполняемые при загрузке модуля. Создайте класс NoteModule со следующим содержимым: package org.nop.examples.notes;
import org.nop.core.AbstractModule;
public class NoteModule extends AbstractModule {
@Override
protected void load() {
app.loadPackage("org.nop.examples.notes");
}
}Это указывает nop, что надо просмотреть все классы пакета org.nop.examples.notes и вложенных пакетов, найти среди них "специальные" классы и загрузить их в систему. К специальным классам в нашем случае относятся все классы, описанные выше. В большинстве случаев такого кода должно хватать. Сборка и запуск приложенияПришло время собрать приложение и посмотреть, что оно уже умеет. Щёлкните по проекту правой кнопкой и выберите Export. В дереве выберите Java -> JAR file. Задав параметры экспорта, и выбирав имя файла, создайте JAR-файл и скопируйте его в поддиректорию modules в директории, куда установлен nop. Запустите nop. Теперь в браузере можно открыть страницу http://localhost:8080/notes/hello/username. Можно так же поиграться с последним компонентом адреса и понаблюдать за изменениями получившейся страницы. Бизнес-логикаТеперь пришло время заняться более серьёзными вещами. Потому не всегда можно будет сразу увидеть результаты нашей работы, придётся запастись терпением и неспешно писать код. Хорошей практикой считается отделение логики приложения от пользовательского интерфейса. Т.е. должен быть некоторый прикладной слой, который бы фактически и реализовывал все доступные пользователю действия. А поверх него пишется слой, который бы, обращаясь только к прикладному слою, рисовал бы пользовательский интерфейс. Для начала определим интерфейс для взаимодействия с прикладным слоем. Создайте пакет org.nop.examples.notes.api и в нём класс Note и интерфейс NoteRepository со следующим содержимым: package org.nop.examples.notes.api;
import java.util.Date;
public class Note {
private int id;
private String title;
private Date creationDate;
}package org.nop.examples.notes.api;
import java.util.List;
public interface NoteRepository {
List<Note> getLastNotes(int limit);
Note getNote(int noteId);
String getNoteContent(int noteId) throws IllegalArgumentException;
int createNote(String title, String content);
}В классе Note необходимо создать публичные методы для доступа к полям из вне. Можно это сделать вручную, но Eclipse автоматизирует эту рутинную работу. В классе Note установите курсор перед закрывающей фигурной скобкой и в меню выберите пункт Source -> Generate Getters and Setters... В появившемся окне выберите все три свойства и нажмите OK. Eclipse сгенерирует методы для доступа к свойствам. Напишем реализацию интерфейса NoteRepository. Реализация предельно упрощена, данные должны храниться в памяти, а не в БД. В принципе, тут нет почти ничего специфического для nop, так что вы можете сами написать класс org.nop.examples.notes.NoteRepositoryImpl. Единственное, что следует учесть, что вызовы методов могут осуществляться из нескольких потоков, так что следует позаботиться о синхронизации. После того, как класс написан, необходимо пометить его аннотацией: @ManagedBean(iface = NoteRepository.class, scope = BeanScope.APPLICATION) Это указывает фреймворку, что реализацией прикладного интерфейса NoteRepository является именно класс NoteRepositoryImpl, причём время жизни этого класса должно совпадать со временем жизни приложения, т.е. при запуске приложения будет создан экземпляр класса и все обращения к интерфейсу NoteRepository будут сводиться к обращению к этому единственному экземпляру. Если вы всё-таки не готовы самостоятельно реализовать интерфейс, предлагается его реализовать следующим образом: package org.nop.examples.notes;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.nop.core.BeanScope;
import org.nop.core.ManagedBean;
import org.nop.examples.notes.api.Note;
import org.nop.examples.notes.api.NoteRepository;
@ManagedBean(iface = NoteRepository.class, scope = BeanScope.APPLICATION)
public class NoteRepositoryImpl implements NoteRepository {
private static class NoteData {
public int id;
public String title;
public String content;
public Date creationDate;
}
private List<NoteData> noteStorage = new ArrayList<NoteData>();
private final Object monitor = new Object();
@Override
public List<Note> getLastNotes(int limit) {
synchronized (monitor) {
List<Note> notes = new ArrayList<Note>();
int lowerIndex = Math.max(0, noteStorage.size() - limit);
for (int i = noteStorage.size() - 1; i >= lowerIndex; --i) {
notes.add(noteFromData(noteStorage.get(i)));
}
return notes;
}
}
@Override
public Note getNote(int noteId) {
synchronized (monitor) {
int index = indexOfNote(noteId);
if (index == -1) {
return null;
}
return noteFromData(noteStorage.get(index));
}
}
@Override
public String getNoteContent(int noteId) throws IllegalArgumentException {
synchronized (monitor) {
int index = indexOfNote(noteId);
if (index == -1) {
throw new IllegalArgumentException("Note #" + noteId + " not found");
}
return noteStorage.get(index).content;
}
}
@Override
public int createNote(String title, String content) {
synchronized (monitor) {
NoteData data = new NoteData();
data.id = noteStorage.size();
data.title = title;
data.content = content;
data.creationDate = new Date();
noteStorage.add(data);
return data.id;
}
}
private static Note noteFromData(NoteData data) {
Note note = new Note();
note.setId(data.id);
note.setTitle(data.title);
note.setCreationDate(data.creationDate);
return note;
}
private int indexOfNote(int noteId) {
for (int i = 0; i < noteStorage.size(); ++i) {
if (noteStorage.get(i).id == noteId) {
return i;
}
}
return -1;
}
}Веб-интерфейсВ принципе, всё почти готово, чтобы с нашим приложением могли взаимодействовать другие программы. Однако, чтобы с приложением мог взаимодействовать человек, необходимо для него предоставить веб-интерфейс. Прежде чем двинуться дальше, позаботимся о создании тестовых заметок. В класс NoteRepositoryImpl добавьте конструктор: public NoteRepositoryImpl() {
NoteData data = new NoteData();
data.id = 0;
data.title = "Lorem ipsum";
data.content = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, " +
"sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
data.creationDate = new Date();
noteStorage.add(data);
}Теперь в маршруте пропишем адреса и названия добавляемых страниц. В интерфейс NoteRoute добавьте следующие методы: @RoutePattern("/")
String list();
@RoutePattern("add")
String add();
@RoutePattern("{p1:numeric}/view")
String view(int noteId);Чтобы не ломать приложение, добавьте заглушки в контроллер: public Content list() {
return null;
}
public Content view(int noteId) {
return null;
}
public Content add() {
return null;
}Общий шаблонСтраницы нашего приложения содержат постоянную и изменяемую части. Простейший способ сделать у них общую постоянную часть - это использовать технику copy-paste. Однако, такой подход обладает рядом недостатков, поэтому воспользуемся возможностями nop по разширения шаблонов. Для начала создадим шаблон, содержащий постоянную общую часть всех страниц, а затем в шаблонах страниц просто будет встраивать в него изменяемую часть этих страниц. Сваливать все классы в один пакет - это не очень хорошая практика, потому будем держать свои шаблоны в пакете org.nop.examples.notes.view. Создайте этот пакет а затем в нем создайте интерфейс MainView и файл MainView.xml: package org.nop.examples.notes.view;
import org.nop.templating.Template;
public interface MainView extends Template {
void setTitle(Template title);
void setContent(Template content);
}<?xml version="1.0" encoding="UTF-8"?>
<t:template xmlns:t="http://nop.org/schemas/templating/core">
<t:head>
<t:parameter name="title"/>
<t:parameter name="content"/>
</t:head>
<t:body>
<html lang="ru">
<t:attribute name="xmlns" value="http://www.w3.org/1999/xhtml"/>
<head>
<title>${title}</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
</head>
<body>
<t:include eval="content"/>
</body>
</html>
</t:body>
</t:template>Параметрами шаблона MainView являются другие шаблоны. Вставка параметра-шаблона может осуществляться традиционным образом (через $-нотацию) или с помощью директивы t:include. В первом случае из вставленного шаблона будет взято только текстовое содержимое (теги будут не экранированы, а вообще вырезаны). Во втором случае получим полноценную разметку, порожденную шаблоном. В следующих разделах мы увидим, как использовать этот шаблон. Просмотр заметкиПришло время создать что-то, что будет не просто кодом, а реально работало бы. И так, начнём как и в прошлый раз, с шаблона. В пакете org.nop.examples.notes.view создайте интерфейс NoteView и файл NoteView.xml: package org.nop.examples.notes.view;
import java.util.Date;
import org.nop.templating.Template;
public interface NoteView extends Template {
NoteView setTitle(String title);
NoteView setContent(String content);
NoteView setCreationDate(Date creationDate);
}<?xml version="1.0" encoding="UTF-8"?>
<t:template xmlns:t="http://nop.org/schemas/templating/core">
<t:head>
<t:parameter name="title"/>
<t:parameter name="content"/>
<t:parameter name="creationDate"/>
<t:service name="route" class="##.NoteRoute"/>
<t:service name="lang" class="org.nop.localization.Language"/>
</t:head>
<t:body>
<t:use class="#.MainView">
<t:template name="title">${title}</t:template>
<t:template name="content">
<h1>${title}</h1>
<p>${content}</p>
<div class="noteFooter">Created at: ${lang.dateTime(creationDate)}</div>
<div class="navigationArea">
<a href="${route.list()}">View last notes</a>
</div>
</t:template>
</t:use>
</t:body>
</t:template>Здесь мы как и в прошлый раз объявили интерфейс для программного взаимодействия с шаблоном. В самом шаблоне описали три параметра. Кроме того, в заголовке шаблона мы с помощью элементов t:service указали, что шаблону требуются экземпляры двух интерфейсов - NoteRoute и Language. Предполагается, что при создании шаблона nop автоматически найдёт (или создаст) экземпляры интерфейсов и передаст их шаблону. Запись ##.NoteRoute означает относительное именование пакета. Один символ # означает тот же пакет, в котором находится шаблон, два символа (##) - родительский пакет и т.д. Про интерфейс Language мы ещё поговорим. Сейчас он нам понадобился, чтобы красиво отформатировать дату. С помощью t:use мы включили другой шаблон. Если шаблон принимает параметры, то передать их можно с помощью t:parameter. Однако, для параметров-шаблонов существует нотация t:template, которую мы здесь использовали. В шаблоне мы видим, что встроенный язык позволяет включать вызовы методов. А так же наконец становится понятно ещё одно предназначение маршрута - с его помощью можно получить ссылку на страницу. Теперь модифицируем контроллер. Во-первых, контроллер пока ничего не знает о прикладном слое. А ведь ему надо как-то получать доступ к списку заметок. С помощью механизма внедрения зависимостей (dependency injection) это делается элементарно - допишите в контроллер следующий код: private NoteRepository repository;
@Injected
public NoteController(NoteRepository repository) {
this.repository = repository;
}Здесь мы объявили конструктор, помеченный аннотацией Injected. Это означает, что при создании класса nop будет вызывать именно этот конструктор, автоматически инициализируя объекты и паредавая их в качестве параметров (в нашем случае - объект NoteRepository). Теперь можно сделать нормальную реализацию метода view: Note note = repository.getNote(noteId);
if (note == null) {
return null;
}
String content = repository.getNoteContent(noteId);
return html(createView(NoteView.class)
.setTitle(note.getTitle())
.setCreationDate(note.getCreationDate())
.setContent(content), note.getTitle());Мы не случайно в интерфейсе шаблона указали, чтобы методы возвращали сам шаблон - это позволяет в таком вот компактном стиле заполнить шаблон данными. Пересоберите модуль и запустите приложение. По адресу http://localhost:8080/notes/0/view теперь будет доступна страница с тестовой заметкой. Список заметокПолученных знаний хватит для того, чтобы создать страницу для просмотра списка заметок. Создайте шаблон NoteListView: package org.nop.examples.notes.view;
import java.util.List;
import org.nop.examples.notes.api.Note;
import org.nop.templating.Template;
public interface NoteListView extends Template {
NoteListView setNotes(List<Note> notes);
}<?xml version="1.0" encoding="UTF-8"?>
<t:template xmlns:t="http://nop.org/schemas/templating/core">
<t:head>
<t:parameter name="notes"/>
<t:service name="route" class="##.NoteRoute"/>
<t:service name="lang" class="org.nop.localization.Language"/>
</t:head>
<t:body>
<t:use class="#.MainView">
<t:template name="title">Notes</t:template>
<t:template name="content">
<h1>Notes</h1>
<table>
<tr>
<th>Title</th>
<th>Creation date</th>
</tr>
<tr>
<td colspan="2"><a href="${route.add()}">Create new</a></td>
</tr>
<t:foreach var="note" in="notes">
<tr>
<td><a href="${route.view(note.id)}">${note.title}</a></td>
<td>${lang.dateTime(note.creationDate)}</td>
</tr>
</t:foreach>
</table>
</t:template>
</t:use>
</t:body>
</t:template>Единственное, на что здесь стоит обратить внимание - каким образом мы в шаблоне обрабатываем списки, пораждая разметку для каждого элемента списка. Делается это с помощью директивы t:foreach. Теперь в контроллере напишите следующий код для метода list: List<Note> notes = repository.getLastNotes(200);
return html(createView(NoteListView.class).setNotes(notes));Страница со списком заметок готова. Можно пересобрать модуль и запустить nop. Новая страница станет доступна по адресу http://localhost:8080/notes. Добавление заметокДобавление заметок несколько сложнее чем то, что было ранее. Дело в том, что теперь нужно не просто генерировать страницу по хранящимся данным, а нужно ещё взаимодействовать с пользователем, т.е. принимать данные, которые он вводит. Разберёмся, как это происходит. Перенос данных от пользователя к серверу и наоборот осуществляется с помощью специального объекта. Для каждого вида формы необходимо создать свой класс, помеченный аннотацией org.nop.forms.Form. Если необходимо в методе контроллера получить данные, отправленные пользователем в теле запроса, следует в методе контроллера описать первый параметр этого класса. Отображение данных, переданных пользователем с предыдущим запросом, делается вручную - необходимо объект-форму сделать параметром шаблона и явно вставлять обращения к свойствам этого объекта где нужно. Проиллюстрируем всё это на примере страницы добавления заметок. Вначале в пакете org.nop.examples.notes.view создайте класс формы AddNoteForm: package org.nop.examples.notes.view;
import org.nop.forms.Form;
import org.nop.forms.FormValue;
@Form
public class AddNoteForm {
private String title;
private String content;
@FormValue
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
@FormValue
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}Далее, создайте шаблон AddNoteView: package org.nop.examples.notes.view;
import org.nop.templating.Template;
public interface AddNoteView extends Template {
AddNoteView setForm(AddNoteForm form);
}<?xml version="1.0" encoding="UTF-8"?>
<t:template xmlns:t="http://nop.org/schemas/templating/core">
<t:head>
<t:parameter name="form"/>
<t:service name="route" class="##.NoteRoute"/>
<t:service name="lang" class="org.nop.localization.Language"/>
</t:head>
<t:body>
<t:use class="#.MainView">
<t:template name="title">New note</t:template>
<t:template name="content">
<h1>New note</h1>
<form method="POST" action="${route.add()}">
<div>Title:</div>
<div><input type="text" name="title" value="${form.title}"/></div>
<div>Content:</div>
<div>
<textarea name="content" rows="12" cols="60">${form.content}</textarea>
</div>
<div><input type="submit" name="@method.create" value="Create note"/></div>
</form>
<div class="navigationArea">
<a href="${route.list()}">View last notes</a>
</div>
</t:template>
</t:use>
</t:body>
</t:template>Метод add контроллера модифицируйте: public Content add(AddNoteForm form) {
return html(createView(AddNoteView.class).setForm(form));
}Как видите, здесь просто отрисовывается страница. Где же добавляется заметка? Если на этом этапе попробовать запустить приложение, то вы сможете перейти на страницу добавления заметки, однако нажатие на кнопку Create note будет приводить к ошибке. Нажатие кнопки должно обрабатываться отдельным методом. Имя этого метода задаётся с помощью такой нотации: @method.methodName. Кроме того, этот метод должен быть совместим по типам параметров с исходным методом отрисовки страницы (в данном случае - add). Итак, создайте в контроллере метод create: public Content create(AddNoteForm form) {
int id = repository.createNote(form.getTitle(), form.getContent());
return redirect(route(NoteRoute.class).view(id));
}Метод возвращает не шаблон, а указание на редирект на другую страницу. Кстати, известен ещё один приём, похожий на редирект, называемый forward. В nop нет какого-либо специального механизма для этого - просто вызовите нужный метод контроллера. Если теперь собрать модуль и запустить приложение, то создание заметок будет нормально работать. ЗаключениеИтак, мы создали полноценное приложение для nop. Оно получилось максимально упрощённым - нет даже работы с БД. Вряд ли из такого приложения можно извлечь какую-либо пользу. Однако, первый шаг сделан. В дальнейшем можно совершенствовать это приложение, иллюстрируя другие возможности фреймворка. Скачать получившееся приложение можно здесь. Собрать его можно и без Eclipse, только в директорию lib следует подложить файл nop.all.jar. |