Re[3]: Версия 0.1.0 доступна для скачки.
От: Курилка Россия http://kirya.narod.ru/
Дата: 12.07.09 07:20
Оценка: 5 (1) +1
Здравствуйте, dimgel, Вы писали:

D>Здравствуйте, Курилка, Вы писали:


К>>Можел лучше репозиторий было организовать?

К>>Скажем, на гуглокоде, где бесплатный issue tracker есть?

D>Рано. Там слишком много грязи ещё, и я не хочу, чтобы мне мешали ставить дикие эксперименты, заглядывая в промежуточные коммиты.


Дак не обязательно же всё коммитить в транк
А так feedback хоть какой-то был бы, думаю.
Но, хозяин — барин, конечно.
Версия 0.1.0 доступна для скачки.
От: dimgel Россия https://github.com/dimgel
Дата: 12.07.09 07:05
Оценка: 19 (1)
Всем привет.

Сабж: http://dimgel.ru/lib.web/files/

Начинать с README, разделы "как изучать" и "как запускать примеры".
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Я тут потихоньку ковыряю веб-фреймворк на scala...
От: dimgel Россия https://github.com/dimgel
Дата: 25.06.09 05:51
Оценка: 18 (1)
Всем привет.

В общем, сабж. Будет опен-сорц, public domain. Там всё очень сыро, не утрясены даже некоторые ключевые архитектурные решения. Но парой примерчиков хочется поделиться. Один сегодня, один завтра-послезавтра. То, что утрясено. Пока что в виде статей, без ссылок на скачу исходников (вылью как утрясу кое-что, если появятся заинтересованные). Всё по-английски, т.к. планирую интервенцию в scala community.

файл .../lib/web/example/hello01/Main.scala
package ru.dimgel.lib.web.example.hello01

import ru.dimgel.lib.web.core
import core.request.Request
import core.response.HTMLResponse

/**
 * DISCLAIMER ONE: I have no time yet to format scaladoc blocks appropriately.
 *                 Please read all docs in source code.
 * 
 * DISCLAIMER TWO: It's all in early development phase. Examples itself do work, 
 *                 but stuff mentioned in comments probably does not yet.
 * 
 * This is simplest "Hello world!" example. To run it standalone (outside example 
 * project), ensure your WEB-INF/web.xml looks like this:
 * 
 * <?xml version="1.0" encoding="utf-8"?>
 * <!DOCTYPE web-app PUBLIC 
 *     "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" 
 *     "http://java.sun.com/dtd/web-app_2_3.dtd">
 * <web-app>
 *     <servlet>
 *         <servlet-name>servlet</servlet-name>
 *         <servlet-class>ru.dimgel.lib.web.core.Servlet</servlet-class>
 *         <init-param>
 *             <param-name>main</param-name>
 *             <param-value>ru.dimgel.lib.web.example.hello01.Main</param-value>
 *         </init-param>
 *        </servlet>
 *        <servlet-mapping>
 *         <servlet-name>servlet</servlet-name>
 *         <url-pattern>/ *</url-pattern>
 *     </servlet-mapping>
 * </web-app>
 * 
 * NOTE: There must be no space between / and * chars in <url-pattern>. 
 *       I added space only to avoid them being parsed as nested comment.
 * 
 * Your subclass of lib.web.core.Main class is entry point into your web application.
 * The service() method dispatches incoming requests. For now, we'll just generate
 * simple HTMLResponse for all requests.
 * 
 * Lib.web framework heavily uses Scala's type system. Truly, the main design goal was
 * to enforce as much is possible via typization, to provide maximum support from IDE.
 * 
 * One of major consequences of this approach is using Scala's built-in XML support 
 * instead of external interpreted templates. In fact, there's no template system at all. 
 * I only plan to add some HTMLResponse transformations (e.g. head merging). Anyway,
 * you can (make and) use your own template engine and even introduce another response 
 * class for seamless integration with lib.web.
 * 
 * There are other response classes besides HTMLResponse. Actually, there are hierarchies
 * of requests and responses. The goal main goal is to enforce API restrictions via type
 * system. For example, access to request InputStream is provided only by RawRequest, 
 * but interface to "multipart/form-data" parts is provided by MultipartRequest.
 * The service() method can pattern-match request class (and request attributes) to
 * dispatch request, as demonstrated in the next example.
 * 
 * Response objects are created by application code during request processing. This is a 
 * functional approach - to look at service() method as function (Request) => Response. 
 * It allows easy use of response class hierarchy to provide different APIs for different 
 * response types. It's used in particluar by error handling (shown in the next example).
 * 
 * Oh, and by the way, I prefer factory methods to "new" operator. And have enforced it
 * too. Just coinciseness. If you try writing "new HTMLResponse(...)", you'll get
 * compiler error. =)
 */
class Main(servlet: core.Servlet) extends core.Main(servlet) {
  override def service(rq: Request) = HTMLResponse(rq,
    <html>
      <head>
        <title>{getClass.getCanonicalName}</title>
        <link rel="stylesheet" type="text/css" href="../../lib.web/example.css"/>
      </head>
      <body>
        <h1>Hello world!</h1>
        <p>This is simplest <span class="libweb">lib.web</span> example ever.</p>
      </body>
    </html>
  )
}
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Пример 2: request dispatching, error handling, surrounding t
От: dimgel Россия https://github.com/dimgel
Дата: 26.06.09 06:08
Оценка: 18 (1)
Всем привет.

Сабж. Коду мало, букв в каментах много. Просьба: кроме отзывов по описанному, давать отзывы по качеству описания, ежели оное где-то хромает.

package ru.dimgel.lib.web.example.dispatch02

import java.util.regex.Pattern
import scala.xml.NodeSeq
import ru.dimgel.lib.web.core
import core.request._
import core.response._

/*
 * This is the second lib.web example that demonstrates request dispatching,
 * error handling, and also surrounding template (pure Scala equivalent for Lift's 
 * <lift:surround with="..."> tag).
 
 * The example dispatches and services requests for two normal pages (/ and /about/me), 
 * forwards requests for static content to default servlet (three different use-cases 
 * shown), outputs custom 404 error page for /missing/handled/... requests, outputs 
 * default (container-provided) 404 error page for other /missing/... requests.
 *
 * NOTE ABOUT TEMPLATES
 * 
 * The technique used for implementing surrounding template is based on treating all 
 * templates as just functions (data) => NodeSeq. This is quite universal functional
 * approach, so I'm sure every template-related use-cases can be expressed this way.
 * 
 * The only problem is what can be a template input. Because templates are ordinary
 * functions just as business logic methods are, it's easy to fall into mess. I consider
 * this problem is the problem of programmer's discipline - to carefully separate logic
 * from presentation.
 * 
 * I'll probably make some another example demonstrating various use-cases on this
 * concern, including "view-first" pattern on which Lift's request processing is based,
 * but for now I just emphasized the problem by introducing nested "template" functions
 * in methods home(), aboutMe() and error404().
 */
class Main(servlet: core.Servlet) extends core.Main(servlet) {
  
  /*
   * This method is called by core.Servlet before any other request processing occurs
   * (even before creation of Request wrapper object). Parameter pathInfo is 
   * HttpServletRequest.pathInfo. If this method returns false, request is immediately 
   * forwarded to default (container-provided) servlet, without any futher processing.
   * 
   * Default implementation returns true. Implementation shown in this example states 
     * that subfolders /images/, /js/ and /css/ contain ordinary files and are to be 
     * served by default servlet.
   * 
   * Additionally, framework always forwards requests with pathInfo starting with 
   * "/lib.web/" to default servlet. This is checked before a call to willService() 
   * method. (I plan servicing jquery.js and other stuff from /lib.web/ folder.) 
   * 
   * NOTE: Currently I use single project for lib.web and examples, and this example is 
   *       mapped to /example/dispatch02/ in web.xml. File /lib.web/example.css is 
   *       served by different instance of core.Servlet which is mapped to /. That's why 
   *       in this example I use relative path ../../lib.web/example.css and it all still 
   *       works.
   */
  override def willService(pathInfo: String) =
    pathInfo == null || !Pattern.matches("^/(images|js|css)/.*$", pathInfo)
  
  /*
   * Normally, service() method would not generate response but pattern-match and
   * dispatch request to particular handlers. Request can be matched against 
   * specific request method and path (and against particular Request subclass
   * which is not shown in this example). 
   * 
   * The request.Method class is enum. I code enums in a fashion like this:
   * 
   * sealed abstract class Method(name: String) extends NotNull
   * object Method {
   *   object GET extends Method("GET")
   *   object POST extends Method("POST")
   *   ...
   * 
   *   // Factory method
   *   def apply(name: String): Method = ...
   * }
   * 
   * The request.Path class is representation of HttpServletRequest.pathInfo suitable 
   * for pattern-matching. It consists of, and can be matched against List[String] of 
   * components (split along slashes) and boolean trailingSlash. For example:
   * 
   * > Path(List("about", "me"), false) corresonds to pathInfo="/about/me",
   * > Path(List("about", "me"), true) corresonds to pathInfo="/about/me/",
   * > Path(Nil, false) corresponds to pathInfo=null.
   * 
   * The service() method implementation below matches whole request. If you need to 
   * match only path components, this can be done in a shorter way:
   * 
   * rq.path.components match {
   *   case Nil => home(rq)
   *   case List("about", "me") => aboutMe(rq)
   *   ...
   * }
   */
  override def service(rq: Request) = rq match {
    case Request(Method.GET, Path(Nil, _)) => home(rq)
    case Request(Method.GET, Path(List("about", "me"), _)) => aboutMe(rq)

    // Paths matching "^/missing(/|$)" are handled by serviceError() method.
    // ErrorResponse takes optional "message" argument which is omitted here.
    case Request(_, Path(List("missing", _*), _)) => ErrorResponse(rq, Status.NotFound)

    // Default implementation forwards request to default (container-provided) servlet. 
    // Same can be done by returning ForwardDefaultResponse(rq). Useful for servicing 
    // static content.
    case _ => super.service(rq)
    //case _ => ForwardDefaultResponse(rq)
  }
  
  /*
   * This method is called if service() returns ErrorResponse.
   * 
   * Some more notes on service(), serviceError(), ErrorRequest and ErrorResponse
   * (please read them AFTER reading serviceError() code and comments there):
   * 
   * > Remember that all response classes take request object as first argument.
   *   Attempt to create more than one response for given request will result in
   *   IllegalStateException.
   * 
   * > Attempt to return response created for different request results in
   *   AssertionError.
   * 
   * > Attempt to return ForwardDefaultResponse from serviceError() will result in
   *   AssertionError.
   * 
   * > Request and response objects are not thread-safe. All request processing
   *   must occur in same thread. The good news is that core.Servlet and core.Main
   *   classes contain no static data, so you can declare many applications in a single
   *   web.xml (as it's done in lib.web examples project).
   */
  override def serviceError(rq: ErrorRequest) = rq match {
    
    // Output custom error page for paths mathing "^/missing/handled(/|$)".
    // ErrorRequest is matched against method, path, status and message, where
    // status and message are the same as in ErrorResponse returned by service().
    case ErrorRequest(_, Path(List("missing", "handled", _*), _), Status.NotFound, _) => 
      error404(rq)
    
    /*
     * Default implementation outputs container-generated error page. Same can be done
     * by returning ErrorResponse(rq). The ErrorResponse(ErrorRequest) constructor
     * creates exactly the same object as was previously returned by service() - with 
     * same status and message. Or you can call ErrorResponse(Request, status[, message]).
     * 
     * Returning ErrorResponse from serviceError() finally results in a call to 
     * HttpServletResponse's sendError(status.code, message) if message != null, 
     * or sendError(status.code) otherwise.
     */
    case _ => super.serviceError(rq)
    //case _ => ErrorResponse(rq)
  }
  
  /**
   * Handler for home page, called by service().
   * Contains no logic, just template.
   */
  private def home(rq: Request) = {
    def template = tSurround(rq, 
      <h1>Home</h1>
      <p>This is the second example in {tLibWeb} distribution. Try main menu (looking popup hints and clicking).</p> 
    )
    HTMLResponse(rq, template)
  }
  
  /**
   * Handler for /about/me page, called by service(). 
   * Contains no logic, just template.
   */
  private def aboutMe(rq: Request) = {
    def template = tSurround(rq, 
      <h1>About me</h1>
      <p>Please see comments in the source code.</p>
    )
    HTMLResponse(rq, template)
  }

  /*
   * Error handler for /missing/handled... pages, called by serviceError().
   * Contains no logic, just template.
   * 
   * Note that here we specify additional "status" argument to HTMLResponse, same as 
   * provided by passed ErrorRequest. It would be Status.NotFound, because ErrorRequest 
   * is only created by framework from ErrorResponse returned by service() method, and 
   * there we create only ErrorResponse with Status.NotFound.
   */
  private def error404(rq: ErrorRequest) = {
    def template = tSurround(rq,
      <h1>Error 404</h1>
      <p><strong>Page not found: {rq.path}</strong></p>
      <p>This is error page is generated for paths matching <em>^/missing/handled(/|$)</em>.</p>
    )
    HTMLResponse(rq, rq.status, template)
  }
  
  /*
   * Template for lib.web logo. 
   * In this example, global templates are functions with "tXXX" name convention.
   */
  private def tLibWeb = <span class="libweb">lib.web</span>
  
  /*
   * Surrounding template.
   * In this example, global templates are functions with "tXXX" name convention.
   * 
   * Note how "content" argument is declared: function instead of value. This is
   * "lazy eval" behaviour. If you want content calculated before tSurround()
   * starts execution ("eager eval"), declare it as ordinary value parameter.
   * 
   * This template also takes Request argument, required to generate <base> tag. It 
   * is about "what can be template input" concern. Will dig it in a future examples.
   */
  private def tSurround(rq: Request, content: => NodeSeq): NodeSeq =
    <html>
      <head>
        <title>{getClass.getCanonicalName}</title>
        <base href={rq.baseUrl}/>
        <link rel="stylesheet" type="text/css" href="../../lib.web/example.css"/>
      </head>
      <body>
        <div class="mainmenu">
          <a href="../../lib.web/example.css" title="Forwarded to default servlet by framework because absolute URI starts with /lib.web/. About using relative path here, see NOTE inside willService() method comments.">/lib.web/</a>
          <a href="js/script.js" title="Forwarded to default servlet because willService() returns false for /js/ subfolder">!willService()</a>
          <a href={rq.baseUrl} title="Dispatched by service() method to home()">service(): Home</a>
          <a href="about/me" title="Dispatched by service() method to aboutMe()">service(): About me</a>
          <a href="file.txt" title="Forwarded to default servlet by service() calling super.service()">super.service()</a>
          <a href="missing/handled" title="Dispatched by serviceError() to error404()">serviceError()</a>
          <a href="missing/handled/again" title="Dispatched by serviceError() to error404()">serviceError() again</a>
          <a href="missing/unhandled" title="Displays container-provided error page by serviceError() calling super.serviceError()">super.serviceError()</a>
        </div>
        {content}
      </body>
    </html>
}
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re[9]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: dimgel Россия https://github.com/dimgel
Дата: 26.06.09 11:24
Оценка: 18 (1)
Здравствуйте, Курилка, Вы писали:

К>как реализуются циклы/условные выражения?


Scala-код, внедрённый внутрь XML-литерала, может содержать другие XML-литералы, также с внедрённым скала кодом, и т.д. Рекурсивно. Так что все управляющие конструкции пишутся на scala.

К>По-моему не зря шаблоны делают ограниченными в средствах


Может и не зря, но ничего кроме геморроя это в итоге не даёт. Элементарный, но часто встречающийся и вусмерть за#$%вающий пример: где форматировать дату? Или например был у меня тут заказчик на CMS, в которой bread crumbs для каталога товаров должны были верстаться по-разному в зависимости от категории товара, причём шаблон bread crumbs должен был лежать в настройках CMS (чтобы разные сателлиты-клоны могли обеспечить свою непохожесть друг на дружку). Ну невозможно уложить реальную жизнь в убогие возможности шаблонов. Думаешь, почему Smarty ругают? При использовании шаблонизаторов проникновение вёрстки в код — лишь вопрос времени.

Кстати, в Symfony (самый навороченный на данный момент PHP web framework) в качестве шаблонов используются обычные PHP-файлы. Хочешь — гони обычную разметку с <?php>-вставками (если ты дизайнер с дримвивером), хочешь — гони PHP-код с echo "<p>...</p>". Это очень правильно в том смысле, что в Symfony, в отличие от Smarty, мне не нужно беспокоиться о том, склонировали ли эти козлы PHP-функцию date(), форматирующую даты, или операторы if/for/while, и какой у этих операторов синтаксис в данном шаблонизаторе. У меня гарантированно есть все возможности PHP, и мне не нужно изучать just another piece of shit. Однако что в Symfony сделано плохо — это то, что инклудятся PHP-файлы-шаблоны внутрь библиотечной функции, предоставляющей шаблону соответствующий контекст. То есть шаблон выполняется внутри функции. Чем плохо? Я не могу объявить в шаблоне свои функции, и как следствие не могу ни сделать нормальную декомпозицию шаблона, ни организовать вывод рекурсивных данных (для этого нужно объявить рекурсивную функцию). Решается это через некий template module (не помню как он у них называется) — отдельный модуль-класс (или модуль-функция... не помню) где-то в дебрях структуры сайта, даже если нужен он для одного единственного шаблона. Заточка под дизайнеров с дримвиверами, ага.

Гы, в общем не надо мне рассказывать про "ограничение в средствах" — я этого говна сполна наелся.

Вот. Это было во-первых и в главных.




Во-вторых, добровольное использование искуственных костылей является признанием в неспособности самостоятельно придерживаться простых code conventions. Я не скажу, что это просто — это песец как трудно. Но тем не менее, по сумме аргументов (включая эффективность и поддержку от IDE) я свой выбор сделал. Имею Мнение — Хрен Оспоришь.




Кстати, шаблонизатор Lift хорош тем, что в шаблоны в принципе невозможно внедрить код. При этом шаблоны эти ну просто до безобразия гибкие, и вообще всячески достойны изучения как образец правильного подхода к разделению кода и представления. И тем не менее, в них изначально заложена официальная возможность генерирвать вёрстку из контроллеров. В сущности весь шаблонизатор построен на этом:

// Шаблон:
<table>
  <Hello:world>
        <tr><th>User name:</th><td><user:name/></td>...</tr>
    </Hello:world>
</table>

// Код :
class Hello {
  def world(content: NodeSeq) = bind("user", content, "name" -> "John Brown")
    // Метод возвращает NodeSeq с подставленным значением <user:name/>.
}


Чуете мощь? Контроллер вызывается из шаблона, контроллеру на вход передаётся кусок разметки с макросами <ns:var/> (внутренность тега <Hello:world>), он подставляет туда значения макросов с помощью bind("ns", xml, "var1" -> value1, "var2" -> value2, ...), ничего не зная о структуре разметки, и возвращает готовый NodeSeq. Циклы там делаются похожим образом. Вся диспетчеризация запросов построена на паттерне "view first": запрос мапится на шаблон, а шаблон в нужных местах инклудит контроллеры описанным образом. Очень красиво и удобно. Не нужен единый контроллер на каждую страницу, в котором нужно помнить выполнить генерацию данных для всех блоков страницы. При этом сам шаблон вместе с вызовыми контроллеров и параметрами контроллеров — голый HTML.

Тем не менее, ничто не мешает мне внедрить в шаблон вызов <Goodbye:world/> и сгенерировать в методе Goodbye.world() возвращаемый NodeSeq с нуля. Так что опять всё упирается в самодисциплину.

Кстати говоря, в PHP-шаблонах Symfony (повторюсь: наиболее продвинутый PHP web framework) тоже всё упирается в самодисциплину. Ответственный товарищ будет пользовать date() и number_format(), а безответственный — mysql_query(). Из сказанного я делаю вывод, что эволюция движется по направлению от искуственных ограничений в сторону удобств для ответственных товарищей.




Ну и в-третьих, добавлю чисто философское соображение: если язык сам по себе (включая либы, разумеется) настолько выразителен, что на нём можно сделать что-либо просто, то было бы идиотизмом наворачивать поверх лишний слой сложности. Это лишь добавит головной боли тем, кто будет этот фреймворк изучать. Это и к вопросу о наличии функции date(), и к тому, что память наша не резиновая. Одно дело — тонкий слой поверх известных вдоль и поперёк возможностей языка и Servlet API (спецы по сервлетам могли заметить, что моё решение с диспетчеризацией запросов и обработкой ошибок — это в сущности перепевка настроек web.xml с использованием pattern matching), а другое — дикие навороты, в которых чёрт ногу сломает.




В общем, единственный аргумент за внешний шаблонизатор, который я готов принять, — это дизайнеры с дримвиверами. Но как я уже говорил, мне этот use case на данный момент не интересен. (Если он мне вдруг станет интересен, я в ноль cклонирую шаблоны Lift в отдельный независимый модуль. Обеспечу совместимость, так сказать.)
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re[8]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: dimgel Россия https://github.com/dimgel
Дата: 26.06.09 11:45
Оценка: 18 (1)
Здравствуйте, Mamut, Вы писали:

M>А если шаблон состоит из несколькизх подшаблонов?


Функциональная декомпозиция. В моём втором примере показан один use-case. Ты зачем поставил там оценку, если разобраться не удосужился?

M>А если шаблон один раз скомпилировать в некое промежуточное представление?


Ага, создадим себе трудности и затем успешно их преодолеем. Автор Lift как раз сейчас работает над кешированием своих шаблонов. (Интересно, cglib использует?) Я повторю свои аргументы:

(1) внешние шаблоны не дают нужной мне степени контроля со стороны IDE;
(2) внешние шаблоны лично мне не нужны, т.к. вёрстку я (почти всегда переписываю и) прикручиваю к сайту сам;
(3) поэтому для меня внешние шаблоны — это просто ещё один слой поверх языка, и так предоставляющего всё, что мне нужно;
(4) желающие могут легко заюзать свой любимый шаблонизатор: в простейшем случае можно тупо генерировать всё что нужно через шаблонизатор, а затем отдавать результат на вход HTMLResponse.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re[11]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: dimgel Россия https://github.com/dimgel
Дата: 26.06.09 12:53
Оценка: 18 (1)
Здравствуйте, Mamut, Вы писали:

M>То есть это будет выглядеть типа


M>
M>NodeSeq = 
M>   <html><body><p>
M>       {SomeClass.SomeMethod(SomeVar)}
M>       { 
M>          for...
M>          ....
M>          ....
M>       }
M>       {  while.... }
M>   </p></body></html>
M>

M>?

Скорее так:

<table>
  <tr><th>ID</th><th>Name</th></tr>
  {for(r <- users.getList(rq)) yield
    <tr><td>{r.id}</td>{r.name}</td></tr>
  }
</table>


То есть всё выглядит довольно пристойно и читабельно, главное здесь — со вкусом расставить скобки. :-P
Ну и какой-то контекст придётся протаскивать всюду — Request или что-то application-specific... Не плодить же ThreadLocal по каждому поводу.

Сдаётся мне, следующий пример будет "pure scala templates howto". Хотя я планировал нечто иное... оно сейчас в процессе обмозговывания, но лучше я сначала его домозгую, а потом "с высоты достигнутого" просмотрю свою доку по шаблонам ещё раз и оформлю примером.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re[4]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: dimgel Россия https://github.com/dimgel
Дата: 25.06.09 07:31
Оценка: +1
Здравствуйте, Курилка, Вы писали:

К>Явное разделение на мой взгляд проще и удобнее, чем умная (возможно) но большая дока.


Кстати, насчёт удобства — не флейма ради, но аргументация моя достаточно тривиальна: во внешнем шаблоне можно опечататься в имени подствляемой переменной, а в XML-литерале — нет. Единственное, что меня печалит, — у всех объектов есть метод toString, поэтому внутрь XML-литерала я могу написать любую фигню и оно скомпилируется, например <span>{user}</span> вместо <span>{user.name}</span>.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: Курилка Россия http://kirya.narod.ru/
Дата: 25.06.09 06:30
Оценка:
Здравствуйте, dimgel, Вы писали:
[cut]
D>
[cut]
D>class Main(servlet: core.Servlet) extends core.Main(servlet) {
D>  override def service(rq: Request) = HTMLResponse(rq,
D>    <html>
D>      <head>
D>        <title>{getClass.getCanonicalName}</title>
D>        <link rel="stylesheet" type="text/css" href="../../lib.web/example.css"/>
D>      </head>
D>      <body>
D>        <h1>Hello world!</h1>
D>        <p>This is simplest <span class="libweb">lib.web</span> example ever.</p>
D>      </body>
D>    </html>
D>  )
D>}
D>


Смешиваем хтмл и логику? Да здравствует PHP?
Re[2]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: dimgel Россия https://github.com/dimgel
Дата: 25.06.09 06:49
Оценка:
Здравствуйте, Курилка, Вы писали:

К>Смешиваем хтмл и логику?


Где ты тут увидел логику? Вообще это вопрос дисциплины программиста. Даже при наличии отдельного шаблонизатора. Я ещё месяц назад накатал огромадную доку по правилам написания шаблонов. Т.е. мой шаблонизатор не содержит библиотечного кода, но содержит большую доку.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re[3]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: Курилка Россия http://kirya.narod.ru/
Дата: 25.06.09 07:10
Оценка:
Здравствуйте, dimgel, Вы писали:

D>Здравствуйте, Курилка, Вы писали:


К>>Смешиваем хтмл и логику?


D>Где ты тут увидел логику? Вообще это вопрос дисциплины программиста. Даже при наличии отдельного шаблонизатора. Я ещё месяц назад накатал огромадную доку по правилам написания шаблонов. Т.е. мой шаблонизатор не содержит библиотечного кода, но содержит большую доку.


Т.е. логики не будет?
Более вменяемый пример был бы гораздо полезней.
Re[3]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: Курилка Россия http://kirya.narod.ru/
Дата: 25.06.09 07:17
Оценка:
Здравствуйте, dimgel, Вы писали:

D>Здравствуйте, Курилка, Вы писали:


К>>Смешиваем хтмл и логику?


D>Где ты тут увидел логику? Вообще это вопрос дисциплины программиста. Даже при наличии отдельного шаблонизатора. Я ещё месяц назад накатал огромадную доку по правилам написания шаблонов. Т.е. мой шаблонизатор не содержит библиотечного кода, но содержит большую доку.


+ Вопрос — править это дело можно будет только ручками/IDE? Вход дизайнерам запрещён (со всякими там дримвиверами)?
Явное разделение на мой взгляд проще и удобнее, чем умная (возможно) но большая дока.
Re[4]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: dimgel Россия https://github.com/dimgel
Дата: 25.06.09 07:17
Оценка:
Здравствуйте, Курилка, Вы писали:

К>Т.е. логики не будет?

К>Более вменяемый пример был бы гораздо полезней.

Следующий пример будет по поводу диспетчеризации запросов и обработки ошибок. Там тоже один голый HTML. Логика будет в третьем примере, который будет предположительно через неделю. Но твою претензию по поводу разделения логики и представления я взял на заметку — во втором примере задокументирую кратенько идею своей многостраничной доки. Тем более, что в этом примере присутствует композиция шаблонов.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re[4]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: dimgel Россия https://github.com/dimgel
Дата: 25.06.09 07:22
Оценка:
Здравствуйте, Курилка, Вы писали:

К>+ Вопрос — править это дело можно будет только ручками/IDE? Вход дизайнерам запрещён (со всякими там дримвиверами)?


Угу. Мой опыт фрилансера-программиста показывает, что за дизайнерами-фрилансерами всю вёрстку приходится переделывать. Ну а для более правильных пацанов я и написал в комментариях про расширяемость. Гы, я конечно понимаю, что расширяемость это одно, а готовый шаблонизатор — совсем другое, но клонирование (весьма понравившихся мне) шаблонов Lift-а у меня далеко не на первом месте.

К>Явное разделение на мой взгляд проще и удобнее, чем умная (возможно) но большая дока.


+1. Но в той доке в основном тщательно разжёванные примеры use cases, как можно встроенными средствами языка повторять конструкции шаблонизатора Lift. Я выше тебе ответил, что идею там можно описать весьма кратко. В сущности, в двух словах: шаблон — это функция (DTO) => XML.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re[5]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: Курилка Россия http://kirya.narod.ru/
Дата: 25.06.09 08:40
Оценка:
Здравствуйте, dimgel, Вы писали:

D>Здравствуйте, Курилка, Вы писали:


К>>Явное разделение на мой взгляд проще и удобнее, чем умная (возможно) но большая дока.


D>Кстати, насчёт удобства — не флейма ради, но аргументация моя достаточно тривиальна: во внешнем шаблоне можно опечататься в имени подствляемой переменной, а в XML-литерале — нет. Единственное, что меня печалит, — у всех объектов есть метод toString, поэтому внутрь XML-литерала я могу написать любую фигню и оно скомпилируется, например <span>{user}</span> вместо <span>{user.name}</span>.


А toString гвоздями в компиляторе скальном пришит (сам её давно не трогал)?
Re[5]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: Mamut Швеция http://dmitriid.com
Дата: 25.06.09 13:25
Оценка:
Здравствуйте, dimgel, Вы писали:

d> К>+ Вопрос — править это дело можно будет только ручками/IDE? Вход дизайнерам запрещён (со всякими там дримвиверами)?


d> Угу. Мой опыт фрилансера-программиста показывает, что за дизайнерами-фрилансерами всю вёрстку приходится переделывать. Ну а для более правильных пацанов я и написал в комментариях про расширяемость. Гы, я конечно понимаю, что расширяемость это одно, а готовый шаблонизатор — совсем другое, но клонирование (весьма понравившихся мне) шаблонов Lift-а у меня далеко не на первом месте.


Можно взять StringTemplate


d> К>Явное разделение на мой взгляд проще и удобнее, чем умная (возможно) но большая дока.


d> +1. Но в той доке в основном тщательно разжёванные примеры use cases, как можно встроенными средствами языка повторять конструкции шаблонизатора Lift. Я выше тебе ответил, что идею там можно описать весьма кратко. В сущности, в двух словах: шаблон — это функция (DTO) => XML.


А если шаблон ну ооочень большой?
avalon 1.0rc1 rev 239, zlib 1.2.3


dmitriid.comGitHubLinkedIn
Re[6]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: dimgel Россия https://github.com/dimgel
Дата: 26.06.09 01:54
Оценка:
Здравствуйте, Курилка, Вы писали:

К>А toString гвоздями в компиляторе скальном пришит (сам её давно не трогал)?


Объявлен на классе Any (т.е. и на примитивах, и на подклассе AnyRef aka java.lang.Object). Оверрайднуть его можно, но в данном случае смысла нет.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re[6]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: dimgel Россия https://github.com/dimgel
Дата: 26.06.09 01:54
Оценка:
Здравствуйте, Mamut, Вы писали:

M>А если шаблон ну ооочень большой?


Тогда загружать и парсить внешний файл — ещё более накладно (и по процессору, и по памяти) по сравнению с внедрённым в код литералом.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re[7]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: Mamut Швеция http://dmitriid.com
Дата: 26.06.09 07:08
Оценка:
d> M>А если шаблон ну ооочень большой?

d> Тогда загружать и парсить внешний файл — ещё более накладно (и по процессору, и по памяти) по сравнению с внедрённым в код литералом.


А если шаблон состоит из несколькизх подшаблонов?
А если шаблон один раз скомпилировать в некое промежуточное представление?
avalon 1.0rc1 rev 239, zlib 1.2.3


dmitriid.comGitHubLinkedIn
Re[8]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: Курилка Россия http://kirya.narod.ru/
Дата: 26.06.09 07:15
Оценка:
Здравствуйте, Mamut, Вы писали:

d>> M>А если шаблон ну ооочень большой?


d>> Тогда загружать и парсить внешний файл — ещё более накладно (и по процессору, и по памяти) по сравнению с внедрённым в код литералом.


M>А если шаблон состоит из несколькизх подшаблонов?

M>А если шаблон один раз скомпилировать в некое промежуточное представление?

В эту же копилку:
как реализуются циклы/условные выражения?
на "целиковой" Скале?
По-моему не зря шаблоны делают ограниченными в средствах, чтобы даже не было соблазна писать "факториалы" внутри шаблонов, и делать ту задачу, которая возложена — генерить выходной HTML.
"Разделяй и властвуй" (ц) Цезарь (?)
Re[10]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: dimgel Россия https://github.com/dimgel
Дата: 26.06.09 12:04
Оценка:
D>Кстати, в Symfony (самый навороченный на данный момент PHP web framework) в качестве шаблонов используются обычные PHP-файлы. Хочешь — гони обычную разметку с <?php>-вставками (если ты дизайнер с дримвивером), хочешь — гони PHP-код с echo "<p>...</p>". Это очень правильно в том смысле,

А ещё в том, что для PHP+HTML все известные мне HTML-редакторы обеспечивают syntax hilighting & code completion, а для Smarty-шаблонов — нет.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re[10]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: Mamut Швеция http://dmitriid.com
Дата: 26.06.09 12:35
Оценка:
Здравствуйте, dimgel, Вы писали:

d> К>как реализуются циклы/условные выражения?


d> Scala-код, внедрённый внутрь XML-литерала, может содержать другие XML-литералы, также с внедрённым скала кодом, и т.д. Рекурсивно. Так что все управляющие конструкции пишутся на scala.



То есть это будет выглядеть типа

NodeSeq = 
   <html><body><p>
       {SomeClass.SomeMethod(SomeVar)}
       { 
          for...
          ....
          ....
       }
       {  while.... }
   </p></body></html>

?

d> К>По-моему не зря шаблоны делают ограниченными в средствах


d> Может и не зря, но ничего кроме геморроя это в итоге не даёт. Элементарный, но часто встречающийся и вусмерть за#$%вающий пример: где форматировать дату?


Ай. Это больно

[skip]

d>


d> Кстати, шаблонизатор Lift хорош тем, что в шаблоны в принципе невозможно внедрить код. При этом шаблоны эти ну просто до безобразия гибкие, и вообще всячески достойны изучения как образец правильного подхода к разделению кода и представления. И тем не менее, в них изначально заложена официальная возможность генерирвать вёрстку из контроллеров. В сущности весь шаблонизатор построен на этом:


d>
d> // Шаблон:
d> <table>
d>         <tr><th>User name:</th><td><user:name/></td>...</tr>
d> </table>

d> // Код :
d>   def world(content: NodeSeq) = bind("user", content, "name" -> "John Brown")
d>     // Метод возвращает NodeSeq с подставленным значением <user:name/>.
d> }
d>


d> Тем не менее, ничто не мешает мне внедрить в шаблон вызов <Goodbye:world/> и сгенерировать в методе Goodbye.world() возвращаемый NodeSeq с нуля. Так что опять всё упирается в самодисциплину.



У меня, кстати, в планах — допилить Эрланговский ErlyDTL до чего-то похожего (или тут). Потому что даже ограниченный инструмент должен бть максимально расширенным
avalon 1.0rc1 rev 239, zlib 1.2.3


dmitriid.comGitHubLinkedIn
Re[10]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: Курилка Россия http://kirya.narod.ru/
Дата: 26.06.09 13:33
Оценка:
Здравствуйте, dimgel, Вы писали:

[cut]

D>В общем, единственный аргумент за внешний шаблонизатор, который я готов принять, — это дизайнеры с дримвиверами. Но как я уже говорил, мне этот use case на данный момент не интересен. (Если он мне вдруг станет интересен, я в ноль cклонирую шаблоны Lift в отдельный независимый модуль. Обеспечу совместимость, так сказать.)


Ты забыл ещё возможность правки шаблонов и смены их без перекомпиляции кода.
Плюс ещё сейчас, насколько я понимаю, не поддерживаются "темы" (прописываемые в конфиге, или выбираемые пользователем). Внешние шаблоны для этого не обязательны, конечно, но на них это реализуется практически тривиально.
Re[11]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: dimgel Россия https://github.com/dimgel
Дата: 26.06.09 13:44
Оценка:
Здравствуйте, Курилка, Вы писали:

D>>В общем, единственный аргумент за внешний шаблонизатор, который я готов принять, — это дизайнеры с дримвиверами.

К>Ты забыл ещё возможность правки шаблонов и смены их без перекомпиляции кода.

Да это в сущности то же самое. Если я из Eclipse не вылезаю, мне что внешний шаблон править, что внедрённый — один хрен. Так что фича эта тоже полезна исключительно дизайнерам с дримвивером.

К>Плюс ещё сейчас, насколько я понимаю, не поддерживаются "темы" (прописываемые в конфиге, или выбираемые пользователем). Внешние шаблоны для этого не обязательны, конечно, но на них это реализуется практически тривиально.


Под тривиальностью ты имеешь в виду — тупо копируем каталог с шаблонами и подкручиваем копию? Ну в общем да. Тут правда имеется небольшая засада — деплоймент. Либо мы пакуем все шаблоны всех тем в war-файл, и тогда каждая правка требует перепаковки (что отличается от перекомпиляции только затратами времени, да и то не сильно, если не всё подряд перекомпилять), либо мы держим шаблоны на сервере в отдельном месте, в распакованном виде, и огребаем геморрой с деплойментом. Всё это звучит неприятно, но не страшно; в конце концов, есть rsync, так что всё автоматизируемо.

// Начал было писать про то, что под существенные изменения дизайна практически всегда приходится перезатачивать логику. Но потом очухался: разные темы — это несколько попроще, чем разные дизайны.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: meowth  
Дата: 28.06.09 11:53
Оценка:
Здравствуйте, dimgel, Вы писали:

D>Всем привет.


D>В общем, сабж. Будет опен-сорц, public domain. Там всё очень сыро, не утрясены даже некоторые ключевые архитектурные решения. Но парой примерчиков хочется поделиться. Один сегодня, один завтра-послезавтра. То, что утрясено. Пока что в виде статей, без ссылок на скачу исходников (вылью как утрясу кое-что, если появятся заинтересованные). Всё по-английски, т.к. планирую интервенцию в scala community.



А Lift вам чем не УГодил?
Re[2]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: dimgel Россия https://github.com/dimgel
Дата: 28.06.09 12:25
Оценка:
Здравствуйте, meowth, Вы писали:

M>А Lift вам чем не УГодил?


1) Он stateful с дикими требованиями к памяти даже на простейших задачах. На моём VPS успешно крутится PHP-проектик под весьма приличной нагрузкой, но попытка перетащить пару активно используемых скриптов на Lift немедленно привела к переполнению памяти. С такими требованиями недолго и PHP полюбить. Ну а вконец добили меня uploaded files, которые он целиком загружает в память, хотя существует Apache FileUpload — стандарт де-факто (а в Servlet 3.0 — уже и де-юре). И это блин называется 1.0 release.

2) Про single responsibility principle автор лифта походу вообще не слышал. Его сверх-убогий, не годный ни на что кроме хомяков Васи Пупкина, ORM (что он и сам признаёт открытым текстом: если у вас больше пары десятков таблиц или имеются сложные joins, то юзайте JPA). Понатыкано в этом ORM всё в кучу — и persistence, и validation, и presentation. Я тут где-то успел порадовался на эту тему, но увы, радует это только первые пару дней. А уж на список ответственностей класса Menu из его книги, и на реализацию всего этого вообще смотреть нельзя без истерики.

Короче, перегруженная помойка дурных велосипедов.

3) Да и шаблоны внешние я не хочу. Все эти bind() со строковыми константами. Раздражает.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re[3]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: dimgel Россия https://github.com/dimgel
Дата: 28.06.09 13:06
Оценка:
D>Про single responsibility principle автор лифта походу вообще не слышал.

И не только про него. С глобальными конфигурациями и thread-local переменными мужик, прямо скажем, чуток переборщил.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re[4]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: Mamut Швеция http://dmitriid.com
Дата: 29.06.09 08:46
Оценка:
Здравствуйте, dimgel, Вы писали:

d> D>Про single responsibility principle автор лифта походу вообще не слышал.


d> И не только про него. С глобальными конфигурациями и thread-local переменными мужик, прямо скажем, чуток переборщил.


Ну, что поделать. Первопроходцам тяжело
avalon 1.0rc1 rev 239, zlib 1.2.3


dmitriid.comGitHubLinkedIn
Пример 1 (по-русски)
От: dimgel Россия https://github.com/dimgel
Дата: 29.06.09 09:47
Оценка:
package ru.dimgel.lib.web.example.ru.hello01

/*
 * _root_ указан, т.к. в противном случае компилятор ищет внутри пакета 
 * ru.dimgel.lib.web.example.ru вместо _root_.ru.
 */
import _root_.ru.dimgel.lib.web.core
import core.request.Request
import core.response.HTMLResponse

/*
 * DISCLAIMER: Всё это пока что на ранней стадии разработки. Примеры работают, 
 *             но вещи, упомянутые в комментариях, возможно ещё нет.
 * 
 * Это простейший пример "Hello world". Чтобы запустить его в виде отдельного 
 * веб-приложения (вне данного проекта), ваш web.xml должен выглядеть так:
 * 
 * <?xml version="1.0" encoding="utf-8"?>
 * <!DOCTYPE web-app PUBLIC 
 *     "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" 
 *     "http://java.sun.com/dtd/web-app_2_3.dtd">
 * <web-app>
 *     <servlet>
 *         <servlet-name>servlet</servlet-name>
 *         <servlet-class>ru.dimgel.lib.web.core.Servlet</servlet-class>
 *         <init-param>
 *             <param-name>main</param-name>
 *             <param-value>ru.dimgel.lib.web.example.ru.hello01.Main</param-value>
 *         </init-param>
 *        </servlet>
 *        <servlet-mapping>
 *         <servlet-name>servlet</servlet-name>
 *         <url-pattern>/ *</url-pattern>
 *     </servlet-mapping>
 * </web-app>
 * 
 * ЗАМЕЧАНИЕ: Между символами / и * внутри <url-pattern> пробела быть не должно. 
 *       Я добвил пробел, чтобы компилятор не ругался на вложенный комментарий.
 * 
 * Точкой входа в ваше веб-приложение является ваш подкласс класса core.Main. Метод 
 * service() выполняет обработку входящих запросов. В данном примере, мы генерируем 
 * простой HTMLResponse в ответ на любые запросы.
 * 
 * Lib.web интенсивно использует систему типов языка Scala. В сущности, одной из основных 
 * моих целей было получить максимальную поддержку от IDE, а также максимально ограничить 
 * пользователям библиотеки возможности по отстреливанию себе ног, через использование
 * статической типизации.
 * 
 * Одно из основных следствий данного подхода - я использую встроенную в Scala поддержку
 * XML вместо внешних шаблонизаторов. В будущих примерах я планирую показать, каким
 * образом различные варианты композиции и использования шаблонов могут быть выражены
 * в чисто функциональном стиле одними лишь средствами языка Scala. Также я планирую
 * добавить некоторые преобразования HTMLResponse (например, для "head merging"), но
 * это будут просто вспомогательные утилиты; система шаблонов в lib.web как таковая
 * отсутствует. Однако, вы можете использовать с lib.web любой шаблонизатор и отдавать
 * результат его работы на вход HTMLResponse (если результат - NodeSeq) или WriterResponse 
 * (если результат - String).
 * 
 * Подробнее мои аргументы за и против шаблонизаторов см. в обсуждении на форуме RSDN:
 * http://rsdn.ru/forum/web/3444298.aspx
 * 
 * В lib.web существуют расширяемые иерархии классов запросов и ответов. Цель, как уже было
 * сказано, - выразить ограничения протокола HTTP и Servlet API через систему типов Scala.
 * Например, доступ к InputStream запроса предоставляется только классом RawRequest,
 * а интерфейс к частям запроса "multipart/form-data" (т.е. к выгруженным файлам) - классом 
 * MultipartRequest. Метод service() может использовать pattern matching для диспетчеризации
 * запросов, как показано в следующем примере (example/ru/dispatch02).
 * 
 * Экземпляр класса-ответа (подклассов класса Response) создаётся кодом приложения
 * в процессе обработки запроса. Это функциональный подход - рассматривать метод service()
 * как функция (Request) => Response. Такой подход позволяет удобно использовать иерерхию
 * классов Response и обеспечивать различные API у разных подклассов Response. Это 
 * используется, в частности, при обработке ошибок (показано также в следующем примере).
 * 
 * Кстати говоря, я предпочитаю фабричные методы вместо оператора new. Во-первых, из-за
 * краткости, а во-вторых, реализацию фабричных методов проще поменять прозрачным для 
 * пользователя образом. Поэтому я запрещаю использование new всюду, где могу. Если вы 
 * попробуете написать "new HTMLResponse(...)", вы получите ошибку компиляции.
 */
class Main(servlet: core.Servlet) extends core.Main(servlet) {
  override def service(rq: Request) = HTMLResponse(rq,
    <html>
      <head>
        <title>{getClass.getCanonicalName}</title>
      </head>
      <body>
        <h1>Превед!</h1>
        <p>Это простейший пример использования <strong>lib.web</strong>.</p>
      </body>
    </html>
  )
}
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Пример 2 (по-русски)
От: dimgel Россия https://github.com/dimgel
Дата: 29.06.09 09:47
Оценка:
package ru.dimgel.lib.web.example.ru.dispatch02

import java.util.regex.Pattern
import scala.xml.NodeSeq
import _root_.ru.dimgel.lib.web.core
import core.request._
import core.response._

/*
 * Это второй пример lib.web. Он демонстрирует диспетчеризацию запросов, обработку
 * ошибок, и заодно surrounding template - эквивалент тега <lift:surround with="...">
 * средствами языка Scala).
 
 * Данный пример обрабатывает запросы для двух обычных страниц (/ и /about/me),
 * перенаправляет запросы статичного контента к сервлету "default" (показаны три 
 * варианта использования), выводит собственную страницу 404 для запросов вида 
 * /missing/handled/..., выводит дефолтную (генерируемую контейнером) страницу 404
 * для всех остальных запросов вида /missing/...
 *
 * ЗАМЕЧАНИЕ О ШАБЛОНАХ
 * 
 * В основе моего подхода к созданию шаблонов (в частности surrounding template в данном
 * примере) лежит взгяд на любой шаблон как на функцию (данные) => NodeSeq. Это вполне
 * универсальный функциональный подход, и я уверен, что любые варианты использования
 * шаблонов могут быть выражены в его рамках.
 * 
 * Единственная проблема - решить, какие данные подавать на вход шаблона. Поскольку 
 * шаблоны - это обычные функции, как и методы бизнес-логики, можно очень легко прийти 
 * к бардаку. Я считаю, что это проблема самодисциплины программиста - грамотное 
 * разделение логики от преставления. 
 * 
 * Я планирую сделать ещё один пример, посвящённый исключительно разным вариантам 
 * композиции и использования шаблонов, включая рассмотрение паттерна "view first",
 * на основе которого построена вся обработка запросов в Lift framework. А пока что,
 * в данном примере я подчеркнул проблему, добавив вложенные функции template()
 * внутрь методов home(), aboutMe() и error404().
 * 
 * На всякий случай повторю ссылку на мои соображения касательно внешних шаблонизаторов
 * на форуме RSDN: http://rsdn.ru/forum/web/3444298.aspx
 */
class Main(servlet: core.Servlet) extends core.Main(servlet) {
  
  /*
   * Метод willService(pathInfo) вызывается из core.Servlet при получении запроса в самую 
   * первую очередь, до какой-либо дальнейшей обработки (даже до создания экземпляра 
   * подкласса Request). Параметр pathInfo - это HttpServletRequest.pathInfo. Если данный 
   * метод возвращает false, запрос тут же перенаправляется сервлету "default" 
   * (контейнеру), без какой-либо дальнейшей обрабоки запроса.
   *
   * Реализация по умолчанию всегда возвращает true. Реализация, показанная в данном
   * примере, предполагает, что подкаталоги /images/, /js/ and /css/ содержат только
   * обычные файлы и должны обслуживаться сервлетом по умолчанию (контейнером).
   * 
   * В дополнение к данному методу, фреймворк всегда перенаправляет контейнеру запросы,
   * у которых pathInfo начинается с "/lib.web/". Эта проверка выполняется до вызова
   * метода willService(). (Я планирую поместить в этот подкаталог query.js и прочие
   * вещи того же рода.) 
   * 
   * NOTE: В данный момент я использую один общий проект для lib.web и примеров. Данный 
   *       пример замапен на /example/ru/dispatch02/ в файле web.xml, при этом файл 
   *       /lib.web/example.css отдаётся другим экземпляром core.Servlet, который замапен
   *       на корень /. Поэтому в данном примере я использую относительный путь 
   *       ../../../lib.web/example.css, и всё работает.
   */
  override def willService(pathInfo: String) =
    pathInfo == null || !Pattern.matches("^/(images|js|css)/.*$", pathInfo)
  
  /*
   * Метод service() обычно занимается не генерацией ответа, а анализом (с использованием
   * pattern matching) и диспетчеризацией запросов к соответствующим обработчикам.
   * У запроса может проверяться метод и путь (а также собственно класс запроса -
   * конкретный подкласс класса Request, но в данном примере это не показано).
   * 
   * Класс request.Method - это enum. Я кодирую enum следующим образом:
   * 
   * sealed abstract class Method(name: String) extends NotNull
   * object Method {
   *   object GET extends Method("GET")
   *   object POST extends Method("POST")
   *   ...
   * 
   *   // Фабричный метод:
   *   def apply(name: String): Method = ...
   * }
   * 
   * Класс request.Path - это представление HttpServletRequest.pathInfo, удобное для
   * pattern matching. Оно состоит из, и может быть сопоставлено со списком List[String]
   * компонент пути (разделённых слешем) и с булевым значением trailingSlash (хвостовой
   * слеш). Например:
   * 
   * > Path(List("about", "me"), false) соответствует pathInfo="/about/me",
   * > Path(List("about", "me"), true) соответствует pathInfo="/about/me/",
   * > Path(Nil, false) соответствует pathInfo=null.
   * 
   * Приведённая в данном примере реализация метода service() сопоставляет объект-запрос
   * целиком. Если вам, например, нужно сопостовлять только компоненты пути, это можно 
   * сделать короче:
   * 
   * rq.path.components match {
   *   case Nil => home(rq)
   *   case List("about", "me") => aboutMe(rq)
   *   ...
   * }
   */
  override def service(rq: Request) = rq match {
    case Request(Method.GET, Path(Nil, _)) => home(rq)
    case Request(Method.GET, Path(List("about", "me"), _)) => aboutMe(rq)

    /*
     * Для путей вида /missing или /missing/... мы возвращаем ErrorResponse; фреймворк 
     * передаст обработку этих запросов методу serviceError(). Конструктор (вернее, 
     * фабричный метод) ErrorResponse принимает дополнительный аргумент message, 
     * здесь опущенный. 
     */
    case Request(_, Path(List("missing", _*), _)) => ErrorResponse(rq, Status.NotFound)

    /*
     * Реализация метода core.Main.service(Request) перенаправляет запрос сервлету 
     * "default" (контейнеру), путём возврата ForwardResponse(rq). Этот ответ можно 
     * вернуть и самостоятельно, с тем же эффектом. Это полезно для отдачи статического 
     * контента.
     */
    case _ => super.service(rq)
    //case _ => ForwardResponse(rq)
  }
  
  /*
   * Метод serviceError(ErrorRequest) вызывается фреймворком, если метод service(Request)
   * вернул ErrorResponse.
   * 
   * Несколько дополнительных замечаний насчёт service(), serviceError(), ErrorRequest и
   * ErrorResponse (пожалуйста, сначала прочитайте код и комментарии метода serviceError()):
   * 
   * > Все подклассы Response принимают первым параметром ссылку на Request.
   *   Попытка создать более одного ответа для одного запроса приведёт к выбросу
   *   IllegalStateException.
   * 
   * > Попытка вернуть из методов service() или serviceError() ответ, созданный для 
   *   другого запроса (отличного от переданного в параметре метода), приведёт к 
   *   выбросу AssertionError.
   * 
   * > Объекты запросов и ответов не являются thread-safe. Вся обработка запроса должна
   *   выполняться в одном потоке. Теперь хорошие новости: классы core.Servlet и core.Main
   *   не содержат статических данных, поэтому вы можете объявлять в одном web.xml 
   *   произвольное количество приложений (как и сделано в данном проекте: на каждый 
   *   пример - отдельный <servlet> и <servlet-mapping>, и всё в одном web.xml).
   */
  override def serviceError(rq: ErrorRequest) = rq match {
    
    /*
     * Генерируем собственную страницу ошибки 404 для путей вида /missing/handled и
     * /missing/handled/... Класс ErrorRequest сопостовляется по полям method, path, 
     * status и message, причём status и message заполняются фреймворком из экземпляра 
     * ErrorResponse, возвращённого методом service().
     */
    case ErrorRequest(_, Path(List("missing", "handled", _*), _), Status.NotFound, _) => 
      error404(rq)
    
    /*
     * Реализация метода core.Main.serviceError() выводит страницу ошибки, сгенерированную 
     * контейнером. Такой же эффект можно получить, вернув объект ErrorResponse(rq).
     * 
     * Фабричный метод ErrorResponse(ErrorRequest) создаёт точно такой же экземпляр 
     * ErrorResponse, какой был возвращён методом service() - с такими же значениями 
     * полей status и message. Впрочем, вы можете вызвать полную форму - 
     * ErrorResponse(Request, status, message).
     * 
     * Возврат ErrorResponse из serviceError() в итоге приводит к вызову метода
     * HttpServletResponse.sendError(): sendError(status.code, message) если 
     * message != null, или sendError(status.code) в противном случае.
     */
    case _ => super.serviceError(rq)
    //case _ => ErrorResponse(rq)
  }
  
  /*
   * Обработчик главной страницы, вызывается из service().
   * Не содержит логики, только шаблон.
   */
  private def home(rq: Request) = {
    def template = tSurround(rq, 
      <h1>Главная</h1>
      <p>Это второй пример в дистрибутиве {tLibWeb}.</p>
      <p>Наводите мышью на пункты главного меню для получения подсказок, и кликайте на них чтобы проверить их работу.</p>
      <p>Все рассказы что и как - в комментариях в исходном коде примера.</p> 
    )
    HTMLResponse(rq, template)
  }
  
  /*
   * Обработчик страницы /about/me, вызывается из service().
   * Не содержит логики, только шаблон.
   */
  private def aboutMe(rq: Request) = {
    def template = tSurround(rq, 
      <h1>Обо мне</h1>
      <p>Йа примерко. См. мой исходный код и комментарии в нём.</p>
    )
    HTMLResponse(rq, template)
  }

  /*
   * Обработчик для страниц /missing/handled и /missing/handled/..., вызывается из 
   * serviceError(). Не содержит логики, только шаблон.
   *
   * Обратите внимание, что здесь конструктору HTMLResponse передаётся дополнительный
   * параметр status, со значением, взятым из обрабатываемого запроса ErrorRequest.
   * Это значение Status.NotFound, т.к. ErrorRequest создаётся фреймворком только
   * на основании ErrorResponse, возвращённого методом service(), а в методе service()
   * мы возвращаем только один ErrorResponse - со статусом Status.NotFound.
   */
  private def error404(rq: ErrorRequest) = {
    def template = tSurround(rq,
      <h1>Ошибка 404</h1>
      <p><strong>Страница не найдена: {rq.path}</strong></p>
      <p>Данная страница ошибки генерируется для путей вида <em>/missing/handled</em> и <em>/missing/handled/...</em>.</p>
    )
    HTMLResponse(rq, rq.status, template)
  }
  
  /*
   * Шаблона для логотипа lib.web.
   * В данном примере глобально используемые функции-шаблоны имеют имена вида "tXXX".
   */
  private def tLibWeb = <span class="libweb">lib.web</span>
  
  /*
   * Surrounding template.
   * В данном примере глобально используемые функции-шаблоны имеют имена вида "tXXX".
   * 
   * Обратите внимание, как объявлен параметр content: как "ленивое" значение, вычисляемое
   * в момент использования. Если вам нужно, чтобы значение content вычислялось до вызова 
   * tSurround(), объявите этот параметр обычным образом.
   *
   * Данный шаблон также принимает параметр Request, который нужен для генерации элемента
   * <base/>. Это к вопросу о том, "какие данные подавать на вход шаблона". Здесь я 
   * использовал простейшее решение. Я постараюсь пройтись подробнее по этой теме в будущих 
   * примерах.
   */
  private def tSurround(rq: Request, content: => NodeSeq) =
    <html>
      <head>
        <title>{getClass.getCanonicalName}</title>
        <base href={rq.baseUrl}/>
        <link rel="stylesheet" type="text/css" href="../../../lib.web/example.css"/>
      </head>
      <body>
        <div class="mainmenu">
          <a href="../../../lib.web/example.css" title="Перенаправляется фреймворком контейнеру, т.к. путь начинается с /lib.web/. 
                       Касательно использования относительных путей в HTML-коде, см. замечание в в комментариях к методу willService().">/lib.web/</a>
          <a href="js/script.js" title="Перенаправляется контейнеру, т.к. willService() возвращает false для подкаталога /js/">!willService()</a>
          <a href={rq.baseUrl} title="Генерируется методом home(), вызываемым из service()">service(): Главная</a>
          <a href="about/me" title="Генерируется методом aboutMe(), вызываемым из service()">service(): Обо мне</a>
          <a href="file.txt" title="Перенаправляется контейнеру методом service() путём вызова super.service()">super.service()</a>
          <a href="missing/handled" title="Генерируется методом error404(), вызываемым из serviceError()">serviceError()</a>
          <a href="missing/handled/again" title="Генерируется методом error404(), вызываемым из serviceError()">тоже serviceError()</a>
          <a href="missing/unhandled" title="Генерируется контейнером, путём вызова super.serviceError() из метода serviceError()">super.serviceError()</a>
        </div>
        {content}
      </body>
    </html>
}
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Пример 3: параметры запроса - валидация, конвертация.
От: dimgel Россия https://github.com/dimgel
Дата: 11.07.09 06:11
Оценка:
package ru.dimgel.lib.web.example.ru.e03param

import java.util.regex.Pattern
import scala.xml.NodeSeq
import _root_.ru.dimgel.lib.web.core
import core.param._
import core.request._
import core.response._

/*
 * Данный пример демонстрирует доступ к параметрам запроса, валидацию значений параметров
 * и преобразование их к другим типам данных (конвертацию). В примере используются
 * встроенные в lib.web валидаторы и конвертеры значений. Каким образом можно создавать 
 * свои собственные валидаторы и конвертеры, будет продемонстрировано в следующем примере 
 * (e04customparam).
 * 
 * Я стараюсь излагать последовательно, но вероятно, данный пример нужно будет прочитать
 * дважды. Рекомендуется попробовать в работе все приведённые в данном примере TestCases, 
 * вводя в поля формы различные правильные и неправильные значения. Адрес работающего 
 * примера: 
 * 
 *     http://dimgel.ru:8080/lib.web/example/ru/e03param/
 */
class Main(servlet: core.Servlet) extends core.Main(servlet) {
  
  case class TestCase(title: String, result: Param.Value[_])
  
  override def service(rq: Request) = {
    /*
     * Свойство Request.param является экземпляром класса core.param.ParameterMap. 
     * С полным интерфейсом этого класса я ещё не определился (да и с именем тоже), 
     * но как минимум у него имеется метод apply(name: String), возвращающий экземпляр 
     * класса core.param.Param.
     * 
     * В данном примере мы будем использовать три параметра (nhb поля формы) - текстовую 
     * строку (text), целое десятичное число (int) и целое шестнадцатеричное число (hex):
     */
    val text = rq.param("text")
    val int = rq.param("int")
    val hex = rq.param("hex")
    
    /*
     * В общем случае параметры могут иметь по нескольку значений (например, multiselect
     * list). Поэтому внутреннее представление данных параметра - это List[String]. Вы
     * можете получить этот список, обратившись к свойству Param.data. Объявление класса
     * Param выглядит следующим образом:
     * 
     *     final case class Param(data: List[String]) extends NotNull { ... }
     * 
     * Большинство валидаторов и конвертеров (в частности, все, упомянутые в данном
     * примере) будут работать только с первым значением из списка (data.head) -
     * это наиболее часто используемый случай.
     * 
     * ЗАМЕЧАНИЕ. Этот же класс Param будет использован для доступа к заголовкам запроса,
     *            которые также могут иметь по нескольку значений (ещё не сделано).
     * 
     * Начнём. Чтобы определить, была ли засабмичена форма, проверяем наличие любого из 
     * параметров. Метод Param.isFound возвращает true если список значений параметра не 
     * пуст (data != null && data != Nil).
     */
    val submitted = rq.param("text").isFound
    
    /*
     * Этот список объектов TestCase мы передадим на вход шаблона.
     */
    val testResults =
      if (!submitted) Nil
      else {
        List (
          /*
           * Основной (канонический) способ получить значение параметра, приведённое к 
           * нужному типу (как правило это строка, число или дата), - это вызвать метод:
           *
           *     class Param {
           *       def apply[D] (validator, converter, defaultValue: D): Param.Value[D]
           *       ...
           *     }
           * 
           * Здесь validator - это цепочка валидаторов значения параметра; converter -
           * преобразователь значения из List[String] к нужному типу; defaultValue 
           * (значение по умолчанию) является обязательным - чтобы не забывали.
           * 
           * Типы данных валидаторов и конвертеров, входящих в lib.web, согласованы между 
           * собой и с типом значения по умолчанию. Например, вы не сможете записать в 
           * один вызов Param.apply() валидатор email-адреса (String), конвертер в целое 
           * число (Int) и значение по умолчанию типа Date. Эти ограничения форсируются
           * на этапе компиляции с помощью системы типов (generics + variance). Подробнее
           * о том, как именно это реализовано, см. в следующем примере (e04customparam).
           * 
           * Возвращаемый объект Param.Value объявлен следующим образом:
           *
           *     final case class Value[D](get: D, errors: List[VError]) extends NotNull {
           *       val ok = errors.isEmpty
           *     }
           * 
           * Он инкапсулирует значение параметра (либо значение по умолчанию) и список
           * ошибок валидации. Класс VError - это простой враппер над сообщением об ошибке
           * валидации:
           * 
           *     case class VError(message: String) extends NotNull
           * 
           * Приведённый ниже TestCase демонстрирует простейший и часто используемый 
           * случай - получение значения необязательного параметра в виде строки. Если 
           * параметр не задан или содержит пустое значение, будет возвращена пустая
           * строка:
           */
          TestCase(
            """text(VOptional, CString, "")""", 
            text(VOptional, CString, "")),
          
          /*
           * Имена валидаторов имеют вид Vxxx, имена конвертеров  - Cxxx. И те, и другие 
           * объявлены в пакете core.param.
           * 
           * Цепочки валидаторов всегда должны начинаться либо с VRequired, либо с 
           * VOptional. Это ограничение форсируется на этапе компиляции.
           * 
           * Оба валидатора - VRequired и VOptional - проверяют метод Param.isEmpty. 
           * Если параметр пуст (Param.data == null, Nil, или первое значение в списке 
           * является null или пустой строкой), то:
           * 
           * - VOptional-валидация считается успешной и возвращается значение по умолчанию 
           *   (без выполнения остальных валидаторов в цепочке);
           * 
           * - VRequired-валидация возвращает ошибку и значение по умолчанию 
           *   (также без выполнения остальных валидаторов в цепочке).
           * 
           * Если же параметр не пуст, то выполняются остальные валидаторы в цепочке.
           * При успешной валидации вызывается конвертер и возвращается сконвертированное
           * значение, в противном случае возвращается список ошибок и значение по 
           * умолчанию.
           * 
           * Такое решение позволяет упростить остальные валидаторы и конвертеры, избавив 
           * их от необходимости проверять значение параметра на непустоту. Кроме того,
           * получающийся в итоге вызов Param.apply() удобно и однозначно читается.
           */
          TestCase(
            """text(VRequired, CString, "hello")""", 
            text(VRequired, CString, "hello")),
          
          /*
           * Все стандартные валидаторы, кроме VOptional, принимают необязательный
           * параметр message - сообщение об ошибке валидации. По умолчанию выводится
           * сообщение на английском, индивидуальное для каждого валидатора.
           */
          TestCase(
            """text(VRequired(msg), CString, "goodbye")""", 
            text(VRequired("Введите строку"), CString, "goodbye")),
          
          /*
           * Как уже было сказано, валидаторы могут группироваться в цепочки.
           * Для этого используются операторы: "|" после VOptional, "&" после VRequired,
           * "&" и "&&" (short-circuit and full-circuit "and") после остальных валидаторов.
           */
          TestCase(
            """text(VOptional | VEmail, CString, "default")""", 
            text(VOptional | VEmail, CString, "default")),
          TestCase(
            """text(VRequired & VEmail(msg), CString, "default")""", 
            text(VRequired & VEmail("На Email не похоже"), CString, "default")),
          
          /*
           * Закомментированная ниже строка не скомпилируется, т.к. цепочка
           * валидаторов начинается не с VRequired или VOptional.
           */
          //TestCase("wrong", text(VEmail, CString, "")),
          
          /*
           * Ниже следуют TestCases с более длинными цепочками, использующими short-circuit 
           * и full-circuit "and". Валидаторы выполняются слева направо. Оператор "&" 
           * прекращает валидацию, если его левый параметр (часть цепочки слева от него)
           * вернул ошибку.
           * 
           * Операторы также можно группировать с помошью скобок. При этом реализация 
           * операторов VRequired.& и VOptional.| такова, что они неявно ставят скобки 
           * вокруг всех остальных валидаторо в цепи:
           * 
           *     VRequired & V1 & V2  ==>  VRequired & (V1 & V2)
           *     VOptional | V1 & V2  ==>  VOptional | (V1 & V2)
           *     (VRequired & V1 & V2) & V3  ==>  VRequired & ((V1 & V2) & V3)
           *     (VOptional | V1 & V2) & V3  ==>  VOptional | ((V1 & V2) & V3)
           * 
           * То есть, в корне дерева валидаторов всегда будут операции VRequired.& или
           * VOptional.|.
           * 
           * Валидатор в нижеприведённом TestCase читается так: обязательный параметр, 
           * длина 5-10 символов, должен быть числом (возможно, с лидирующими и хвостовыми
           * пробелами). Здесь также фигурирует разрешающий пробелы конвертер в целое 
           * (Int), и значение по умолчанию Int.
           */
          TestCase(
            """int(VOptional | VLen(5, 10) & VTrimInt, CTrimInt, 0)""", 
            int(VOptional | VLen(5, 10) & VTrimInt, CTrimInt, 0)),
        
          /*
           * Отличия данного TestCase от предыдущего: пробелы не разрешены, используется 
           * full-circuit "and". То есть, если вы введёте в поле int, например, строку 
           * "ааа", предыдущий валидатор выдаст только ошибку VLen (неправильная длина 
           * строки) и на этом остановится (т.к. short-circuit), а данный валидатор выдаст 
           * обе ошибки: VLen и VInt.
           */
          TestCase(
            """int(VOptional | VLen(5, 10) && VInt, CInt, 0)""", 
            int(VOptional | VLen(5, 10) && VInt, CInt, 0)),
          
          /*
           * Как уже упоминалось, стандартные валидаторы и конвертеры согласованы 
           * по типам данных между собой и со значениями по умолчанию. Как именно это
           * реализовано, подробно рассказано в следующем примере (e04customparam),
           * демонстрирующем создание собственных валидаторов и конвертеров. 
           * 
           * Нижеследующие закомментированные TestCases не скомпилируются:
           * 1) в первом указан конвертер целого без валидатора целого;
           * 2) во втором указано строковое значение по умолчанию с валидатором и 
           *    конвертером целого;
           * 3) в третьем используется валидатор целого, допускающий пробелы, с 
           *    конвертером целого, не допускающим пробелы (а вот наоборот можно).
           */
          //TestCase("wrong", int(VOptional, CInt, 0)),
          //TestCase("wrong", int(VOptional | VInt, CInt, "")),
          //TestCase("wrong", int(VOptional | VTrimInt, CInt, 0)),
          
          /*
           * На данный момент набор конкретных валидаторов и конвертеров - сырой и
           * ограниченный. Нет работы с датами (необходимо для замены getDateHeader()).
           * Кроме того, я не до конца уверен, не перемудрил ли я с Trim-валидаторами -
           * вроде бы всё хорошо, потому что строго, но выглядит несколько избыточно.
           * 
           * Что и как на данный момент реализовано, лучше всего смотреть в исходных 
           * текстах (core/param/validators.scala и core/param/converters.scala), а здесь 
           * я покажу ещё только один валидатор, весьма полезный сам по себе и интенсивно
           * используемый как базовый класс для многих других валидаторов: VRegex,
           * проверяющий первое значение параметра (Param.data.head) по регулярному 
           * выражению. Ниже показаны четыре формы фабричного метода для валидатор VRegex.
           */
          TestCase(
            """hex(VOptional | VRegex(Regex), CString, "")""", 
            hex(VOptional | VRegex("^[0-9a-fA-F]+$".r), CString, "")),
          TestCase(
            """hex(VOptional | VRegex(Regex, msg), CString, "")""", 
            hex(VOptional | VRegex("^[0-9a-fA-F]+$".r, "Не 16-ричное число"), CString, "")),
          TestCase(
            """hex(VOptional | VRegex(String), CString, "")""", 
            hex(VOptional | VRegex("^[0-9a-fA-F]+$"), CString, "")),
          TestCase(
            """hex(VOptional | VRegex(String, msg), CString, "")""", 
            hex(VOptional | VRegex("^[0-9a-fA-F]+$", "Не 16-ричное число"), CString, ""))
        )
      }
    
    HTMLResponse(rq, template(rq, testResults))
  }
  
  /*
   * Шаблон страницы. Форма для ввода параметров свёрстана вручную. (Модуль форм будет 
   * показан в последующих примерах... когда я его добью.)
   * 
   * Обратите внимание, каким образом значения параметров копируются в поля ввода 
   * (в атрибуты <input value=.../>). Продемонстрированы три способа:
   * 
   * 1. В поле name="text" используется способ, показанный в самом первом TestCase:
   *    (VOptional, CString, "")
   * 
   * 2. В поле name="int" вместо трёх параметров (валидатор, конвертер и значение по
   *    умолчанию) передаётся один объект - PString. Этот объект наследуется из класса
   *    Param.Strategy, инкапсулирующего все три параметра (валидатор, конвертер, 
   *    значение по умолчанию) в одном объекте. Объявления таковы:
   * 
   *        class Param { 
   *          def apply[D] (Param.Strategy[D]): Param.Value[D]
   *          ...
   *        } 
   *        object Param { 
   *          abstract case class Strategy[D] (validator, converter, default: D)
   *          ...
   *        }
   *        object PString extends Strategy(VOptional, CString, "")
   *    
   *    То есть, это паттерн "стратегия". В данном случае использование PString
   *    полностью эквивалентно предыдущей "полной" записи.
   * 
   *    Я не вижу большой пользы от этой идеи в плане добавления стандартных стратегий в 
   *    библиотеку, т.к. слишком много ограничений: нужны отдельные стратегии для
   *    VRequired и VOptional, плюс непонятно как быть с сообщениями об ошибках. Но
   *    возможно, при разработке конкретных веб-приложений эта возможность окажется 
   *    востребованной.
   * 
   * 3. В поле name="hex" используется специальный метод Param.s, дающий результат,
   *    полностью эквивалентный двум предыдущим вариантам.
   */
  private def template(rq: Request, testResults: List[TestCase]) = 
    <html>
      <head>
        <title>{getClass.getCanonicalName}</title>
        <base href={rq.baseUrl}/>
        <link rel="stylesheet" type="text/css" href="../../../lib.web/example.css"/>
      </head>
      <body>
        <h1>Чтение параметров запроса</h1>
        <p>Это третий пример в <span class="libweb">lib.web</span>, демонстрирующий 
          валидацию и конвертацию параметров запроса.</p>
        <h2>Форма</h2>
        <form><table>
          <tr>
            <th>Text:</th>
            <td><input type="text" name="text" value={rq.param("text")(VOptional, CString, "").get}/></td>
          </tr>
          <tr>
            <th>Int:</th>
            <td><input type="text" name="int" value={rq.param("int")(PString).get}/></td>
          </tr>
          <tr>
            <th>Hex:</th>
            <td><input type="text" name="hex" value={rq.param("hex").s}/></td>
          </tr>
          <tr>
            <th/>
            <td><input type="submit" value="Submit"/></td>
          </tr>
        </table></form>
        <h2>Результаты</h2>
        {if (testResults.isEmpty)
          <p>Введите данные форму.</p>
        else
          <dl class="messages">{
            for (r <- testResults) 
            yield {
              val attr = if (r.result.ok) "info" else "error"
              <dt class={attr}>{r.title} = {r.result.get}</dt> ++ {
                for (VError(message) <- r.result.errors) 
                yield <dd class={attr}>{message}</dd> 
              }
            }
          }</dl>
        }
      </body>
    </html>
}
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Пример 4: создание своих валидаторов и конвертеров
От: dimgel Россия https://github.com/dimgel
Дата: 11.07.09 06:32
Оценка:
package ru.dimgel.lib.web.example.ru.e04customparam

import java.util.regex.Pattern
import scala.xml.NodeSeq
import scala.collection.mutable.ListBuffer
import _root_.ru.dimgel.lib.web.core
import core.param._
import core.request._
import core.response._

/*
 * Данный пример демонстрирует создание своих собственных валидаторов и конвертеров для 
 * параметров запроса. НЕОБХОДИМО ЗНАКОМСТВО С ПРЕДЫДУЩИМ ПРИМЕРОМ, демонстрирующим
 * использование параметров.
 * 
 * Здесь мы рассмотрим валидацию поля "возраст" - целого числа в диапазоне от 16 до 150
 * (лет; отличный диапазон для сайтов знакомств).
 * 
 * Материал сложный. Возможно, его придётся прочитать минимум дважды. Рекомендуется 
 * попробовать пример в работе по адресу:
 * 
 *     http://dimgel.ru:8080/lib.web/example/ru/e04customparam/
 */
class Main(servlet: core.Servlet) extends core.Main(servlet) {
  /*
   * Согласование использования валидаторов и конвертеров на этапе компиляции необходимо 
   * для того, чтобы не допустить ошибочного использования, например, конвертера целого 
   * числа без предварительного вызова валидатора целого числа. 
   * 
   * Это реализовано с помощью type parametrization (generics), в т.ч. с использованием
   * lower bounds. Все валидаторы и конвертеры помечаются generic-параметром - тегом 
   * (подклассом core.param.Tags.TAny). Иерархия тегов отражает совместимость валидаторов 
   * и конвертеров. Сигнатура метода Param.apply() задаёт требуемое отношение между тегами 
   * валидаторов и конвертеров.
   * 
   * 
   * НАПОМИНАНИЕ. Собственное представление данных в классе Param - это List[String] 
   * (параметры и заголовки запроса могут иметь несколько значений). Однако большинство 
   * валидаторов и конвертеров работают только с первым элементом списка значений, т.е. 
   * рассматривают параметр как имеющий единственное значение (наиболее частый вариант
   * использования параметров в реальной жизни). Наличие первого элемента гарантируется
   * специальными валидаторами VRequired и VOptional, один из которых обязан быть
   * первым в цепочке валидаторов (это проверяется на этапе компиляции). Так что все 
   * остальные валидаторы (и конвертеры) могут без дополнительных проверок обращаться 
   * к Param.data.head (гарантируется != null).
   * 
   * 
   * Рассмотрим для примера пару имеющихся в lib.web тегов, пару валидаторов и конвертер:
   * 
   *     package ru.dimgel.lib.web.core.param
   * 
   *     object Tags {
   *       trait TAny  // список значений
   *       trait TString extends TAny  // одно значение
   *       ...
   *       trait TTrimInt extends TString
   *       trait TInt extends TTrimInt
   *       ...
   *     }
   * 
   *     class VInt extends VImp[TInt]
   *     class VTrimInt extends VImp[TTrimInt]
   * 
   *     // Первый параметр Int - это тип возвращаемого конвертером значения.
   *     class CTrimInt extends Converter[Int, CTrimInt]
   * 
   * Валидаторы, помеченные тегом TTrimInt или его подклассом(!), пропускают параметр, 
   * первое значение которого является целым числом с необязательными лидирующими и 
   * хвостовыми пробелами (это отражено в инфиксе "Trim" имени тега). Аналогично, 
   * конвертеры, помеченные тегом TTrimInt или его подклассом(!), гарантируют успешную 
   * конвертацию (не важно во что) параметра, первое значение которого является целым 
   * числом с необязательными лидирующими и хвостовыми пробелами.
   * 
   * Например, тег TInt является подклассом TTrimInt, что означает буквально следующее: 
   * "если параметр проходит валидацию как целое число с пробелами, то он и подавно 
   * пройдёт валидацию как целое число без пробелов". Соответственно, валидатор VInt 
   * может быть использован совместно с конвертером CTrimInt.
   * 
   * Замечания по использованию множественного наследования тегов - ниже, в примере 
   * тега TAge.
   * 
   * 
   * Точная сигнатура метода Param.apply() выглядит следующим образом:
   * 
   *     final def apply[D, CT <: TAny, VT <: CT] ( 
   *       validator: VChain[VT], 
   *       converter: Converter[D, CT], 
   *       default: D
   *     ): Param.Value[D]
   * 
   * Здесь D - тип данных, возвращаемых конвертером, CT - тег конвертера, VT - тег
   * валидатора, TAny - базовый trait для тегов. (ЗАМЕЧАНИЕ. Параметр validator имеет тип 
   * VChain, а не VImp; это связано с реализацией требования, чтобы цепочки валидаторов
   * начинались с VRequired или VOptional.)
   * 
   * Видно, что взаимное соответствие валидаторов и конвертеров задаётся условием 
   * VT <: CT (VT - подкласс CT). Это гарантирует, что если валидатор успешно выполнил 
   * валидацию параметра, то конвертер успешно этот параметр сконвертирует.
   * 
   * 
   * Объявление тега TTrimAge следует читать аналогично: "возраст - частный случай целого;
   * если параметр проходит валидацию как TTrimAge, то он пройдёт валидацию и как TTrimInt".
   */
  trait TTrimAge extends Tags.TTrimInt
  
  /*
   * Валидатор VTrimAge наследуется из VImp (базового класса для всех валидаторов) и 
   * переопределяет виртуальный метод validate(Param).
   * 
   * Конструкторы валидатора объявлены приватными. Для инстанциирования валидатора нужно 
   * использовать объект-компаньон. Этот подход используется в lib.web повсеместно.
   */
  class VTrimAge private(message: String) extends VImp[TTrimAge] {
    private def this() = this("Возраст задан неверно.")
    
    def validate(p: Param): List[VError] = {
      p.data.head match {
        // Здесь trim не используется (вызов s.toInt вместо s.trim.toInt), т.к. s содержит 
        // группу в круглых скобках (\d{2}), а не всю исходную строку.
        case VTrimAge.regex(s) if { val i = s.toInt; i >= 16 && i <= 150 } => Nil
        case _ => VError(message) :: Nil
      }
    }
  }
  
  /*
   * Объект-компаньон наследуется из класса, что позволяет использовать его в цепочках
   * валидаторов (без лишней пустой пары скобок, как было бы в случае использования метода 
   * apply()). То есть, в итоге мы имеем ровно два разрешённых варианта записи валидатора 
   * в выражениях: VTrimAge и VTrimAge(message).
   */
  object VTrimAge extends VTrimAge {
    def apply(message: String) = new VTrimAge(message)
    
    // Используется в validate().
    private val regex = """^\s*(\d{2,3})\s*$""".r
  }
  
  /*
   * Это второй пример тега. Он наследуется сразу от двух тегов и означает следующее: "если 
   * параметр проходит валидацию как возраст без пробелов, то он пройдёт валидацию и как 
   * целое без без пробелов, и как возраст с пробелами".
   * 
   * При объявлении тегов нужно максимально аккуратно и полно описывать их взаимосвязи.
   * Здесь не надо пренебрегать множественным наследованием, т.к. это даёт дополнительную
   * гибкость. Например, в данном случае с валидатором, помеченным тегом TAge, могут 
   * быть использованы конвертеры, помеченные тегами TAge, TTrimAge, TTrimInt или TInt
   * (TInt и TTrimAge наследуются из TTrimInt).
   */
  trait TAge extends Tags.TInt with TTrimAge
  
  /*
   * Данный валидатор использует вспомогательный класс VFunc, наследуемый из VImp и 
   * принимающий параметром конструктора либо функцию (Param) => List[VError] (точно 
   * соответствующую прототипу виртуального метода validate()), либо два параметра: 
   * функцию (Param) => Boolean и message: String. 
   * 
   * В данном примере я использую второй вариант конструктора. Также здесь я не стал
   * делать класс с параметром message, а оставил только объект с жёстко прошитым
   * сообщением об ошибке. Так что вариант использования данного валидатора в выражениях
   * только один: VAge.
   * 
   * ЗАМЕЧАНИЕ. Для валидаторов, чью логику можно выразить через единственное регулярное
   * выражение, рекомендуется использовать базовый класс VRegex. Если использовать VRegex
   * в цепочках как самостоятельный валидатор, он имеет тег TString, а при наследовании 
   * из него можно указывать любой тег.
   */
  object VAge extends VFunc[TAge] (
    (p: Param) => {
      val s = p.data.head
      // TODO Как сделать этот вызов попроще, не ссылаясь на pattern, но без match?
      if (!"^\\d{2,3}$".r.pattern.matcher(s).matches) false
      else {
        val i = s.toInt
        i >= 16 && i <= 150
      }
    },
    "Возраст задан неверно."
  )
  
  /*
   * Реализация данного конвертера похожа на CTrimInt. Он возвращает целое число.
   * Разница только в теге и отсутствии вызова trim перед toInt.
   * 
   * Конвертеры не проверяют параметр на непустоту, т.к. эта проверка выполняется методом 
   * Param.apply() после валидации. Если валидация была успешной и параметр пуст (такое 
   * возможно только при использовании VOptional), то конвертер не вызывается, а 
   * Param.apply() возвращает значение по умолчанию.
   * 
   * ЗАМЕЧАНИЕ. Это неудобно в случае checkbox: unchecked checkbox сабмитит пустое 
   * значение Param(Nil). Нужно помнить, что значение по умолчанию всегда должно быть 
   * false. Поэтому в модуле форм под checkbox создан готовый класс с настроенными 
   * валидаторами и конвертерами, причём конвертер возвращает true при любом значении
   * параметра, а значение по умолчанию false.
   */
  object CAge extends Converter[Int, TAge] {
    def apply(p: Param) = Some(p.data.head.toInt)
  }
  
  /*
   * А теперь посмотрим, как всё это будет работать. Проверять будем на одной странице,
   * с вручную свёрстанной GET-формой, содержащей одно поле "age".
   */
  case class TestCase(title: String, result: Param.Value[_])
  
  override def service(rq: Request) = {
    val p = rq.param("age")
    val testResults =
      if (!p.isFound) Nil
      else
        List(
          /*
           * Закомментированная ниже строка не скомпилируется, т.к. VTrimAge помечен 
           * тегом TTrimAge, CAge - тегом TAge, и TTrimAge не является подклассом TAge:
           */
          //TestCase("VRequired & VTrimAge, CAge", p(VRequired & VTrimAge, CAge, 0)),
          TestCase("VRequired & VTrimAge, CTrimInt", p(VRequired & VTrimAge, CTrimInt, 0)),
          TestCase("VRequired & VAge, CTrimInt", p(VRequired & VAge, CTrimInt, 0)),
          TestCase("VRequired & VAge, CAge", p(VRequired & VAge, CAge, 0)),
          
          /*
           * В цепочках валидаторов тег каждого следующего валидатора должен быть
           * равен тегу предыдущего валидатора, или быть его подклассом. Причины тому
           * две:
           * 
           * 1) Лень возиться с ковариантностью: боюсь, мозги задымятся.
           * 
           * 2) Оно и так смотрится вполне логично: сначала проверяются более слабые 
           *    условия, затем более жёсткие. Кроме того, при таком подходе невозможно 
           *    включить в одну цепочку валидаторы для несовместимых тегов (например,
           *    TInt и TDate, не являющихся подклассами друг дружки).
           *    
           * Тег всей цепочки валидаторов равен тегу последнего валидатора в ней, т.е.
           * самому дальнему подклассу TAny.
           * 
           * Закомментированная ниже строка не скомпилируется, т.к. тег VAge помечен 
           * тегом TAge, тег VLen - тегом TString, при этом TAge является подклассом 
           * TString (TAge <: TInt <: TString). Чтобы использовать оба эти валидатора, 
           * нужно поменять их местами, как в раскомментированном примере ниже, где мы
           * имеем следующие отношения между тегами:
           * 
           *     Объекты: VRequired & VLen(2) &  VAge     Вся цепь    CInt
           *     Теги:    TAny   <:   TString <: TAge ==> TAge     >: TInt
           */
          //TestCase("VRequired & VAge & VLen(2), CAge", p(VRequired & VAge & VLen(2), CInt, 0)),
          TestCase("VRequired & VLen(2) & VAge, CAge", p(VRequired & VLen(2) & VAge, CInt, 0))
        )
    
    HTMLResponse(rq, template(rq.baseUrl, p(VOptional, CString, "").get, testResults))
  }
  
  private def template(baseUrl: String, age: String, testResults: List[TestCase]) = 
    <html>
      <head>
        <title>{getClass.getCanonicalName}</title>
        <base href={baseUrl}/>
        <link rel="stylesheet" type="text/css" href="../../../lib.web/example.css"/>
      </head>
      <body>
        <h1>Валидаторы возраста</h1>
        <p>Это четвёртый пример в <span class="libweb">lib.web</span>, демонстрирующий 
          создание собственных валидаторов и конвертеров.</p>
        <h2>Форма</h2>
        <form>
          <p>Ваш возраст (16-150): 
            <input type="text" name="age" value={age} style="width: 5em"/>
            <input type="submit" value="Проверить"/></p>
        </form>
        <h2>Примеры</h2>
        <ul>
          <li><a href="?age=16">"16"</a> &mdash; всё ок.</li>
          <li><a href="?age=%2016%20">" 16 " (с пробелами)</a> &mdash; срабатывают VAge и VLen; первый пример ok.</li>
          <li><a href="?age=150">"150"</a> &mdash; срабатывает VLen(2); остальные примеры ok.<br/></li>
          <li><a href="?age=">"" (пустой ввод)</a> &mdash; срабатывают VRequired.</li>
          <li><a href="?age=a">"a"</a> &mdash; срабатывают VTrimAge, VAge и VLen(2).</li>
          <li><a href="?age=aa">"aa"</a> &mdash; срабатывают VTrimAge и VAge.</li>
          <li><a href="?age=1">"1"</a> &mdash; срабатывают VTrimAge, VAge и VLen(2).</li>
          <li><a href="?age=11">"11"</a> &mdash; срабатывают VTrimAge и VAge.</li>
        </ul>
        <h2>Результаты</h2>
        {if (testResults.isEmpty)
          <p>Введите данные форму или кликните на ссылке в примерах.</p>
        else
          <dl class="messages">{
            for (r <- testResults) 
            yield {
              val attr = if (r.result.ok) "info" else "error"
              <dt class={attr}>{r.title}</dt> ++ {
                for (VError(message) <- r.result.errors) 
                yield <dd class={attr}>{message}</dd> 
              }
            }
          }</dl>
        }
      </body>
    </html>
}
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re: Версия 0.1.0 доступна для скачки.
От: Курилка Россия http://kirya.narod.ru/
Дата: 12.07.09 07:08
Оценка:
Здравствуйте, dimgel, Вы писали:

D>Всем привет.


D>Сабж: http://dimgel.ru/lib.web/files/


D>Начинать с README, разделы "как изучать" и "как запускать примеры".


Можел лучше репозиторий было организовать?
Скажем, на гуглокоде, где бесплатный issue tracker есть?
Re[2]: Версия 0.1.0 доступна для скачки.
От: dimgel Россия https://github.com/dimgel
Дата: 12.07.09 07:17
Оценка:
Здравствуйте, Курилка, Вы писали:

К>Можел лучше репозиторий было организовать?

К>Скажем, на гуглокоде, где бесплатный issue tracker есть?

Рано. Там слишком много грязи ещё, и я не хочу, чтобы мне мешали ставить дикие эксперименты, заглядывая в промежуточные коммиты.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re[4]: Версия 0.1.0 доступна для скачки.
От: dimgel Россия https://github.com/dimgel
Дата: 13.07.09 13:04
Оценка:
Здравствуйте, Курилка, Вы писали:

К>Дак не обязательно же всё коммитить в транк


А можешь посоветовать, как лучше организовать? У меня нет опыта branche merging. Я пока подумываю что-то вроде следующего:
1) в рабочей ветке либа и примеры в одном проекте (до поры до времени, по крайней мере: так удобнее отлаживаться с `mvn jetty:run`);
2) созревшая версия копируется в отстойник, где разбивается на два проекта — либа отдельно, примеры отдельно (попутно удаляется всякий хлам);
3) вот это вылизанное копируется в так называемый "транк", который в данном случае транком собственно и не является.

Или как вариант (если google code такое позволяет) — даю доступ к tags (e.g. tags/0.1.0), в качестве отстойника использую branches (e.g. branches/0.1.0), а шуршу в закрытом trunk (так более по-человечески).
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re[5]: Версия 0.1.0 доступна для скачки.
От: Курилка Россия http://kirya.narod.ru/
Дата: 13.07.09 18:56
Оценка:
Здравствуйте, dimgel, Вы писали:

D>Здравствуйте, Курилка, Вы писали:


К>>Дак не обязательно же всё коммитить в транк


D>А можешь посоветовать, как лучше организовать? У меня нет опыта branche merging. Я пока подумываю что-то вроде следующего:

D>1) в рабочей ветке либа и примеры в одном проекте (до поры до времени, по крайней мере: так удобнее отлаживаться с `mvn jetty:run`);
D>2) созревшая версия копируется в отстойник, где разбивается на два проекта — либа отдельно, примеры отдельно (попутно удаляется всякий хлам);
D>3) вот это вылизанное копируется в так называемый "транк", который в данном случае транком собственно и не является.

D>Или как вариант (если google code такое позволяет) — даю доступ к tags (e.g. tags/0.1.0), в качестве отстойника использую branches (e.g. branches/0.1.0), а шуршу в закрытом trunk (так более по-человечески).


В "средствах разработки" спроси, не велик опыт бранчевания у меня .
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.