جاوا و تکنولوژی های آن

java programming language

در این وبلاگ به بررسی نکات موجود در جاوا و تکنولوژی های آن می پردازیم

طبقه بندی موضوعی


FIQL یا Feed Item Query Language یک زبان ساختارمند، منعطف، URI friendly برای ساخت عبارات کوئری های سرچ و فیلتر است که توسط کتابخانه RSQL میتوانیم از آن در REST API استفاده کنیم 


ابتدا کتابخانه آنرا به پروژه اضافه میکنیم :
<dependency>
    <groupId>cz.jirutka.rsql</groupId>
    <artifactId>rsql-parser</artifactId>
    <version>2.0.0</version>
</dependency>


یک Entity تعریف میکنیم که بعدا قرار است در مثال ها از آن استفاده کنیم :

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
  
    private String firstName;
    private String lastName;
    private String email;
  
    private int age;
}



Parse کردن دستورات رسیده از کلاینت :


عبارات RSQL باید توسط مکانیزمی پردازش شوند برای این منظور میتوانیم از اینترفیس RSQLVisitor  استفاده کنیم 

public class CustomRsqlVisitor<T> implements RSQLVisitor<Specification<T>, Void> {
 
    private GenericRsqlSpecBuilder<T> builder;
 
    public CustomRsqlVisitor() {
        builder = new GenericRsqlSpecBuilder<T>();
    }
 
    @Override
    public Specification<T> visit(AndNode node, Void param) {
        return builder.createSpecification(node);
    }
 
    @Override
    public Specification<T> visit(OrNode node, Void param) {
        return builder.createSpecification(node);
    }
 
    @Override
    public Specification<T> visit(ComparisonNode node, Void params) {
        return builder.createSecification(node);
    }
}

حالا نیاز داریم که بتوانیم با دیتابیس هم تعامل داشته باشیم از اینرو از Spring Data JPA Specification استفاده میکنیم و یک Specification Builder میسازیم :

public class GenericRsqlSpecBuilder<T> {
 
    public Specification<T> createSpecification(Node node) {
        if (node instanceof LogicalNode) {
            return createSpecification((LogicalNode) node);
        }
        if (node instanceof ComparisonNode) {
            return createSpecification((ComparisonNode) node);
        }
        return null;
    }
 
    public Specification<T> createSpecification(LogicalNode logicalNode) {        
        List<Specification> specs = logicalNode.getChildren()
          .stream()
          .map(node -> createSpecification(node))
          .filter(Objects::nonNull)
          .collect(Collectors.toList());
 
        Specification<T> result = specs.get(0);
        if (logicalNode.getOperator() == LogicalOperator.AND) {
            for (int i = 1; i < specs.size(); i++) {
                result = Specification.where(result).and(specs.get(i));
            }
        } else if (logicalNode.getOperator() == LogicalOperator.OR) {
            for (int i = 1; i < specs.size(); i++) {
                result = Specification.where(result).or(specs.get(i));
            }
        }
 
        return result;
    }
 
    public Specification<T> createSpecification(ComparisonNode comparisonNode) {
        Specification<T> result = Specification.where(
          new GenericRsqlSpecification<T>(
            comparisonNode.getSelector(), 
            comparisonNode.getOperator(), 
            comparisonNode.getArguments()
          )
        );
        return result;
    }
}


نکته ای که وجود دارد :

- LogicalNode شامل نود های And یا Or هستند که دارای فرزندانی نیز میباشند 

- ComparisonNode فرزندی ندارد و در بر گیرنده Selector ها ، Operator ها و Argument ها میباشد


برای مثال در کوئری “name==john” :


name یک Selector است

== یک Operator است 

john یک Argument است 


Specification سفارشی :

موقع ساخت کوئری میتوانیم از کلاس زیر استفاده کنیم :

public class GenericRsqlSpecification<T> implements Specification<T> {
 
    private String property;
    private ComparisonOperator operator;
    private List<String> arguments;
 
    @Override
    public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
        List<Object> args = castArguments(root);
        Object argument = args.get(0);
        switch (RsqlSearchOperation.getSimpleOperator(operator)) {
 
        case EQUAL: {
            if (argument instanceof String) {
                return builder.like(root.get(property), argument.toString().replace('*', '%'));
            } else if (argument == null) {
                return builder.isNull(root.get(property));
            } else {
                return builder.equal(root.get(property), argument);
            }
        }
        case NOT_EQUAL: {
            if (argument instanceof String) {
                return builder.notLike(root.<String> get(property), argument.toString().replace('*', '%'));
            } else if (argument == null) {
                return builder.isNotNull(root.get(property));
            } else {
                return builder.notEqual(root.get(property), argument);
            }
        }
        case GREATER_THAN: {
            return builder.greaterThan(root.<String> get(property), argument.toString());
        }
        case GREATER_THAN_OR_EQUAL: {
            return builder.greaterThanOrEqualTo(root.<String> get(property), argument.toString());
        }
        case LESS_THAN: {
            return builder.lessThan(root.<String> get(property), argument.toString());
        }
        case LESS_THAN_OR_EQUAL: {
            return builder.lessThanOrEqualTo(root.<String> get(property), argument.toString());
        }
        case IN:
            return root.get(property).in(args);
        case NOT_IN:
            return builder.not(root.get(property).in(args));
        }
 
        return null;
    }
 
    private List<Object> castArguments(final Root<T> root) {
         
        Class<? extends Object> type = root.get(property).getJavaType();
         
        List<Object> args = arguments.stream().map(arg -> {
            if (type.equals(Integer.class)) {
               return Integer.parseInt(arg);
            } else if (type.equals(Long.class)) {
               return Long.parseLong(arg);
            } else {
                return arg;
            }            
        }).collect(Collectors.toList());
 
        return args;
    }
 
    // standard constructor, getter, setter
}


توجه کنید که در کد بالا از Generics استفاده شده است که باعث شده وابسته به عامل دیگری مانند User نباشد و عملیات در RsqlSearchOperation بصورت Enum تعریف شده است که در قبل مشاهده کردیم 


public enum RsqlSearchOperation {
    EQUAL(RSQLOperators.EQUAL), 
    NOT_EQUAL(RSQLOperators.NOT_EQUAL), 
    GREATER_THAN(RSQLOperators.GREATER_THAN), 
    GREATER_THAN_OR_EQUAL(RSQLOperators.GREATER_THAN_OR_EQUAL), 
    LESS_THAN(RSQLOperators.LESS_THAN), 
    LESS_THAN_OR_EQUAL(RSQLOperators.LESS_THAN_OR_EQUAL), 
    IN(RSQLOperators.IN), 
    NOT_IN(RSQLOperators.NOT_IN);
 
    private ComparisonOperator operator;
 
    private RsqlSearchOperation(ComparisonOperator operator) {
        this.operator = operator;
    }
 
    public static RsqlSearchOperation getSimpleOperator(ComparisonOperator operator) {
        for (RsqlSearchOperation operation : values()) {
            if (operation.getOperator() == operator) {
                return operation;
            }
        }
        return null;
    }
}


تست :

قبل از شروع تست چند Entity ذخیره میکنیم :

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { PersistenceConfig.class })
@Transactional
@TransactionConfiguration
public class RsqlTest {
 
    @Autowired
    private UserRepository repository;
 
    private User userJohn;
 
    private User userTom;
 
    @Before
    public void init() {
        userJohn = new User();
        userJohn.setFirstName("john");
        userJohn.setLastName("doe");
        userJohn.setEmail("john@doe.com");
        userJohn.setAge(22);
        repository.save(userJohn);
 
        userTom = new User();
        userTom.setFirstName("tom");
        userTom.setLastName("doe");
        userTom.setEmail("tom@doe.com");
        userTom.setAge(26);
        repository.save(userTom);
    }
}


تست کردن Quality :

@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    Node rootNode = new RSQLParser().parse("firstName==john;lastName==doe");
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    List<User> results = repository.findAll(spec);
 
    assertThat(userJohn, isIn(results));
    assertThat(userTom, not(isIn(results)));
}


تست Negation :

@Test
public void givenFirstNameInverse_whenGettingListOfUsers_thenCorrect() {
    Node rootNode = new RSQLParser().parse("firstName!=john");
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    List<User> results = repository.findAll(spec);
 
    assertThat(userTom, isIn(results));
    assertThat(userJohn, not(isIn(results)));
}


تست Greater Than :

@Test
public void givenMinAge_whenGettingListOfUsers_thenCorrect() {
    Node rootNode = new RSQLParser().parse("age>25");
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    List<User> results = repository.findAll(spec);
 
    assertThat(userTom, isIn(results));
    assertThat(userJohn, not(isIn(results)));
}


تست Like :

@Test
public void givenFirstNamePrefix_whenGettingListOfUsers_thenCorrect() {
    Node rootNode = new RSQLParser().parse("firstName==jo*");
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    List<User> results = repository.findAll(spec);
 
    assertThat(userJohn, isIn(results));
    assertThat(userTom, not(isIn(results)));
}


تست In :

@Test
public void givenListOfFirstName_whenGettingListOfUsers_thenCorrect() {
    Node rootNode = new RSQLParser().parse("firstName=in=(john,jack)");
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    List<User> results = repository.findAll(spec);
 
    assertThat(userJohn, isIn(results));
    assertThat(userTom, not(isIn(results)));
}


ایجاد کلاس Controller :

@RequestMapping(method = RequestMethod.GET, value = "/users")
@ResponseBody
public List<User> findAllByRsql(@RequestParam(value = "search") String search) {
    Node rootNode = new RSQLParser().parse(search);
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    return dao.findAll(spec);
}


درخواست به کنترلر :

http://localhost:8080/users?search=firstName==jo*;age<25


نمونه پاسخ دریافتی کلاینت :

[{
    "id":1,
    "firstName":"john",
    "lastName":"doe",
    "email":"john@doe.com",
    "age":24
}]









نظرات  (۰)

هیچ نظری هنوز ثبت نشده است

ارسال نظر

ارسال نظر آزاد است، اما اگر قبلا در بیان ثبت نام کرده اید می توانید ابتدا وارد شوید.
شما میتوانید از این تگهای html استفاده کنید:
<b> یا <strong>، <em> یا <i>، <u>، <strike> یا <s>، <sup>، <sub>، <blockquote>، <code>، <pre>، <hr>، <br>، <p>، <a href="" title="">، <span style="">، <div align="">
تجدید کد امنیتی