2
\$\begingroup\$

I have a spring cloud gateway application with this yml structure for request validation:

        - name: ValidateRequest
          args:
            when:
              and:
                - path:
                    - regex: "^.*/webui;jsessionid=[a-zA-Z0-9]+$"
                      match: negate
                - query:
                      - name: param1
                        regex: "test"
            then:
              or:
                - query:
                    - name: param1
                      regex: "^.*/webui;jsessionid=[a-zA-Z0-9]+$"
                - query:
                    - name: param2
                      regex: "^.*/webui;jsessionid=[a-zA-Z0-9]+$"
            orElse:
              action: 302
              location: "/path/to"

The yml property will be configured as follows:

  1. The when and then clause are mandatory and they can have either of and or or but not both.
  2. Both and and or will have request properties like path query headers etc.
  3. Based on the provided clause, my code will check the when clause and only if its true it will check for then clause, if then is also true the filter chain will proceed if not then action mentioned in the orElse clause will be performed.

In my filter I have defined Bean to bind the properties from yml as follows:

@Data
public static class When{
    List<And> and;
    List<Or> or;

}

@Data
public static class Then{
    List<And> and;
    List<Or> or;

}

@Data
public static class OrElse{
    int action;
    String location;
}

@Data
public static class And{
    List<Path> path;
    List<Query> query;
}

@Data
public static class Or{
    List<Path> path;
    List<Query> query;
}

@Data
public static class Path {
    String regex;
    String match;
}

@Data
public static class Query {
    private String name;
    private String regex;
    private String match;
}

I have a Config class, where I use these bindings to validate request and perform required action:

@Data
public static class Config{

    When when;
    Then then;
    OrElse orElse;

    
    final Logger logger = LoggerFactory.getLogger(ValidateRequestGatewayFilterFactory.class);

    public boolean evaluateWhen(String path,Map<String,String> queryParam) {
        try {
            if (getWhen().getAnd() != null)
                return evaluateAnd(getWhen().getAnd(), path, queryParam);
            else
                return evaluateOr(getWhen().getOr(), path, queryParam);
        }catch (NullPointerException nullPointerException){
            logger.debug("Evaluating query property in when clause, but request contains no query parameters");
            return false;  //This is done as if when condition is not true, we don't have to check for then/orelse, which means there is no query param to check
        }
    }

    public boolean evaluateThen(String path,Map<String,String> queryParam){
        try {
            if (getThen().getAnd() != null)
                return evaluateAnd(getWhen().getAnd(), path, queryParam);
            else
                return evaluateOr(getThen().getOr(), path, queryParam);
        }catch (NullPointerException nullPointerException){
            logger.debug("Evaluating query property in then clause, but request contains no query parameters");
            return false; //This is done as when condition is valid, then also should be valid, request query param returning null indicates query is not as per config.
        }
    }
    public boolean evaluateAnd(List<And> ands,String path,Map<String,String> queryParam){
        for (And and : ands) {
            if (and.getPath() != null)
                return evaluateAndOfPath(and,path,queryParam);
            else if (and.getQuery() != null)
                return evaluateAndOfQuery(and,path,queryParam);
        }
        return false;
    }

    public boolean evaluateOr(List<Or> ors,String path,Map<String,String> queryParam){
        for(Or or:ors){
            if(or.getPath()!=null)
                return evaluateOrOfPath(or,path,queryParam);
            else if (or.getQuery() != null)
                return evaluateOrOfQuery(or,path,queryParam);
        }
        return false;
    }

    private boolean evaluateAndOfQuery(And and,String path,Map<String,String> queryParam) {
        if (checkAndOfQuery(and.getQuery(),queryParam)) {
            if(and.getPath() != null)
                return checkAndOfPath(and.getPath(),path);
            else
                return true;
        }else
            return false;

    }

    private boolean evaluateOrOfQuery(Or or,String path,Map<String,String> queryParam) {
        if (checkOrOfQuery(or.getQuery(),queryParam))
                return true;
        else if(or.getPath() != null)
            return checkOrOfPath(or.getPath(),path);
        else
            return false;
    }

    private boolean evaluateAndOfPath(And and,String path,Map<String,String> queryParam) {
        if (checkAndOfPath(and.getPath(),path)) {
            if(and.getQuery() != null)
                return checkAndOfQuery(and.getQuery(),queryParam);
            else
                return true;
        }else
            return false;
    }

    private boolean evaluateOrOfPath(Or or,String path,Map<String,String> queryParam) {
        if (checkAndOfPath(or.getPath(),path)) {
            if(or.getQuery() != null)
                return checkAndOfQuery(or.getQuery(),queryParam);
            else
                return true;
        }else
            return false;
    }
    public boolean checkAndOfQuery(List<Query> queries, Map<String,String> queryParam) throws NullPointerException{
        return queries.stream().allMatch(query -> query.getRegex().matches(queryParam.get(query.getName())));
    }

    public boolean checkOrOfQuery(List<Query> queries, Map<String,String> queryParam) throws NullPointerException{
        return queries.stream().anyMatch(query -> query.getRegex().matches(queryParam.get(query.getName())));
    }

    public boolean checkAndOfPath(List<Path> paths, String pathParam){
        if(paths.get(0).getMatch().equals("negate"))                //Request can have only one path
            return paths.stream().noneMatch(path -> path.getRegex().matches(pathParam));
        else
            return paths.stream().allMatch(path -> path.getMatch().matches(pathParam));
    }

    public boolean checkOrOfPath(List<Path> paths, String pathParam){
        if(paths.get(0).getMatch().equals("negate"))
            return paths.stream().noneMatch(path -> path.getRegex().matches(pathParam));
        else
            return paths.stream().anyMatch(path -> path.getMatch().matches(pathParam));
    }
}

Basically, what I am trying to do with my code is based on the configuration in yml, validate weather the request properties are valid according to different clauses and based on the decision either allow it through the filter chain or perform the action as specified in the orElseclause in the config. According to my current code, the yml to bean bindings work fine, and the conditional clauses are also working as expected, but my code is very complicated with lot of repetitions as well as it is not extensible, as in tomorrow if I decide to introduce new property like cookie I would have to right methods to check those as well and modify existing class for it.

I looked up Chain of responsibility which seems the closest pattern for my requirement, but I am not sure how I can refactor my code and also if its even the right pattern. The yml config I have provided will only have the same structure but the inner condtions for path query headers can change. Is there a way to refactor my code?

\$\endgroup\$
3
  • 1
    \$\begingroup\$ Welcome to Code Review! I changed the title so that it describes what the code does per site goals: "State what your code does in your title, not your main concerns about it.". Feel free to edit and give it a different title if there is something more appropriate. \$\endgroup\$ Commented Apr 29, 2024 at 16:43
  • 1
    \$\begingroup\$ Ah, expressing boolean logic with a structured document... haven't we all been there at some point. Well at least it's not XML this time. :) Having the "negate" keyword there to represent "not" operator suggests that it might just be simpler to implement a fully fledged boolean logic language with nested operators and all. \$\endgroup\$ Commented Apr 30, 2024 at 8:37
  • 1
    \$\begingroup\$ BTW, writing a parser for boolean logic expressions like you use in Java isn't too difficult and there's code examples available. If you're going to invent your own configuration language, you could as well go the distance and make it human readable. I mean "programmer readable." \$\endgroup\$ Commented Apr 30, 2024 at 8:42

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.