All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.inversion.Api Maven / Gradle / Ivy

The newest version!
/*
 * 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.utils.Path;
import io.inversion.utils.Task;
import io.inversion.utils.Utils;
import io.inversion.utils.ListMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

/**
 * Contains the Servers, Dbs, Collections, Endpoints and Actions that make up a REST API.
 */
public final class Api {

    protected final transient Logger log = LoggerFactory.getLogger(getClass().getName());

    /**
     * Host and root path config
     */
    protected final List servers = new ArrayList<>();

    /**
     * The underlying data sources for the Api.
     */
    protected final List dbs = new ArrayList<>();

    /**
     * The Request HTTP method/path combinations that map to a distinct set of Actions.
     * 

* A single Endpoint will be selected to run to service a Request. Any additional * Path matching rules that exist on these Endpoint's Actions will be interpreted * as relative to the end of the selected Endpoint's Path match. */ protected final List endpoints = new ArrayList<>(); /** * Actions that may be selected to run regardless of the matched Endpoint. *

* The Action's Path match statements will be considered relative to the Api's * base URL NOT relative to the selected Endpoint. */ protected final List actions = new ArrayList<>(); /** * The data objects being served by this API. In a simple API these may map * one-to-one to, for example, database tables from a JdbcDb connecting to a * RDBMS such as MySql or SqlServer. */ protected final List collections = new ArrayList<>(); /** * Listeners that receive callbacks on startup/shutdown/request/error. */ protected final transient List listeners = new ArrayList<>(); protected transient Linker linker = new Linker(); protected transient List ops = new ArrayList(); protected String name = null; transient protected String hash = null; protected boolean debug = false; protected String url = null; transient volatile boolean started = false; transient volatile boolean starting = false; transient long loadTime = 0; transient Engine engine = null; protected String version = "1"; transient List delayedConfig = new ArrayList(); public Api() { } public Api(String name) { withName(name); } public boolean isStarted() { return started; } synchronized Api startup(Engine engine) { if (this.engine != null && engine != this.engine) { this.engine.removeApi(this); } if (started || starting) //starting is an accidental recursion guard return this; this.engine = engine; starting = true; try { for (Db db : dbs) { db.startup(this); } for (Runnable r : delayedConfig) r.run(); removeExcludes(); configureServers(); configureOps(); started = true; for (ApiListener listener : listeners) { try { listener.onStartup(engine, this); } catch (Exception ex) { log.warn("Error notifying api startup listener: " + listener, ex); } } return this; } finally { starting = false; } } void shutdown(Engine engine) { if (!started) return; started = false; for (Db db : dbs) { db.shutdown(this); } for (ApiListener listener : listeners) { try { listener.onShutdown(engine, this); } catch (Exception ex) { log.warn("Error notifying api shutdown listener: " + listener, ex); } } } public void withDelayedConfig(Runnable r) { if (isStarted()) r.run(); else delayedConfig.add(r); } public void removeExcludes() { for (Db db : getDbs()) { for (Collection coll : db.getCollections()) { if (coll.isExclude()) { db.removeCollection(coll); } else { for (Property col : coll.getProperties()) { if (col.isExclude()) coll.removeProperty(col); } } for (Relationship rel : coll.getRelationships()) { if (rel.isExclude()) { coll.removeRelationship(rel); } } } } } public String getHash() { return hash; } public String toString(){ return "Api: " + name; } public Api withHash(String hash) { this.hash = hash; return this; } public List getServers() { return new ArrayList<>(servers); } public Api withServers(String... urls) { for (String url : urls) { Server server = new Server().withUrls(url); withServer(server); } return this; } public Api withServer(Server server) { if (!servers.contains(server)) { servers.add(server); } return this; } public Api withCollection(Collection coll) { if (coll.isExclude()) return this; if (!collections.contains(coll)) collections.add(coll); return this; } public List getCollections() { return Collections.unmodifiableList(collections); } public Collection getCollection(String name) { for (Collection coll : collections) { if (name.equalsIgnoreCase(coll.getName())) return coll; } return null; } public Db getDb(String name) { if (name == null) return null; for (Db db : dbs) { if (name.equalsIgnoreCase(db.getName())) return db; } return null; } /** * @return the dbs */ public List getDbs() { return new ArrayList<>(dbs); } /** * @param dbs the dbs to set * @return this */ public Api withDbs(Db... dbs) { for (Db db : dbs) withDb(db); return this; } public Api withDbs(List dbs) { for (Db db : dbs) withDb(db); return this; } public Api withDb(Db db) { if (!dbs.contains(db)) { dbs.add(db); for (Collection coll : db.getCollections()) { withCollection(coll); } } return this; } public String getName() { return name; } public Api withName(String name) { this.name = name; return this; } public Api withVersion(String version) { this.version = version; return this; } public String getVersion() { return version; } public long getLoadTime() { return loadTime; } public void setLoadTime(long loadTime) { this.loadTime = loadTime; } public Endpoint getEndpoint(String name) { for (Endpoint ep : endpoints) { if (name.equalsIgnoreCase(ep.getName())) return ep; } return null; } public List getEndpoints() { return new ArrayList<>(endpoints); } public Api removeEndpoint(Endpoint ep) { endpoints.remove(ep); return this; } public Api withEndpoint(Action action1, Action... actions) { Endpoint endpoint = new Endpoint(action1); endpoint.withActions(actions); withEndpoint(endpoint); return this; } public Api withEndpoint(String ruleMatcherSpec, Action... actions) { Endpoint endpoint = new Endpoint(ruleMatcherSpec, actions); withEndpoint(endpoint); return this; } public Api withEndpoint(Endpoint... endpoints) { for (Endpoint endpoint : endpoints) { if (!this.endpoints.contains(endpoint)) { boolean inserted = false; for (int i = 0; i < this.endpoints.size(); i++) { if (endpoint.getOrder() < this.endpoints.get(i).getOrder()) { this.endpoints.add(i, endpoint); inserted = true; break; } } if (!inserted) this.endpoints.add(endpoint); endpoint.withApi(this); } } return this; } /** * Creates a ONE_TO_MANY Relationship from the parent to child collection and the inverse MANY_TO_ONE from the child to the parent. * The Relationship object along with the required Index objects are created. *

* If parentPropertyName is null, the ONE_TO_MANY relationship will not be crated. *

* If childPropertyName is null, the MANY_TO_ONE relationship will not be created. *

* If both parentPropertyName and childPropertyName are null, nothing will be performed, this will be a noop. *

* This configuration does not occur until after the Api has been started so that underlying Collections/Properties don't have to exist. * * @param parentCollectionName the name of the parent collection * @param parentPropertyName the name of the json property for the parent that references the children (optional) * @param childCollectionName the target child collection name * @param childPropertyName the name of hte json property for the child that references the parent (optional) * @param childFkProps names of the existing Properties that make up the foreign key * @return this * @see Collection#withOneToManyRelationship(String, Collection, String...) * @see Collection#withManyToOneRelationship(String, Collection, String...) */ public Api withRelationship(String parentCollectionName, String parentPropertyName, String childCollectionName, String childPropertyName, String... childFkProps) { withDelayedConfig(() -> { Collection parentCollection = getCollection(parentCollectionName); Collection childCollection = getCollection(childCollectionName); if (parentCollection == null || childCollection == null) throw ApiException.new500InternalServerError("You have specified a relationship between collections that don't exist in the Api after startup during delayed config: '{}' and, '{}'", parentCollectionName, childCollectionName); if(parentPropertyName != null) parentCollection.withOneToManyRelationship(parentPropertyName, childCollection, childFkProps); if(childPropertyName != null) childCollection.withManyToOneRelationship(childPropertyName, parentCollection, childFkProps); }); return this; } // /** // * Creates a ONE_TO_MANY Relationship from the parent to child collection and the inverse MANY_TO_ONE from the child to the parent. // * The Relationship object along with the required Index objects are created. // *

// * For collections backed by relational data sources (like a SQL db) the length of childFkProps will generally match the // * length of parentCollections primary index. If the two don't match, then childFkProps must be 1. In this // * case, the compound primary index of parentCollection will be encoded as an resourceKey in the single child table property. // * // * @param parentCollection the collection to add the relationship to // * @param parentPropertyName the name of the json property for the parent that references the child // * @param childCollection the target child collection // * @param childPropertyName the name of hte json property for the child that references the parent // * @param childFkProps Properties that make up the foreign key // * @return this // */ // public Api withRelationship(Collection parentCollection, String parentPropertyName, Collection childCollection, String childPropertyName, Property... childFkProps) { // parentCollection.withOneToManyRelationship(parentPropertyName, childCollection, childPropertyName, childFkProps); // return this; // } public Action getAction(String name) { for (Action action : actions) { if (name.equalsIgnoreCase(action.getName())) return action; } return null; } public List getActions() { return new ArrayList<>(actions); } /** * Add Action(s) may be selected to run across multiple Endpoints. * * @param actions actions to match and conditionally run across all Requests * @return this */ public synchronized Api withActions(Action... actions) { for (Action action : actions) if (!this.actions.contains(action)) this.actions.add(action); return this; } public Api withAction(Action action) { if (!this.actions.contains(action)) this.actions.add(action); return this; } public Engine getEngine() { return engine; } public boolean isDebug() { return debug; } public Api withDebug(boolean debug){ this.debug = debug; return this; } public void setDebug(boolean debug) { this.debug = debug; } public String getUrl() { return url; } public Api withUrl(String url) { this.url = url; return this; } public Linker getLinker() { return linker; } public Api withLinker(Linker linker) { this.linker = linker; return this; } public Api withApiListener(ApiListener listener) { if (!listeners.contains(listener)) listeners.add(listener); return this; } public List getApiListeners() { return Collections.unmodifiableList(listeners); } /** * Listener that can be registered with an {@code Api} to receive lifecycle, * per request and per error callback notifications. */ public interface ApiListener { default void onStartup(Engine engine, Api api) { //implement me } default void onShutdown(Engine engine, Api api) { //implement me } default void onAfterRequest(Request req, Response res) { //implement me } default void onAfterError(Request req, Response res) { //implement me } default void onBeforeFinally(Request req, Response res) { //implement me } } public Db matchDb(String method, Path requestPath) { //-- find the db with the most specific (longest) path match Db winnerDb = null; Path winnerDbMatch = null; for (Db db : getDbs()) { Path dbMatch = db.match(method, requestPath); if (dbMatch != null) { if (winnerDbMatch == null || dbMatch.size() > winnerDbMatch.size()) { winnerDb = db; winnerDbMatch = dbMatch; } } } return winnerDb; } public List getOps() { return ops; } public Op getOp(String name) { for (Op op : ops) { if (name.equalsIgnoreCase(op.getName())) return op; } return null; } void configureServers() { System.out.println("\r\n--------------------------------------------"); System.out.println("SERVERS: "); for (Server server : servers) { for(Server.ServerMatcher sm : server.getServerMatches()){ System.out.println(" - " + sm); } } } List configureOps() { Map>>> allPaths = generatePaths(); List ops = new ArrayList<>(); for (String method : allPaths.keySet()) { Map>> epMap = allPaths.get(method); for (Endpoint ep : epMap.keySet()) { List> groupedPaths = epMap.get(ep); for (List paths : groupedPaths) { Op op = new Op(); op.withMethod(method); op.withApi(this); op.withEngine(engine); paths.forEach(p -> op.withPath(p)); if (matchOp(ep, op)) { ops.add(op); //System.out.println(op.getMethod() + " - " + op.getPath() + " - " + op); } } } } for (Op op : ops) { Task.buildTask(op.getActions(), "configureOp", op).go(); if(op.getCollection() != null && op.getCollection().getDb() != null && op.getDb() == null && op.getDbPathMatch() == null){ Db db = op.getCollection().getDb(); Path match = db.match(op.getMethod(), op.getPath()); op.withDb(db); op.withDbPathMatch(match); } } ops.removeIf(o -> o.getFunction() == null); ops.removeIf(o -> o.getPath().toString().indexOf("{_") > 0); ops.removeIf(o -> o.getActions().size() == 0); ops.removeIf(o -> o.getActions().parallelStream().allMatch(a -> a.isDecoration())); ops.removeIf(o -> o.getName() == null); Collections.sort(ops); deduplicateOperationNames(ops); System.out.println("\r\n--------------------------------------------"); //System.out.println("\r\nOPERATIONS:"); List table = new ArrayList<>(); List header = new ArrayList(); table.add(header); Utils.add(header, "METHOD", "PATH", "OPERATION", "ENDPOINT", "COLLECTION", "ACTIONS", "PARAMS"); for (Op op : ops) { List row = new ArrayList(); table.add(row); List actionNames = new ArrayList(); op.getActions().forEach(a -> actionNames.add(a.getName() != null ? a.getName() : a.getClass().getSimpleName())); Utils.add(row, op.getMethod(), op.getPath(), op.getName(), op.getEndpoint().getName(), op.getCollection() != null ? op.getCollection().getName() : null, actionNames, op.getParams()); } System.out.println(Utils.printTable(table)); System.out.println("\r\n--------------------------------------------"); this.ops = Collections.unmodifiableList(ops); return new ArrayList(ops); } boolean matchOp(Endpoint ep, Op op) { String method = op.getMethod(); Path requestPath = op.getPath().copy(); Path endpointMatch = ep.match(method, requestPath); if (endpointMatch == null) return false; op.withEndpoint(ep); op.withEndpointPathMatch(endpointMatch.copy()); //-- pull out api action match paths before consuming the //-- request path for the endpoint action matches for (Action action : getActions()) { Path actionMatch = action.match(method, requestPath, true); if (actionMatch != null) { op.withActionMatch(action, actionMatch.copy(), false); } } //-- consume the endpoint part of the request path to relative match the actions for (int i = 0; i < endpointMatch.size(); i++) { if (endpointMatch.isOptional(i) || endpointMatch.isWildcard(i)) break; requestPath.remove(0); } op.withActionPathMatch(requestPath.copy()); for (Action action : ep.getActions()) { Path actionMatch = action.match(method, requestPath, true); if (actionMatch != null) { op.withActionMatch(action, actionMatch.copy(), true); } } return true; } Map>>> generatePaths() { LinkedHashMap>>> paths = new LinkedHashMap<>(); Set methods = new LinkedHashSet(); Utils.add(methods, "GET", "POST", "PUT", "PATCH", "DELETE"); for (String method : methods) { List eps = new ArrayList(getEndpoints()); Collections.sort(eps); for (Endpoint ep : eps) { List endpointPaths = new ArrayList(); for (Rule.RuleMatcher epMatcher : ep.getIncludeMatchers()) { if (!epMatcher.hasMethod(method)) continue; for (Path epPath : epMatcher.getPaths()) { Db db = matchDb(method, epPath); List epActions = ep.getActions(); List allActions = new ArrayList(ep.getActions()); for (Action action : getActions()) { if (!allActions.contains(action)) allActions.add(action); } Collections.sort(allActions); List allPathsForSingleEpMatcherPath = new ArrayList<>(); for (Action action : allActions) { if(action.isDecoration()) continue; for(Path fullActionPath : (List)action.getFullIncludePaths(this, db, method, epPath, epActions.contains(action))){ allPathsForSingleEpMatcherPath.add(fullActionPath); } } allPathsForSingleEpMatcherPath = Path.filterDuplicates(allPathsForSingleEpMatcherPath); allPathsForSingleEpMatcherPath.removeIf(p -> p.size() == 0); endpointPaths.addAll(allPathsForSingleEpMatcherPath); } } endpointPaths = Path.filterDuplicates(endpointPaths); endpointPaths = removeObscuredWildcards(endpointPaths); if(endpointPaths.size() > 0){ List> groupedPaths = groupPaths(endpointPaths); Map>> epMap = paths.get(method); if (epMap == null) { epMap = new LinkedHashMap<>(); paths.put(method, epMap); } epMap.put(ep, groupedPaths); } } } // System.out.println("\r\nPATHS ------------------------"); // for(String method : paths.keySet()){ // Map>> pathSet = paths.get(method); // for(Endpoint ep : pathSet.keySet()){ // System.out.println(method + " - " + ep.getAllIncludePaths()); // for(Object o : pathSet.get(ep)){ // System.out.println(" - " + o); // } // } // } // System.out.println("END PATHS --------------------\r\n"); return paths; } List removeObscuredWildcards(List paths){ ListMap templates = new ListMap(); for(Path path : paths){ String template = path.getTemplate(); templates.put(template, path); } List sorted = new ArrayList(templates.keySet()); Collections.sort(sorted); for(int i=1; i newPaths = new ArrayList(); for(String key : templates.keySet()){ List ps = templates.get(key); newPaths.addAll(ps); } Collections.sort(newPaths); return newPaths; } /** * Groups into lists of compatible potentially variablized paths * based on bidirectional matching. * * @param paths * @return */ List> groupPaths(List paths) { paths = Path.filterDuplicates(paths); //System.out.println(paths); List> groups = new ArrayList<>(); List dynamics = new ArrayList(); List statics = new ArrayList(); List unbounds = new ArrayList(); for (Path p : paths) { if (p.hasVars()) { boolean unbound = false; for (int i = 0; i < p.size(); i++) { String name = p.getVarName(i); if (name != null && name.startsWith("_")) { unbound = true; break; } } if (unbound) unbounds.add(p); else dynamics.add(p); } else statics.add(p); } statics.forEach(p -> groups.add(Utils.add(new ArrayList(), p))); dynamics.forEach(p -> groups.add(Utils.add(new ArrayList(), p))); for (Path unbound : unbounds) { for (List group : groups) { boolean matches = true; for (Path p : group) { if (!p.matches(unbound, true)) { matches = false; break; } } if (matches) group.add(unbound); } } //System.out.println(groups); return groups; } void deduplicateOperationNames(List ops) { Collections.sort(ops); ListMap map = new ListMap<>(); ops.forEach(op -> map.put(op.getName(), op)); for (String operationName : map.keySet()) { List values = map.get(operationName); if (values.size() > 1) { for (int i = 0; i < values.size(); i++) { String name = values.get(i).getName() + (i + 1); values.get(i).withName(name); } } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy