Статьи

Помимо спецификации JAX-RS: расширение поиска Apache CXF

В сегодняшнем посте мы рассмотрим не только  спецификацию JAX-RS 2.0, но  и полезные расширения, которые  Apache CXF , одна из популярных реализаций  JAX-RS 2.0  , предлагает разработчикам  сервисов и API-интерфейсов REST . В частности, мы поговорим о расширении поиска с использованием подмножества  фильтров запросов OData 2.0 .

В двух словах, расширение поиска просто отображает какое-то выражение фильтра на набор совпадающих типизированных объектов (экземпляров классов Java). В  OData 2.0  фильтры запросов могут быть  очень сложными ,  однако на данный момент  Apache CXF  поддерживает только подмножество из них:

оператор Описание пример
уравнение равных городской экв ‘Редмонд’
Небраска Не равный город н ‘Лондон’
GT Лучше чем цена GT 20
GE Больше или равно цена ge 10
л Меньше, чем цена 20 лт
ле Меньше или равно цена ле 100
и Логический и цена ле 200 и цена гт 3,5
или же Логический или цена ле 3,5 или цена гт 200

По сути, для настройки и активации расширения поиска для ваших   служб JAX-RS достаточно определить два свойства:  search.query.parameter.name  и  search.parser , а также один дополнительный поставщик SearchContextProvider :

@Configuration
public class AppConfig {    
    @Bean( destroyMethod = "shutdown" )
    public SpringBus cxf() {
        return new SpringBus();
    }
    
    @Bean @DependsOn( "cxf" )
    public Server jaxRsServer() {
        final Map< String, Object > properties = new HashMap< String, Object >();        
        properties.put( "search.query.parameter.name", "$filter" );
        properties.put( "search.parser", new ODataParser< Person >( Person.class ) );

        final JAXRSServerFactoryBean factory = 
            RuntimeDelegate.getInstance().createEndpoint( 
                jaxRsApiApplication(), 
                JAXRSServerFactoryBean.class 
            );
        factory.setProvider( new SearchContextProvider() );
        factory.setProvider( new JacksonJsonProvider() );
        factory.setServiceBeans( Arrays.< Object >asList( peopleRestService() ) );
        factory.setAddress( factory.getAddress() );      
        factory.setProperties( properties );

        return factory.create();
    }
    
    @Bean 
    public JaxRsApiApplication jaxRsApiApplication() {
        return new JaxRsApiApplication();
    }
    
    @Bean 
    public PeopleRestService peopleRestService() {
        return new PeopleRestService();
    }   
}

В  search.query.parameter.name  определяет то , что было бы имя запрос параметра строки , используемым в качестве фильтра (мы устанавливаем его , чтобы быть  $ фильтра ), в то время как  search.parser  определяет анализатор будет использоваться для разбора выражения фильтра (мы устанавливаем это быть  ODataParser  параметризованным с   классом Person ).  ODataParser  построен на вершине превосходного Apache Olingo  проекта , который в настоящее время реализует  OData 2.0  протокола (поддержку для  OData 4.0  находится на пути).

Как только конфигурация выполнена, любая   служба JAX-RS 2.0 может воспользоваться возможностями поиска, введя контекстный параметр  SearchContext . Давайте посмотрим на это в действии, определив   службу REST для управления людьми, представленными следующим классом  Person :

public class Person {
    private String firstName;
    private String lastName;
    private int age;

    // Setters and getters here
}

В  PeopleRestService  просто позволит создать новое лицо с помощью  HTTP  POST  и выполнить поиск с помощью  HTTP  GET , перечисленный в  / поиске  конечной точки:

package com.example.rs;

import java.util.ArrayList;
import java.util.Collection;

import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;

import org.apache.cxf.jaxrs.ext.search.SearchCondition;
import org.apache.cxf.jaxrs.ext.search.SearchContext;

import com.example.model.Person;

@Path( "/people" ) 
public class PeopleRestService {
    private final Collection< Person > people = new ArrayList<>();
       
    @Produces( { MediaType.APPLICATION_JSON  } )
    @POST
    public Response addPerson( @Context final UriInfo uriInfo,
            @FormParam( "firstName" ) final String firstName, 
            @FormParam( "lastName" ) final String lastName,
            @FormParam( "age" ) final int age ) {      
        
        final Person person = new Person( firstName, lastName, age );
        people.add( person );
        
        return Response
            .created( uriInfo.getRequestUriBuilder().path( "/search" )
            .queryParam( "$filter=firstName eq '{firstName}' and lastName eq '{lastName}' and age eq {age}" )
            .build( firstName, lastName, age ) )
            .entity( person ).build();
    }
    
    @GET
    @Path("/search")
    @Produces( { MediaType.APPLICATION_JSON  } )
    public Collection< Person > findPeople( @Context SearchContext searchContext ) {        
        final SearchCondition< Person > filter = searchContext.getCondition( Person.class );
        return filter.findAll( people );
    }
}

Метод  findPeople  — это тот, который мы ищем. Благодаря всем сложностям, которые   делает Apache CXF , метод выглядит очень простым:  SearchContext  внедряется, а выражение фильтра автоматически выбирается из   параметра строки запроса $ filter . Последняя часть заключается в применении фильтра к данным, который в нашем случае является просто набором  людей . Очень чистый и понятный.

Давайте построим проект и запустим его:

mvn clean package
java -jar target/cxf-search-extension-0.0.1-SNAPSHOT.jar

Используя удивительный   инструмент curl , давайте выполним пару   запросов HTTP  POST, чтобы сгенерировать некоторые данные для выполнения запросов фильтра:

> curl http://localhost:8080/rest/api/people -X POST -d "firstName=Tom&lastName=Knocker&age=16"
{
    "firstName": "Tom",
    "lastName": "Knocker",
    "age": 16
}

> curl http://localhost:8080/rest/api/people -X POST -d "firstName=Bob&lastName=Bobber&age=23"
{
    "firstName": "Bob",
    "lastName": "Bobber",
    "age": 23
}

> curl http://localhost:8080/rest/api/people -X POST -d "firstName=Tim&lastName=Smith&age=50"
{
    "firstName": "Tim",
    "lastName": "Smith",
    "age": 50
}

Имея образцы данных, давайте продолжим и предложим пару различных критериев поиска, достаточно сложных, чтобы продемонстрировать мощь   фильтров запросов OData 2.0 :

  • найти все лицо , чьи  первое имя  является  Боб  ($ = фильтр «ПгвЬЫат эк„Bob“» )
> curl -G -X GET http://localhost:8080/rest/api/people/search --data-urlencode 
  $filter="firstName eq 'Bob'" 
[
    {
        "firstName": "Bob",
        "lastName": "Bobber",
        "age": 23
    }
]
  • найти все лицо , чьи  фамилия  является  Поплавок  или  фамилией  является  Смитом  и  ПгвЬЫат  не  Боб ($ фильтр = «LastName эк„Поплавок“или (LastName э„Smith“и ПгвЬЫат п„Bob“)» )
  • > curl -G -X GET http://localhost:8080/rest/api/people/search --data-urlencode 
      $filter="lastName eq 'Bobber' or (lastName eq 'Smith' and firstName ne 'Bob')" 
    [
        {
            "firstName": "Bob",
            "lastName": "Bobber",
            "age": 23
        },
        {    
            "firstName": "Tim",
            "lastName": "Smith",
            "age": 50
        }
    ]
    
  • найти всех людей, чьи  имена  начинаются с буквы  T,  и которые старше  16  лет ($ filter = «firstName eq ‘T *’ и age ge 16» )
  • > curl -G -X GET http://localhost:8080/rest/api/people/search --data-urlencode 
      $filter="firstName eq 'T*' and age ge 16"
    [
        {
            "firstName": "Tom",
            "lastName": "Knocker",
            "age": 16
        },
        {
            "firstName": "Tim",
            "lastName": "Smith",
            "age": 50
        }
    ]
    

    Примечание : если вы запускаете эти команды в Linux-подобной среде, вам может потребоваться экранировать   знак $, используя  вместо этого \ $ , например:  curl -X GET -G http: // localhost: 8080 / rest / api / people / search —data-urlencode \ $ filter = «firstName eq ‘Bob’»

    На данный момент  Apache CXF  предлагает только базовую поддержку  фильтров запросов OData 2.0 со многими мощными выражениями в стороне. Тем не менее, существует обязательство продвигать его вперед, как только сообщество проявит достаточный интерес к использованию этой функции.

    Стоит отметить, что   фильтры запросов OData 2.0 — не единственная доступная опция. Расширение поиска также поддерживает  FIQL  (язык запросов элементов фида ),  и  эта замечательная статья  от одного из основных  разработчиков Apache CXF является прекрасным введением в него.

    Я думаю, что эта весьма полезная функция  Apache CXF  может сэкономить много вашего времени и усилий, предоставляя простые (и не такие простые) возможности поиска для ваших   служб JAX-RS 2.0 . Пожалуйста, попробуйте, если он соответствует потребностям вашего приложения.

    Полный исходный код проекта доступен на  Github .