io.inversion.Rule Maven / Gradle / Ivy
/*
* c * // * Copyright (c) 2015-2018 Rocket Partners, LLC
* https://github.com/inversion-api
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.inversion;
import io.inversion.context.Context;
import io.inversion.json.JSMap;
import io.inversion.utils.Path;
import io.inversion.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
/**
* Matches against an HTTP method and URL path to determine if the object
* should be included when processing the associated Request.
*
* Matching relies heavily on variablized Path matching via {@link Path#matches(String)}
*/
public abstract class Rule implements Comparable {
public static final SortedSet ALL_METHODS = Collections.unmodifiableSortedSet(new TreeSet(Utils.asSet("GET", "POST", "PUT", "PATCH", "DELETE")));
protected final transient Logger log = LoggerFactory.getLogger(getClass().getName());
/**
* Method/path combinations that would cause this Rule to be included in the relevant processing.
*/
protected final List includeMatchers = new ArrayList<>();
/**
* Method/path combinations that would cause this Rule to be excluded from the relevant processing.
*/
protected final List excludeMatchers = new ArrayList<>();
/**
* {@code JSNode} is used because it implements a case insensitive map without modifying the keys
*/
protected final transient JSMap configMap = new JSMap();
/**
* The name used for configuration and debug purposes.
*/
protected String name = null;
/**
* Rules are always processed in sequence sorted by ascending order.
*/
protected int order = 1000;
/**
* An optional querystring that will be applied to every request processed.
* This is useful to force specific params on different endpoints/actions etc.
*/
//protected String query = null;
protected String includeOn = null;
protected String excludeOn = null;
protected String description = null;
protected List params = new ArrayList();
transient boolean lazyConfiged = false;
static List asPathsList(String... paths) {
List pathsList = new ArrayList<>();
for (String path : Utils.explode(",", paths)) {
pathsList.add(new Path(path));
}
if (pathsList.size() == 0)
pathsList.add(new Path("*"));
return pathsList;
}
static Path[] asPathsArray(String... paths) {
List list = asPathsList(paths);
return list.toArray(new Path[0]);
}
public void afterWiringComplete(Context context) {
checkLazyConfig();
}
protected void checkLazyConfig() {
//-- reluctant lazy config defaultIncludes if no other
//-- includes/excludes have been configured by the user.
if (!lazyConfiged) {
synchronized (this) {
if (!lazyConfiged) {
lazyConfiged = true;
doLazyConfig();
}
}
}
}
protected void doLazyConfig() {
if (includeOn != null)
withIncludeOn(includeOn);
if (excludeOn != null)
withExcludeOn(excludeOn);
if (includeMatchers.size() == 0 && excludeMatchers.size() == 0) {
List matchers = getDefaultIncludeMatchers();
for (RuleMatcher m : matchers)
withIncludeOn(m);
}
}
/**
* Designed to allow subclasses to provide a default match behavior
* of no configuration was provided by the developer.
*
* @return the default include match "*","*"
*/
protected List getDefaultIncludeMatchers() {
return Utils.asList(new RuleMatcher(null, "*"));
}
/**
* Check if the http method and path match this Rule.
*
* @param method the HTTP method to match
* @param path the concrete path to match
* @return true if the http method and path are included and not excluded
*/
public boolean matches(String method, String path) {
return matches(method, new Path(path));
}
/**
* Check if the http method and path match this Rule.
*
* @param method the HTTP method to match
* @param path the concrete path to match
* @return true if the http method and path are included and not excluded
*/
public boolean matches(String method, Path path) {
return match(method, path) != null;
}
/**
* Find the first ordered Path that satisfies this method/path match.
*
* @param method the HTTP method to match
* @param path the concrete path to match
* @return the first includeMatchers path to match when method also matches, null if no matches or excluded
*/
public Path match(String method, Path path) {
return match(method, path, false);
}
public Path match(String method, Path path, boolean bidirectional) {
checkLazyConfig();
for (RuleMatcher excluder : excludeMatchers) {
if (excluder.methods.size() > 0 && !excluder.methods.contains(method))
continue;
for (Path excludePath : excluder.paths) {
if (excludePath.matches(path, bidirectional)) {
return null;
}
}
}
int includePathCount = 0;
for (RuleMatcher includer : includeMatchers) {
includePathCount += includer.paths.size();
if (includer.methods.size() > 0 && !includer.methods.contains(method))
continue;
for (Path includePath : includer.paths) {
if (includePath.matches(path, bidirectional)) {
return includePath;
}
}
}
//-- path was not excluded but config did not supply any include paths
//-- so this is an implicit * include.
if (includePathCount == 0) {
return new Path("*");
}
return null;
}
public List getAllIncludeMethods() {
checkLazyConfig();
Set methods = new LinkedHashSet();
for (RuleMatcher includer : includeMatchers) {
methods.addAll(includer.methods);
}
return new ArrayList(methods);
}
public List getAllIncludePaths() {
checkLazyConfig();
Set paths = new LinkedHashSet();
for (RuleMatcher includer : includeMatchers) {
paths.addAll(includer.paths);
}
return new ArrayList(paths);
}
public List getAllExcludePaths() {
checkLazyConfig();
Set paths = new LinkedHashSet();
for (RuleMatcher excluder : excludeMatchers) {
paths.addAll(excluder.paths);
}
return new ArrayList(paths);
}
public List getIncludeMatchers() {
checkLazyConfig();
return new ArrayList(includeMatchers);
}
public R withIncludeOn(RuleMatcher matcher) {
includeMatchers.add(matcher);
return (R) this;
}
// /**
// * Select this Rule when any method and path match.
// *
// * @param methods or more comma separated http method names, can be null to match on any
// * @param paths each path can be one or more comma separated variableized Paths
// * @return this
// */
// public R withIncludeOn(String methods, String paths) {
// withIncludeOn(new RuleMatcher(methods, paths));
// return (R) this;
// }
public R withIncludeOn(String... specs) {
for(int i=0; specs != null && i < specs.length; i++)
withIncludeOn(new RuleMatcher(specs[i]));
return (R) this;
}
// /**
// * Don't select this Rule when any method and path match.
// *
// * @param methods or more comma separated http method names, can be null to match on any
// * @param paths each path can be one or more comma separated variableized Paths
// * @return this
// */
// public R withExcludeOn(String methods, String paths) {
// withExcludeOn(new RuleMatcher(methods, paths));
// return (R) this;
// }
/**
* Don't select this Rule when RuleMatcher matches
*
* @param matcher the method/path combo to exclude
* @return this
*/
public R withExcludeOn(RuleMatcher matcher) {
excludeMatchers.add(matcher);
return (R) this;
}
public R withExcludeOn(String... specs) {
for(int i=0; specs != null && i < specs.length; i++)
withExcludeOn(new RuleMatcher(specs[i]));
return (R) this;
}
public List getExcludeMatchers() {
checkLazyConfig();
return new ArrayList(excludeMatchers);
}
public String getName() {
return name;
}
public R withName(String name) {
this.name = name;
return (R) this;
}
public Rule withDescription(String description) {
this.description = description;
return this;
}
public String getDescription() {
return this.description;
}
public int getOrder() {
return order;
}
public R withOrder(int order) {
this.order = order;
return (R) this;
}
// public R withQuery(String query) {
// this.query = query;
// return (R) this;
// }
//
// public String getQuery() {
// return query;
// }
@Override
public int compareTo(Rule a) {
int compare = Integer.compare(getOrder(), a.getOrder());
return compare;
}
public List getParams() {
return params;
}
public R withParams(List params) {
params.forEach(p -> withParam(p));
return (R) this;
}
public R withParam(Param param) {
if (!params.contains(param)) {
params.add(param);
}
return (R) this;
}
public String toString() {
StringBuilder buff = new StringBuilder(getClass().getSimpleName());
if (name != null) {
buff.append(":").append(name);
}
if (includeMatchers.size() > 0 || excludeMatchers.size() > 0) {
buff.append(" -");
if (includeMatchers.size() > 0)
buff.append(" includes: ").append(includeMatchers);
if (excludeMatchers.size() > 0)
buff.append(" exclude: ").append(excludeMatchers);
}
return buff.toString();
}
// static List parseRuleMatcher(String methodsAndOrPaths) {
// List matchers = new ArrayList<>();
// String[] parts = methodsAndOrPaths.split("\\|");
// for (int i = 0; i < parts.length; i++) {
// if (parts.length - i == 1) {
// //there is not another matched pair so parse both methods and paths from this single string
//
// List methodsList = new ArrayList();
// List pathsList = new ArrayList();
//
// for (String part : parts[i].split(",")) {
// part = part.trim();
// if (Utils.in(part.toLowerCase(), "get", "post", "put", "patch", "delete"))
// methodsList.add(part);
// else
// pathsList.add(part);
// }
//
// String methods = methodsList.size() == 0 ? "*" : Utils.implode(",", methodsList);
// String paths = pathsList.size() == 0 ? "*" : Utils.implode(",", pathsList);
// matchers.add(new RuleMatcher(methods, paths));
// } else {
// matchers.add(new RuleMatcher(parts[i], parts[i + 1]));
// i++;
// }
// }
// return matchers;
// }
public static class RuleMatcher {
protected final TreeSet methods = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
protected final LinkedHashSet paths = new LinkedHashSet<>();
public RuleMatcher() {
}
public RuleMatcher(String spec) {
parse(this, spec);
}
public static void parse(RuleMatcher matcher, String spec) {
if (spec == null)
return;
spec = spec.trim();
List parts = Utils.explode(",", spec);
for (String part : parts) {
if (ALL_METHODS.contains(part.toUpperCase()))
matcher.withMethods(part);
else
matcher.withPaths(new Path(part));
}
}
public String toString() {
return Utils.implode(",", methods, paths);
}
public int hashCode() {
return toString().toLowerCase().hashCode();
}
@Override
public boolean equals(Object o) {
return o instanceof RuleMatcher && Utils.equal(this.toString(), o.toString());
}
public RuleMatcher(String methods, String... paths) {
this(methods, asPathsList(paths));
}
public RuleMatcher(String methods, Path path) {
withMethods(methods);
withPaths(path);
}
public RuleMatcher(String methods, List paths) {
withMethods(methods);
withPaths(paths);
}
public RuleMatcher clearPaths() {
paths.clear();
return this;
}
public RuleMatcher clearMethods() {
methods.clear();
return this;
}
public boolean hasMethod(String method) {
return methods.size() == 0 || methods.contains(method);
}
public void withMethods(String... methods) {
for (String method : Utils.explode(",", methods)) {
if (method.equals("*"))
continue;
method = method.toUpperCase();
if (ALL_METHODS.contains(method)) {
this.methods.add(method);
}
}
}
public RuleMatcher withPaths(Path... paths) {
for (int i = 0; paths != null && i < paths.length; i++) {
if (paths[i] != null)
this.paths.add(paths[i]);
}
return this;
}
public RuleMatcher withPaths(List paths) {
for (Path p : paths)
withPaths(p);
return this;
}
public SortedSet getMethods() {
if (methods.size() == 0)
return ALL_METHODS;
return Collections.unmodifiableSortedSet(methods);
}
public LinkedHashSet getPaths() {
if (this.paths.size() == 0) {
return Utils.add(new LinkedHashSet(), new Path("*"));
}
return new LinkedHashSet(paths);
}
}
}