Блог

    Caché (Кашэ́) — промышленная высокопроизводительная, объектная система управления базами данных, интегрированная с технологией разработки веб-приложений. Единая архитектура данных Caché позволяет разработчикам использовать одновременно объектный, реляционный (SQL) и прямой (NoSQL) доступ к одним и тем же данным, хранение которых обеспечивается ориентированным на транзакции многомерным ядром СУБД.

    http://www.intersystems.ru/cache/

Последние записи


Теги

Информация

$(REST - CSP - (-HyperEvents) + EasyUI + File Upload). Часть 2

добавлено: 08 июл 15
понравилось:0
просмотров: 871
комментов: 0

теги:

Автор: servit

Мы остановились на статике. Продолжим..

Как мы видим, url для нашей статики приобретает следующий вид:

/rest/что-то/там/такое/разное

Сразу может возникнуть соблазн внести в карту путей все возможные файлы, которые могут понадобиться, но это неправильно и мы не будем так делать.
Вспомним, что "Url" в карте путей представляет собой регулярное выражение, а значит можно более гибко запрограммировать путь следования для нашего обработчика статики.

Вариант вида

/:что-то там такое разное

не подходит, поскольку параметр не предусматривает символ "/", как раз который у нас может встречаться неопределённое количество раз.
Остаётся вариант

/(.*)

что означает любой символ, встречающийся 0 и более раз.
Примечание:
Более подробно о регулярных выражениях можно почитать здесь: Regular Expressions.
Исправим наш класс:
XData UrlMap [ XMLNamespace "http://www.intersystems.com/urlmap" ]
{
<
Routes>
  <
Route Url="/(.*)" Method="GET" Call="StaticFiles"/>
  <
Route Url="/" Method="GET" Call="MainPage"/>
</
Routes>
}

ClassMethod StaticFiles(urlAs %Status
{
  
// пока ничего полезного не делаем
  
q $$$OK
}
Обновляем страничку и Упс!

Куда-то исчезла наша кнопка и ошибок вроде нет..

А всё потому, что первый же Url в нашей карте путей удовлетворяет всем возможным путям: не только настоящим, но и будущим, а потому абсолютно всё будет обрабатываться методом StaticFiles.
Поэтому в документации на этот счёт есть предупреждение:
Документация
Caché compares the incoming REST URL with the Route URL property or the Map Prefix property. It starts at the first Route or Map element and continues to test each following element until it finds a match. Once it finds a match it either sends the incoming call to the call specified in the Route or forwards the URL to the class specified in the Map. In both cases, it ignores any elements in the Routes after the matching element; consequently, the order of the elements in the Routes is significant. If an incoming URL could match multiple elements of the Routes, Caché uses the first matching element and ignores any subsequent possible matches.
Исправим наш класс, поменяв местами методы в карте путей.

Обработка статики


Исследуя заголовки запросов для статики - не только для нашего пока недоделанного приложения, но и других приложений, - мы видим, что нам передаётся куча параметров: и путь, и тип, и дата файла из кеша (HTTP_IF_MODIFIED_SINCE) и мн.др.

На сервере нужно по переданному Url определить какой файл запрашивается, откуда его взять, его тип, размер, кодировку, дату модификации..

Потом на основании всего этого надо сформировать соответствующие заголовки ответа и поместить сам файл в тело ответа.

Как видите, нужно всего ничего. Итак, приступим..

Нет!!! Я лучше просто напишу так:
ClassMethod StaticFiles(urlAs %Status
{
  
// перекладываем всю работу на %CSP.StreamServer, хотя могли и сами
  
%request.Data
  s 
%request.Data("FILE",1)=%request.URL
  
  d 
##class(%CSP.StreamServer).OnPreHTTP()
  
##class(%CSP.StreamServer).OnPage()
  
q $$$OK
}
Кому не хватает стандартной обработки классом %CSP.StreamServer, - например, нужна дополнительная проверка прав доступа на файл, - могут это сделать, ориентируясь на исходники %CSP.StreamServer.
Ok, убеждаемся что теперь статика подгружается без ошибок.

Далее у нас на повестке гиперсобытия.

Гиперсобытия


Как известно, гиперсобытия были в Caché ещё тогда, когда термин Ajax не был распространён, то есть очень давно.
Рассмотрим, что же представляют собой гиперсобытия в рамках СУБД Caché (основные моменты):

1) они позволяют легко и прозрачно вызывать с клиентской стороны (обычно из javascript) методы на сервере (в Caché);
2) они могут вызываться синхронно [#server] или асинхронно [#call];
3) формат передаваемых/получаемых данных - строка, а значит для более сложных типов (например, json), нужно самостоятельно преобразовывать их к строке, а на сервере соответственно - наоборот;
4) возвращать код js для выполнения на клиентской стороне;
5) скрывать название серверного метода, который мы хотим выполнить, чтобы редисканехороший человек не смог подменить и выполнить код, который пожелает.

Теперь рассмотрим, как это всё работает.

Изменим обработчик для нашей кнопки:

  <button onclick="#server(..Add({А:'1 груша',Б:'5 яблок'}))#">Нажми меня</button>

и посмотрим сгенерированный html:

  <button onclick="cspHttpServerMethod('pXZzvVt0xEkSN4SAJoQbFJIfTLt_wD$J2ghItig6wlo-',{А:'1 груша',Б:'5 яблок'})">Нажми меня</button>

Как видим, вызов метода заменяется на вызов клиентской функции cspHttpServerMethod, которой передаются наши параметры, и имя вызываемого метода зашифровано ключом сессии, который никогда не покидает сервер Caché.

Вызов #call аналогичен вызову #server, кроме названия клиентской функции: в данном случае это будет уже cspCallHttpServerMethod.
Данные две функции определены в файле cspxmlhttp.js, который наряду с cspbroker.js используется при разработке веб-приложения через CSP/CLS.

На серверной стороне за обработку гиперсобытий отвечает класс %CSP.Broker, который соответственно дешифрует полное имя метода и вызывает его с переданными параметрами.
Как же можно улучшить гиперсобытия применительно к нашему rest-приложению?

Для начала, нужно определить две клиентские функции - cspHttpServerMethod и cspCallHttpServerMethod, - используя ajax.
Для этого воспользуемся библиотекой jQuery, которую мы ранее уже подключили в наше приложение.

Добавим в класс следующий код:
ClassMethod WriteCommonJS() [ InternalPrivate ]
{
  
&html<<script type="text/javascript">

// наши улучшенные гипер-события. Их имена мы трогать не можем, потому что такими их генерируют директивы #server/#call
function cspHttpServerMethod(method,params)
{
  $.ajax({
    url: method,
    data: params,
    async: false,
    success: 
function(data){alert($.toJSON(data,null,2))}
  });
}

function cspCallHttpServerMethod(method,params)
{
  $.ajax({
    url: method,
    data: params
  });
}

(
function($){
  
  $.ajaxSetup({
    type: 
'POST',
    dataType: 
'json',
    cache: false,
    async: true,
    contentType: 
'application/json; charset=UTF-8',
    processData: false,
    beforeSend: 
function(jqXHR,settings) {
      settings.data
=$.toJSON(settings.data); // перед отсылкой конвертируем json-объект в json-строку
    
},
    statusCode: {
      
// попробуйте поиграться с методами доступа, чтобы увидеть эти ошибки
      
401function() {alert('Вы не авторизованы')},
      
404function() {alert('Страница не найдена')},
      
405function() {alert('Метод запрещён')},
      
500function(jqXHR,textStatus,errorThrown) {alert(errorThrown)}
    },
    error: 
function(jqXHR, textStatus, errorThrown){
      
if (textStatus==='timeout') {alert('Не успели :(')}
    }
  });
  
})(jQuery);
</
script>>
}
Примечание:
Код я вынес в отдельный метод, чтобы не загромождать метод MainPage.
Теперь при вызове csp(Call)HttpServerMethod будет вызываться, используя ajax, любой метод, который мы подадим первым параметром. Причём имя метода уже будет зашифровано.
Данные туда и обратно будем передавать в формате JSON методом POST в теле запроса.
Один метод будет вызываться синхронно, другой - асинхронно.
Примечание:
Полный список доступных параметров и методов функции ajax можно найти в документации jQuery: jQuery.ajax. Ещё на youtube есть много уроков на русском по jQuery и Javascript.
Теперь нужно вызвать наш новый метод WriteCommonJS. Проще всего это сделать, используя однострочную директиву #[]#:
...
  <script type="text/javascript" src="easyui/js/jquery.fileupload.js"></script>#[..WriteCommonJS()]#
...
Также не забудем про сам метод Add:
ClassMethod Add(ByRef args As %ZEN.proxyObjectAs %Status
{
  
r=args.А args.Б
  d 
args.%Clear()
  
args.Результат=r
  
q $$$OK
}
Здесь мы получаем по ссылке переданный нам json-объект (в Caché это будет объект класса %ZEN.proxyObject) и в нём же вернём результат в каком-нибудь поле.

Сейчас, если нажать на кнопку, мы получим ошибку "405 / Метод запрещён". Это происходит потому, что мы не добавили в карту путей обработчик наших гиперсобытий и поэтому вызывается обработчик для статики.
Но он ожидает метод GET, а мы, если помните, вызываем гиперсобытия через POST. Отсюда и ошибка.

Обновим карту путей:
XData UrlMap [ XMLNamespace "http://www.intersystems.com/urlmap" ]
{
<
Routes>
  <
Route Url="/:hyperevent" Method="POST" Call="RestHyperEvent"/>
  <
Route Url="/" Method="GET" Call="MainPage"/>
  <
Route Url="/(.*)" Method="GET" Call="StaticFiles"/>
</
Routes>
}
Наш полный Url вызова гиперсобытия будет выглядеть примерно так:

hттp://localhost:57772/rest/pXZzvVt0xEkSN4SAJoQbFJIfTLt_wD$J2ghItig6wlo-

Для разных сессий и/или разных методов последняя часть будет варьироваться.
Примечание:
Поскольку имя метода шифруется и специальным образом кодируется, то в нём не может быть символов "/", поэтому такой способ указания пути вполне подходит.
И добавим сам обработчик:
ClassMethod RestHyperEvent(hypereventAs %Status FinalInternal ]
{
  
#dim %request As %CSP.Request
  
#dim params As %ZEN.proxyObject
  
#dim ex As %Exception.AbstractException
  
  
q:hyperevent="" $$$ERROR($$$CSPBadBrokerRequest)
  
q:%session.NewSession $$$ERROR($$$CSPSessionTimeout)
  
try{
    
hyperevent=$lg($zcvt(..Decrypt(hyperevent),"I","UTF8"))
  
}catch{
    
return $$$ERR($$$InvalidDecrypt)
  
}
  
q:hyperevent="" $$$ERR($$$CSPIllegalRequest)
  
find=$find(hyperevent,":")
  
s:find hyperevent=$e(hyperevent,1,find-2)
  
cls=$p(hyperevent,".",1,$l(hyperevent,".")-1),method=$e(hyperevent,$l(cls)+2,*)
  
q:cls="" $$$ERR($$$ClassNameRequired)
  
q:method="" $$$ERROR($$$MethodNameRequired)
  
cls=$$$NormalizeClassname(cls)

  
##class(%ZEN.Auxiliary.jsonProvider).%ConvertJSONToObject(%request.Content,,.params)
  
  
st=$$$OK
  try
{
    
$$$ThrowOnError($classmethod(cls,method,.params))
    
params.%ToJSON(,"aelo")
  
}catch(ex){
    
if ex.Name="<METHOD DOES NOT EXIST>" {
      
st=$$$ERROR($$$MethodDoesNotExist,method)
    
}elseif ex.Name="<CLASS DOES NOT EXIST>" {
      
st=$$$ERROR($$$ClassDoesNotExist,cls)
    
}else{
      
throw
    
}
  }
  
st
}
Этот метод служебный и вами меняться не должен, ну разве только очень осторожно.

Здесь мы расшифровываем имя переданного метода, делаем разные проверки, конвертируем json-строку в объект и вызываем сам метод; после чего пишем в тело ответа результат тоже в формате JSON.

Снова нажмём нашу кнопку и увидим, что результат равен нулю, хотя должен равняться шести.
Если в начале метода A добавить отладочную информацию:
  args.%CopyToArray(.arr)
  
^tmp("arr")=arr
то мы увидим, что к нам приходит белиберда:

USER>zw ^tmp
^tmp("arr","Р"_$c(144))="1 РіС"_$c(128)_"С"_$c(131)_"С"_$c(136)_"Р°"
^tmp("arr","Р"_$c(145))="5 С"_$c(143)_"блок"

Это происходит потому, что тело запроса автоматически (пока) не конвертируется в нужную кодировку.
Важно:
В будущую версию СУБД Caché обещали добавить новый параметр, регулирующий это поведение.
Исправим это. Лучше всего это сделать в callback-методе OnPreDispatch, который вызывается при каждом запросе, поэтому не стоит перегружать его множеством тяжёлых операций:
ClassMethod OnPreDispatch(
  
pUrl As %String,
  
pMethod As %String,
  
ByRef pContinue As %BooleanAs %Status
{
  
  
#dim %request As %CSP.Request

  
If $IsObject(%request.Content),$zcvt(%request.Content.ContentType,"L")["charset=utf-8" {
    
Set contentTmp=%request.Content.%ConstructClone(-1)
    
Set contentTmp.Attributes("CharEncoding")="UTF8"

    
Merge attr=%request.Content.Attributes
    Do 
%request.Content.Clear()
    
Merge %request.Content.Attributes=attr
    
    
Do %request.Content.CopyFrom(##class(%IO.MetaCharacterStream).%New(contentTmp))
  
}
  
  
Quit $$$OK
}
Теперь всё хорошо:

USER>zw ^tmp
^tmp("arr","А")="1 груша"
^tmp("arr","Б")="5 яблок"

и на страничке выводится 6.

В следующей части у нас по программе интеграция с jQuery File Upload, но сперва..

Комментарии




Необходимо войти на сайт, чтобы оставлять комментарии