Вебчик и Scala (via Play! Framework)

Введение

“Не консолью единой живы люди” — подумал я и решил, что надо попробовать наваять простое веб-приложение на Play! Framework.

Play! Framework — это MVC фреймворк для создания веб-приложений на Java/Scala. На первый (да и на второй, если честно) взгляд кажется довольно простым и ничем особо не отличающимся от других подобных фреймворков. Однако, есть мнение, что для версии 2.x надо брать на вооружение Scala, а для Java лучше подойдет более взрослая версия 1.x.

Задача

Свой небольшой эксперимент я решил провести со Scala и Play! версии 2.1, который идет в комплекте с IDE от JetBrains IntelliJ IDEA. Удобным является то, что запустить приложение можно прямо в IDEA.

Если вы еще не установили плагин для Scala — самое время это сделать. Теперь можно создать приложение из шаблона Play 2.x.

Play! Framework

При создании проекта в IDE будет создана базовая структура проекта с необходимым минимумом для приложения: по одному контроллеру и представлению. Модели в шаблон приложения не завезли. Так же для создания скелета приложения можно исспользовать activator. Что радует — активатор содержит в себе полноценную IDE выполнив команду activator ui.

На самом деле, для простого приложения этого более чем достаточно. А делать ничего сильно серьезного и не планировали: просто перенесем предыдущее приложение в веб.

Вкратце: приложение получает на вход поисковый запрос к сайту stackoverflow.com и возвращает список вопросов подходящих этому запросу.

Представление

Представления в Play имеют расширение *.scala.html и, по сути, являются такими же функциями и могут вызываться одно из другого:

<footer>
   Powered by Play Framework
</footer>

@(title: String)(content: => Html)(implicit flash: Flash)
<!DOCTYPE html>
<html>
    <head>
        <title>@title</title>
    </head>
    <body>
        <section class="content">@content</section>
        @footer()
    </body>
</html>

Есть возможность создать “базовое” представление (по аналогии с Master Page в ASP.NET WebForms или Shared Layout в ASP.NET MVC), которое будет расширяться конкретными представлениями. Например, у есть представление main.scala.html:

@(title: String)(content: Html)
<html>
   <head>
      <title>@title</title>
   </head>
   <body>
      @content
   </body>
</html>

В представлении index.scala.html мы можем написать следующий код:

@main("Result page") {
  <h1>Hi there!</h1>
}

На деле всё это соберется во что-то похожее на следующий кусок:

<html>
   <head>
      <title>Result page</title>
   </head>
   <body>
      <h1>Hi there!</h1>
   </body>
</html>

Контроллеры

Контроллеры располагаются в модуле controllers. Каждый контроллер должен наследоваться от класса play.api.mvc.Controller.

Конфигурирование путей (routs) необходимо производить в файле /conf/routes. Например, строка

GET     /Search                     controllers.Application.search(query: String)

Значит, что при обращении к адресу http://host:port/Search?query=scala будет выполняться GET запрос к actionsearch контроллера Application с параметром query равным scala. Подробнее можно почитать здесь.

Модели

Модель — обычный класс, который, по своей сути, являются классом, описывающими бизнес-объекты области.

Приложение

Так как мы делаем веб-приложение, то возможностей у нас больше: будет отображаться не просто список заголовков вопросов. Заголовки будут являться ссылками на сами вопросы на StackOverflow. Так же добавим в список вопросов автора вопроса со ссылкой на его профиль.

Приложение будет включать три представления: index.scala.html (“главная” страница нашего сайта), search.scala.html (страница с поиском и результатами поиска) и мастер-представление main.scala.html. В приложении будет так же один контроллер Application.scala, который будет содержать всю логику и модель QuestionModel.scala, содержащая описание вопроса.

Контроллер содержит два action-а: index и search (соответсвенно используют index.scala.html и search.scala.html). Представление и action index будет отображаться, если к приложению обращаются по адресу http://host:port/. Если обращаются по адресу http://host:port/search?query=scala, то будет вызываться action search с параметром из адресной строки q.

В приложении используются библиотеки json4s для работы с JSON и scalaj-http для выполнеия запросов к API StackOverflow.

Логика получения ответов от API StackOverflow ничем не отличается от предыдущего приложения:

   val url = "https://api.stackexchange.com/2.2/search?order=desc&sort=activity&site=stackoverflow&intitle=" +
      java.net.URLEncoder.encode(query, "UTF-8")
   // response будет содержать ответ от API StackOverflow
   val response: HttpResponse[String] = Http(url).asString

Для того, чтобы ответ корректно десериализовался необходимо создать несклько вспомогательных case-классов: Questions, представляющий собой список всех вопросов; Question — один вопрос; User — автор вопроса:

case class Questions(items: List[Question])
case class Question(answer_count: Int,
                    creation_date: String,
                    is_answered: Boolean,
                    last_activity_date: Long,
                    link: String,
                    owner: User,
                    question_id: Long,
                    score: Integer,
                    tags: List[String],
                    title: String,
                    view_count: Integer)
case class User (display_name: String,
                 link: String,
                 profile_image: String,
                 reputation: Integer,
                 user_id: Long,
                 user_type: String)

Данные классы написаны на основе ответа от API StackOverflow:

{"items":[{"tags":["java","scala","maven","apache-spark"],"owner":{"reputation":1019,"user_id":317027,"user_type":"registered","accept_rate":69,"profile_image":"https://www.gravatar.com/avatar/de811098d6fa6c11aa0be27262cd1796?s=128&d=identicon&r=PG","display_name":"hba","link":"http://stackoverflow.com/users/317027/hba"},"is_answered":false,"view_count":16,"answer_count":0,"score":-1,"last_activity_date":1449080010,"creation_date":1449080010,"question_id":34050028,"link":"http://stackoverflow.com/questions/34050028/scala-test-maven-plugin-multiple-spark-context-exception","title":"Scala Test Maven Plugin - Multiple Spark Context Exception"}]}

json4s позволяет просто распарсить ответ в виде JSON-строки к case-классу:

   // Требуется для корректной сериализации JSON.
   implicit val formats = Serialization.formats(NoTypeHints)
   // Десериализация ответа StackOverflow к объекту Questions
   val output = org.json4s.jackson.Serialization.read[Questions](response.body)

Представление search.scala.html ожидает на вход объект класса scala.collection.immutable.List[models.QuestionModel] (об этом чуть позже). Для этого преобразуем наш объект класса Questions к scala.collection.immutable.List[models.QuestionModel] и вернем его в представление search.scala.html:

   Ok(views.html.search(output.items.map(q =>
      new models.QuestionModel(q.title, q.link, q.is_answered, q.owner.display_name, q.owner.link))))

Класс QuestionModel не содержит никакой логики:

package models

class QuestionModel(t: String, l: String, a: Boolean, on: String, ol: String) {
  var title: String = t
  var link: String = l
  var is_answered: Boolean = a
  var owner_name: String = on
  var owner_link: String = ol
}

Приложеие содержит три представления:
main.scala.html — “базовое” представление
index.scala.html — стартовая страница приложения
search.scala.html — страница отображения результатов поиска

На каждой странице приложения должна быть возможность поиска вопросов. Таким образом, форму поиска вопросов можно вынести в представление main.scala.html. Так же каждая страница должна иметь стандартную html разметку (теги <html>, <head> и т.п.). Следовательно, их тоже можно вынести в это представление:

@(title: String)(content: Html)

<!DOCTYPE html>
<html>
   <head>
      <title>@title</title>
      <link rel="stylesheet" media="screen" href="assets/stylesheets/main.css">
      <link rel="shortcut icon" type="image/png" href="assets/images/favicon.png">
   </head>
   <body>
      <div class="center">
         <form action="Search" method="GET">
            <input id="query" name="query" type="text" />
            <input id="search" type="submit" value="search"/>
         </form>
      </div>
      @content
   </body>
</html>

В представлении index.scala.html означим заголовок страницы и значение тега title:

@(message: String)

<h1>@message</h1>
@main("Welcome to SO searcher") { }

Строка @(message: String) значит, что в это представление можно передать строку, которой потом можно будет воспользоваться в представлении. Что и делаем в строке <h1>@message</h1>. Строка @main("Welcome to SO searcher") { } вызывает представление main.scala.html с первым параметром Welcome to SO searcher и пустым вторым параметром. Т.е. в итоге эти два представления сольются в одну страницу.

Представление search.scala.html имеет чуть более сложную структуру. В этом представлении требуется отображать резульататы поискового запроса. К счастью, как говорилось выше, представления в Play! являются Scala-функциями и позволяются использовать Scala-код (правда с некоторыми ограничиениями).

Нам достаточно базовой функциональсти предоставляемой шаблонизатором Play!: проход по циклу и формирование разметки маркированного списка с примененем соответствующего класса, если вопрос содержит ответ:

<ul>
   @for(item <- result) {
      <li>
         <a @if(item.is_answered) { class='answered' } href="@item.link">@item.title</a> from <a href="@item.owner_link">@item.owner_name</a>
      </li>
   }
</ul>

Заключение

В целом впечатление Scala как о языке для веб-разработки осталось двоякое: с одной стороны, довольно приятный синтаксис Scala, но, с другой стороны, стандартный шаблонизатор Play! иной раз выводил из себе непонятными сообщениями об ошибках. Возможно, всё это можно решить. Но пока, пожалуй, не буду советовать бросать всё и начинать разрабатывать на связке Play! Framework и Scala.

Итоговый проект можно найти на github

По мотивам:
Scala — Working with REST service calls and handling JSON
Working With JSON in Scala USsing The JSON4S Library (Part Two)
Scala Templats

Вебчик и Scala (via Play! Framework): Один комментарий

Оставьте комментарий

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.