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:
- The
whenandthenclause are mandatory and they can have either ofandororbut not both. - Both
andandorwill have request properties likepathqueryheadersetc. - Based on the provided clause, my code will check the
whenclause and only if its true it will check forthenclause, if then is also true the filter chain will proceed if not then action mentioned in theorElseclause 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?