org.restheart.hal.metadata.singletons.SimpleContentChecker.java Source code

Java tutorial

Introduction

Here is the source code for org.restheart.hal.metadata.singletons.SimpleContentChecker.java

Source

/*
 * RESTHeart - the data REST API server
 * Copyright (C) SoftInstigate Srl
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.restheart.hal.metadata.singletons;

import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import io.undertow.server.HttpServerExchange;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.restheart.handlers.RequestContext;
import org.restheart.utils.JsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author Andrea Di Cesare <andrea@softinstigate.com>
 *
 * SimpleContentChecker allows to check request content by using json path
 * expression
 *
 * the args arguments is an array of condition. a condition is json object as
 * follows: { "path": "PATHEXPR", [ "type": "TYPE]"] ["count": COUNT ] ["regex":
 * "REGEX"] ["nullable": BOOLEAN]}
 *
 * where
 *
 * <br>PATHEXPR the path expression. use the . notation to identify the property
 * <br>COUNT is the number of expected values
 * <br>TYPE can be any BSON type: null, object, array, string, number, boolean *
 * objectid, date,timestamp, maxkey, minkey, symbol, code, objectid
 * <br>REGEX regular expression. note that string values to match come enclosed
 * in quotation marks, i.e. the regex will need to match "the value", included
 * the quotation marks
 *
 * <br>examples for path expressions:
 *
 * <br>root = {a: {b:1, c: {d:2, e:3}}, f:4}
 * <br> $.a -> {b:1, c: {d:2, e:3}}, f:4}
 * <br> $.* -> {a: {b:1, c: {d:2, e:3}}}, {f:4}
 * <br> $.a.b -> 1
 * <br> $.a.c -> {d:2,e:3}
 * <br> $.a.c.d -> 2
 *
 * <br>root = {a: [{b:1}, {c:2,d:3}}, true]}
 *
 * <br> $.a -> [{b:1}, {c:2,d:3}, true]
 * <br> $.a.[*] -> {b:1}, {c:2,d:3}, true
 * <br> $.a.[*].c -> null, 2, null
 *
 *
 * <br>root = {a: [{b:1}, {b:2}, {b:3}]}"
 *
 * <br> $.*.a.[*].b -> [1,2,3]
 *
 * <br>example regex condition that matches email addresses:
 *
 * <br>{"path":"$._id", "regex":
 * "^\"[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}\"$"}
 * <br>with unicode escapes (used by httpie): {"path":"$._id", "regex":
 * "^\\u0022[A-Z0-9._%+-]+@[A-Z0-9.-]+\\u005C\\u005C.[A-Z]{2,6}\\u0022$"}
 *
 */
public class SimpleContentChecker implements Checker {
    static final Logger LOGGER = LoggerFactory.getLogger(SimpleContentChecker.class);

    @Override
    public boolean check(HttpServerExchange exchange, RequestContext context, DBObject args) {
        if (args instanceof BasicDBList) {
            BasicDBList conditions = getApplicableConditions((BasicDBList) args, context.getMethod(),
                    context.getContent());

            return conditions.stream().allMatch(_condition -> {
                if (_condition instanceof BasicDBObject) {
                    BasicDBObject condition = (BasicDBObject) _condition;

                    String path = null;
                    Object _path = condition.get("path");

                    if (_path != null && _path instanceof String) {
                        path = (String) _path;
                    }

                    String type = null;
                    Object _type = condition.get("type");

                    if (_type != null && _type instanceof String) {
                        type = (String) _type;
                    }

                    Set<Integer> counts = new HashSet<>();
                    Object _count = condition.get("count");

                    if (_count != null) {
                        if (_count instanceof Integer) {
                            counts.add((Integer) _count);
                        } else if (_count instanceof BasicDBList) {
                            BasicDBList countsArray = (BasicDBList) _count;

                            countsArray.forEach(countElement -> {
                                if (countElement instanceof Integer) {
                                    counts.add((Integer) countElement);
                                }
                            });
                        }
                    }

                    Set<String> mandatoryFields;
                    Object _mandatoryFields = condition.get("mandatoryFields");

                    if (_mandatoryFields != null) {
                        mandatoryFields = new HashSet<>();

                        if (_mandatoryFields instanceof BasicDBList) {
                            BasicDBList mandatoryFieldsArray = (BasicDBList) _mandatoryFields;

                            mandatoryFieldsArray.forEach(element -> {
                                if (element instanceof String) {
                                    mandatoryFields.add((String) element);
                                }
                            });
                        }
                    } else {
                        mandatoryFields = null;
                    }

                    Set<String> optionalFields;
                    Object _optionalFields = condition.get("optionalFields");

                    if (_optionalFields != null) {
                        optionalFields = new HashSet<>();

                        if (_optionalFields instanceof BasicDBList) {
                            BasicDBList optionalFieldsArray = (BasicDBList) _optionalFields;

                            optionalFieldsArray.forEach(element -> {
                                if (element instanceof String) {
                                    optionalFields.add((String) element);
                                }
                            });
                        }
                    } else {
                        optionalFields = null;
                    }

                    String regex = null;
                    Object _regex = condition.get("regex");

                    if (_regex != null && _regex instanceof String) {
                        regex = (String) _regex;
                    }

                    Boolean optional = false;
                    Object _optional = condition.get("optional");

                    if (_optional != null && _optional instanceof Boolean) {
                        optional = (Boolean) _optional;
                    }

                    Boolean nullable = false;
                    Object _nullable = condition.get("nullable");

                    if (_nullable != null && _nullable instanceof Boolean) {
                        nullable = (Boolean) _nullable;
                    }

                    if (counts.isEmpty() && type == null && regex == null) {
                        context.addWarning(
                                "condition does not have any of 'count', 'type' and 'regex' properties, specify at least one: "
                                        + _condition);
                        return true;
                    }

                    if (path == null) {
                        context.addWarning(
                                "condition in the args list does not have the 'path' property: " + _condition);
                        return true;
                    }

                    if (type != null && !counts.isEmpty() && regex != null) {
                        return checkCount(context.getContent(), path, counts, context)
                                && checkType(context.getContent(), path, type, mandatoryFields, optionalFields,
                                        optional, nullable, context)
                                && checkRegex(context.getContent(), path, regex, optional, nullable, context);
                    } else if (type != null && !counts.isEmpty()) {
                        return checkCount(context.getContent(), path, counts, context)
                                && checkType(context.getContent(), path, type, mandatoryFields, optionalFields,
                                        optional, nullable, context);
                    } else if (type != null && regex != null) {
                        return checkType(context.getContent(), path, type, mandatoryFields, optionalFields,
                                optional, nullable, context)
                                && checkRegex(context.getContent(), path, regex, optional, nullable, context);
                    } else if (!counts.isEmpty() && regex != null) {
                        return checkCount(context.getContent(), path, counts, context)
                                && checkRegex(context.getContent(), path, regex, optional, nullable, context);
                    } else if (type != null) {
                        return checkType(context.getContent(), path, type, mandatoryFields, optionalFields,
                                optional, nullable, context);
                    } else if (!counts.isEmpty()) {
                        return checkCount(context.getContent(), path, counts, context);
                    } else if (regex != null) {
                        return checkRegex(context.getContent(), path, regex, optional, nullable, context);
                    }

                    return true;
                } else {
                    context.addWarning("property in the args list is not an object: " + _condition);
                    return true;
                }
            });
        } else {
            context.addWarning(
                    "checker wrong definition: args property must be an arrary of string property names.");
            return true;
        }
    }

    private BasicDBList filterNullableAndOptionalNullConditions(BasicDBList conditions, DBObject content) {
        // nullPaths contains all paths that result to null and condition is nullable or optional
        Set<String> nullPaths = new HashSet<>();

        BasicDBList ret = new BasicDBList();

        conditions.stream().forEach(condition -> {
            if (condition instanceof BasicDBObject) {
                Boolean nullable = false;
                Object _nullable = ((BasicDBObject) condition).get("nullable");

                if (_nullable != null && _nullable instanceof Boolean) {
                    nullable = (Boolean) _nullable;
                }

                Boolean optional = false;
                Object _optional = ((BasicDBObject) condition).get("optional");

                if (_optional != null && _optional instanceof Boolean) {
                    optional = (Boolean) _optional;
                }

                if (nullable || optional) {
                    Object _path = ((BasicDBObject) condition).get("path");

                    if (_path != null && _path instanceof String) {
                        String path = (String) _path;

                        List<Optional<Object>> props;
                        try {
                            props = JsonUtils.getPropsFromPath(content, path);

                            if (props == null) {
                                nullPaths.add(path);
                            }
                        } catch (IllegalArgumentException ex) {
                            nullPaths.add(path);
                        }
                    }
                }
            }
        });

        conditions.stream().forEach(condition -> {
            if (condition instanceof BasicDBObject) {
                Object _path = ((BasicDBObject) condition).get("path");

                if (_path != null && _path instanceof String) {
                    String path = (String) _path;

                    boolean hasNullParent = nullPaths.stream().anyMatch(nullPath -> {
                        LOGGER.debug("does {} implies {}? {}", nullPath, path, path.startsWith(nullPath));
                        return path.startsWith(nullPath);
                    });

                    if (!hasNullParent) {
                        ret.add(condition);
                    }
                }
            }
        });

        return ret;
    }

    private BasicDBList getApplicableConditions(BasicDBList conditions, RequestContext.METHOD method,
            DBObject content) {
        if (method == RequestContext.METHOD.POST || method == RequestContext.METHOD.PUT) {
            return filterNullableAndOptionalNullConditions(conditions, content);
        } else if (method == RequestContext.METHOD.PATCH) {
            List filtered = conditions.stream().filter(condition -> {
                if (!(condition instanceof BasicDBObject)) {
                    return false;
                }

                BasicDBObject _condition = (BasicDBObject) condition;

                String path = null;
                Object _path = _condition.get("path");

                if (_path != null && _path instanceof String) {
                    path = (String) _path;
                }

                if (path == null) {
                    return false;
                }

                Object _count = _condition.get("count");

                if (_count != null) {
                    if (path.equals("$") || path.equals("$.*")) {
                        return false;
                    } else {
                        try {
                            List<Optional<Object>> matches = JsonUtils.getPropsFromPath(content, path);

                            if (matches == null || matches.isEmpty()) {
                                return false;
                            }

                            return !(matches.size() == 1 && matches.get(0) == null);
                        } catch (IllegalArgumentException ex) {
                            return false;
                        }
                    }
                } else {
                    try {
                        List<Optional<Object>> matches = JsonUtils.getPropsFromPath(content, path);

                        if (matches == null || matches.isEmpty()) {
                            return false;
                        }

                        return !(matches.size() == 1 && matches.get(0) == null);
                    } catch (IllegalArgumentException ex) {
                        return false;
                    }
                }
            }).collect(Collectors.toList());

            BasicDBList ret = new BasicDBList();

            filtered.stream().forEach((fc) -> {
                ret.add(fc);
            });

            return filterNullableAndOptionalNullConditions(ret, content);

        } else {
            return new BasicDBList();
        }
    }

    private boolean checkCount(DBObject json, String path, Set<Integer> expectedCounts, RequestContext context) {
        Integer count;
        try {
            count = JsonUtils.countPropsFromPath(json, path);
        } catch (IllegalArgumentException ex) {
            return false;
        }

        // props is null when path does not exist. count is false
        if (count == null) {
            return false;
        }

        boolean ret = expectedCounts.contains(count);

        LOGGER.debug("checkCount({}, {}) -> {}", path, expectedCounts, ret);

        if (ret == false) {
            context.addWarning("checkCount condition failed: path: " + path + ", expected: " + expectedCounts
                    + ", got: " + count);
        }

        return ret;
    }

    private boolean checkType(DBObject json, String path, String type, Set<String> mandatoryFields,
            Set<String> optionalFields, boolean optional, boolean nullable, RequestContext context) {
        BasicDBObject _json = (BasicDBObject) json;

        List<Optional<Object>> props;

        try {
            props = JsonUtils.getPropsFromPath(_json, path);
        } catch (IllegalArgumentException ex) {
            return false;
        }

        // props is null when path does not exist.
        if (props == null) {
            return optional;
        }

        boolean ret;

        ret = props.stream().allMatch((Optional<Object> prop) -> {
            if (prop == null) {
                return optional;
            }

            if (prop.isPresent()) {
                return JsonUtils.checkType(prop, type);
            } else {
                return nullable;
            }
        });

        boolean failedFieldsCheck = false;

        // check object fields
        if (ret && "object".equals(type) && (mandatoryFields != null || optionalFields != null)) {
            Set<String> allFields = new HashSet<>();

            if (mandatoryFields != null) {
                allFields.addAll(mandatoryFields);
            }

            if (optionalFields != null) {
                allFields.addAll(optionalFields);
            }

            ret = props.stream().allMatch((Optional<Object> prop) -> {
                if (prop == null) {
                    return optional;
                }

                if (prop.isPresent()) {
                    BasicDBObject obj = (BasicDBObject) prop.get();

                    if (mandatoryFields != null) {
                        return obj.keySet().containsAll(mandatoryFields) && allFields.containsAll(obj.keySet());
                    } else {
                        return allFields.containsAll(obj.keySet());
                    }
                } else {
                    return nullable;
                }
            });

            if (ret == false) {
                failedFieldsCheck = true;
            }
        }

        LOGGER.debug("checkType({}, {}, {}, {}) -> {} -> {}", path, type, mandatoryFields, optionalFields, props,
                ret);

        if (ret == false) {
            if (!failedFieldsCheck) {
                context.addWarning("checkType condition failed: path: " + path + ", expected type: " + type
                        + ", got: " + props);
            } else {
                context.addWarning("checkType condition failed: path: " + path + ", mandatory fields: "
                        + mandatoryFields + ", optional fields: " + optionalFields + ", got: " + props);
            }
        }

        return ret;
    }

    private boolean checkRegex(DBObject json, String path, String regex, boolean optional, boolean nullable,
            RequestContext context) {
        BasicDBObject _json = (BasicDBObject) json;

        List<Optional<Object>> props;
        try {
            props = JsonUtils.getPropsFromPath(_json, path);
        } catch (IllegalArgumentException ex) {
            return false;
        }

        // props is null when path does not exist.
        if (props == null) {
            return optional;
        }

        boolean ret;

        Pattern p = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);

        ret = props.stream().allMatch((Optional<Object> prop) -> {
            if (prop == null) {
                return optional;
            }

            if (prop.isPresent()) {
                return p.matcher(JsonUtils.serialize(prop.get())).find();
            } else {
                return nullable;
            }
        });

        LOGGER.debug("checkRegex({}, {}) -> {} -> {}", path, regex, props, ret);

        if (ret == false) {
            context.addWarning(
                    "checkRegex condition failed: path: " + path + ", regex: " + regex + ", got: " + props);
        }

        return ret;
    }
}