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

io.inversion.Engine Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2015-2020 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 ch.qos.logback.classic.Level;
import io.inversion.Api.ApiListener;
import io.inversion.Chain.ActionMatch;
import io.inversion.action.db.DbAction;
import io.inversion.config.Config;
import io.inversion.context.Context;
import io.inversion.context.Includer;
import io.inversion.context.InversionNamer;
import io.inversion.context.codec.ToStringCodec;
import io.inversion.json.JSList;
import io.inversion.json.JSMap;
import io.inversion.json.JSNode;
import io.inversion.json.JSParser;
import io.inversion.rql.Rql;
import io.inversion.rql.Term;
import io.inversion.utils.Path;
import io.inversion.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Matches inbound Request Url paths to an Api Endpoint and executes associated Actions.
 */
public class Engine {

    static {
        //ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) org.slf4j.LoggerFactory.getLogger("ROOT");
        //logger.setLevel(Level.WARN);
    }

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

    final String name = "engine";

    /**
     * Optional override for the configPath sys/env prop used by Config to locate configuration property files
     */
    transient protected String configPath    = null;
    /**
     * Optional override for the sys/env prop used by Config to determine which profile specific configuration property files to load
     */
    transient protected String configProfile = null;

    transient protected Context context = null;

    transient protected Config config = null;


    /**
     * Listeners that will receive Engine and Api lifecycle, request, and error callbacks.
     */
    protected final transient    List listeners    = new ArrayList<>();
    /**
     * The last {@code Response} served by this Engine, primarily used for writing test cases.
     */
    protected transient volatile Response             lastResponse = null;
    /**
     * The {@code Api}s being service by this Engine
     */
    protected                    List            apis         = new Vector<>();

    protected final List filters = new ArrayList();

    /**
     * Base value for the CORS "Access-Control-Allow-Headers" response header.
     * 

* Values from the request "Access-Control-Request-Header" header are concatenated * to this resulting in the final value of "Access-Control-Allow-Headers" sent in the response. *

* Unless you are really doing something specific with browser security you probably won't need to customize this list. */ protected String corsAllowHeaders = "accept,accept-encoding,accept-language,access-control-request-headers,access-control-request-method,authorization,connection,content-type,host,user-agent,x-auth-token"; transient volatile boolean started = false; transient volatile boolean starting = false; public Engine() { //System.out.println("Engine<>"); } public Engine(String configPath, String configProfile) { this.configPath = configPath; this.configProfile = configProfile; } public Engine(Api... apis) { if (apis != null) for (Api api : apis) withApi(api); } /** * Convenient pre-startup hook for subclasses guaranteed to only be called once. *

* Called after starting has been set to true but before the {@code Wirer} is run or any {@code Api}s have been started. */ protected void startup0() { //implement me } /** * Runs the {@code Wirer} and calls startupApi for each Api. *

* An Engine can only be started once. * Any calls to startup after the initial call will not have any affect. * * @return this Engine */ public synchronized Engine startup() { if (started || starting) //accidental recursion guard return this; System.out.println("STARTING ENGINE..."); long start = System.currentTimeMillis(); starting = true; try { startup0(); Config config = getConfig(); Context context = getContext(); Map properties = config.getProperties(); //Map firstPassApplied = context.wire(properties, this); Map firstPassEncoded = context.encode(this); Map firstPassApplied = context.decode(properties); autowire(context); //-- remove props that were previously applied so configed classes don't get re-instantiated etc. //properties.entrySet().removeIf(entry -> firstPassApplied.containsKey(entry.getKey())); properties.entrySet().removeIf(entry -> entry.getKey().toLowerCase().endsWith(".class") || entry.getKey().toLowerCase().endsWith(".classname")); //Map secondPassApplied = context.wire(properties, this); Map secondPassEncoded = context.encode(this); Map secondPassApplied = context.decode(properties); started = true; boolean hasApi = false; for (Api api : apis) { hasApi = true; if (api.getEndpoints().size() == 0) throw ApiException.new500InternalServerError("CONFIGURATION ERROR: You have configured an Api without any Endpoints."); startupApi(api); } if (!hasApi) throw ApiException.new500InternalServerError("CONFIGURATION ERROR: You don't have any Apis configured."); System.out.println("...ENGINE STARTED IN: " + (System.currentTimeMillis() - start) + "ms"); return this; } finally { starting = false; } } /** * Removes all Apis and notifies listeners.onShutdown */ public void shutdown() { for (Api api : getApis()) { shutdownApi(api); } for (EngineListener listener : listeners) { try { listener.onShutdown(this); } catch (Exception ex) { ex.printStackTrace(); } } started = false; starting = false; Chain.resetAll(); } /** * Convenience overloading of {@code #service(Request, Response)} to run a REST GET Request on this Engine. *

* IMPORTANT: This method does not make an external HTTP request, it runs the request on this Engine. * If you want to make an external HTTP request see io.inversion.ApiClient. *

* GET requests for a specific resource should return 200 of 404. * GET requests with query string search conditions should return 200 even if the search did not yield any results. * * @param url the url that will be serviced by this Engine * @return the Response generated by handling the Request * @see #service(Request, Response) */ public Response get(String url) { return service("GET", url, null); } /** * Convenience overloading of {@code #service(Request, Response)} to run a REST GET Request on this Engine. *

* IMPORTANT: This method does not make an external HTTP request, it runs the request on this Engine. * If you want to make an external HTTP request see io.inversion.ApiClient. *

* GET requests for a specific resource should return 200 of 404. * GET requests with query string search conditions should return 200 even if the search did not yield any results. * * @param url the url that will be serviced by this Engine * @param params additional key/value pairs to add to the url query string * @return the Response generated by handling the Request * @see #service(Request, Response) */ public Response get(String url, Map params) { return service("GET", url, null, params); } /** * Convenience overloading of {@code #service(Request, Response)} to run a REST GET Request on this Engine. *

* IMPORTANT: This method does not make an external HTTP request, it runs the request on this Engine. * If you want to make an external HTTP request see io.inversion.ApiClient. *

* GET requests for a specific resource should return 200 of 404. * GET requests with query string search conditions should return 200 even if the search did not yield any results. * * @param url the url that will be serviced by this Engine * @param queryTerms additional keys (no values) to add to the url query string * @return the Response generated by handling the Request * @see #service(Request, Response) */ public Response get(String url, List queryTerms) { if (queryTerms != null && queryTerms.size() > 0) { Map params = new HashMap<>(); queryTerms.stream().filter(Objects::nonNull).forEach(key -> params.put(key.toString(), null)); return service("GET", url, null, params); } else { return service("GET", url, null, null); } } /** * Convenience overloading of {@code #service(Request, Response)} to run a REST POST Request on this Engine. *

* IMPORTANT: This method does not make an external HTTP request, it runs the request on this Engine. * If you want to make an external HTTP request see io.inversion.ApiClient. *

* Successful POSTs that create a new resource should return a 201. * * @param url the url that will be serviced by this Engine * @param body the JSON body to POST which will be stringified first * @return the Response generated by handling the Request * @see #service(Request, Response) */ public Response post(String url, JSNode body) { return service("POST", url, body.toString()); } /** * Convenience overloading of {@code #service(Request, Response)} to run a REST PUT Request on this Engine. *

* IMPORTANT: This method does not make an external HTTP request, it runs the request on this Engine. * If you want to make an external HTTP request see io.inversion.ApiClient. *

* Successful PUTs that update an existing resource should return a 204. * If the PUT references a resource that does not exist, a 404 will be returned. * * @param url the url that will be serviced by this Engine * @param body the JSON body to POST which will be stringified first * @return the Response generated by handling the Request * @see #service(Request, Response) */ public Response put(String url, JSNode body) { return service("PUT", url, body.toString()); } /** * Convenience overloading of {@code #service(Request, Response)} to run a REST PATCH Request on this Engine. *

* IMPORTANT: This method does not make an external HTTP request, it runs the request on this Engine. * If you want to make an external HTTP request see io.inversion.ApiClient. *

* Successful PATCHes that update an existing resource should return a 204. * If the PATCH references a resource that does not exist, a 404 will be returned. * * @param url the url for a specific resource that should be PATCHed that will be serviced by this Engine * @param body the JSON body to POST which will be stringified first * @return the Response generated by handling the Request * @see #service(Request, Response) */ public Response patch(String url, JSNode body) { return service("PATCH", url, body.toString()); } /** * Convenience overloading of {@code #service(Request, Response)} to run a REST DELETE Request on this Engine. *

* IMPORTANT: This method does not make an external HTTP request, it runs the request on this Engine. * If you want to make an external HTTP request see io.inversion.ApiClient. * * @param url the url of the resource to be DELETED * @return the Response generated by handling the Request with status 204 if the delete was successful or 404 if the resource was not found * @see #service(Request, Response) */ public Response delete(String url) { return service("DELETE", url, null); } /** * Convenience overloading of {@code #service(Request, Response)} to run a REST DELETE Request on this Engine. *

* IMPORTANT: This method does not make an external HTTP request, it runs the request on this Engine. * If you want to make an external HTTP request see io.inversion.ApiClient. * * @param url the url of the resource to be DELETED * @param hrefs the hrefs of the resource to delete * @return the Response generated by handling the Request with status 204 if the delete was successful or 404 if the resource was not found * @see #service(Request, Response) */ public Response delete(String url, JSList hrefs) { return service("DELETE", url, hrefs.toString()); } /** * Convenience overloading of {@code #service(Request, Response)} *

* IMPORTANT: This method does not make an external HTTP request, it runs the request on this Engine. * If you want to make an external HTTP request see io.inversion.ApiClient. * * @param method the http method of the requested operation * @param url the url that will be serviced by this Engine * @return the Response generated by handling the Request * @see #service(Request, Response) */ public Response service(String method, String url) { return service(method, url, null); } /** * Convenience overloading of {@code #service(Request, Response)} *

* IMPORTANT: This method does not make an external HTTP request, it runs the request on this Engine. * If you want to make an external HTTP request see io.inversion.ApiClient. * * @param method the http method of the requested operation * @param url the url that will be serviced by this Engine. * @param body a stringified JSON body presumably to PUT/POST/PATCH * @return the Response generated by handling the Request * @see #service(Request, Response) */ public Response service(String method, String url, String body) { return service(method, url, body, null); } /** * Convenience overloading of {@code #service(Request, Response)} *

* IMPORTANT: This method does not make an external HTTP request, it runs the request on this Engine. * If you want to make an external HTTP request see io.inversion.ApiClient. * * @param method the http method of the requested operation * @param url the url that will be serviced by this Engine. * @param body a stringified JSON body presumably to PUT/POST/PATCH * @param params additional key/value pairs to add to the url query string * @return the Response generated by handling the Request * @see #service(Request, Response) */ public Response service(String method, String url, String body, Map params) { if (url == null) throw new ApiException("Unable to service request with null url."); Request req = new Request(method, url, body); req.withEngine(this); if (params != null) { for (String key : params.keySet()) { req.getUrl().withParam(key, params.get(key)); } } Response res = new Response(); service(req, res); return res; } /** * The main entry point for processing a Request and generating Response content. *

* This method is designed to be called by integrating runtimes such as {@code EngineServlet} or by {@code Action}s that * need to make recursive calls to the Engine when performing composite operations. *

* The host and port component of the Request url are ignored assuming that this Engine instance is supposed to be servicing the request. * The url does not have to start with "http[s]://". If it does not, urls that start with "/" or not are handled the same. *

* All of the following would be processed the same way: *

    *
  • https://library.com/v1/library/books?ISBN=1234567890 *
  • https://library.com:8080/v1/library/books?ISBN=1234567890 *
  • https://localhost/v1/library/books?ISBN=1234567890 *
  • /v1/library/books?ISBN=1234567890 *
  • v1/library/books?ISBN=1234567890 *
* * @param req the api Request * @param res the api Response * @return the Chain representing all of the actions executed in populating the Response */ public Chain service(Request req, Response res) { Chain chain = null; if (res.getRequest() == null) res.withRequest(req); try { if (!started) startup(); chain = Chain.push(this, req, res); req.withEngine(this); req.withChain(chain); Url url = req.getUrl(); if (req.isMethod("options")) { //this is a CORS preflight request. All of the work was done above res.withStatus(Status.SC_200_OK); return chain; } //-- //-- CORS header setup //-- String allowedHeaders = this.corsAllowHeaders; String corsRequestHeader = req.getHeader("Access-Control-Request-Header"); if (corsRequestHeader != null) for (String h : corsRequestHeader.split(",")) { h = h.trim(); allowedHeaders = allowedHeaders.concat(h).concat(","); } if (req.isOptions()) { res.withHeader("Access-Control-Allow-Origin", "*"); res.withHeader("Access-Control-Allow-Credentials", "true"); res.withHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS"); res.withHeader("Access-Control-Allow-Headers", allowedHeaders); //-- //-- End CORS Header Setup } else { res.withHeader("Cache-Control", "no-store"); } Path urlPath = url.getPath(); for (int i = 0; i < urlPath.size(); i++) { if (urlPath.isVar(i) || urlPath.isWildcard(i)) throw ApiException.new400BadRequest("URL {} is malformed.", url); } if (url.toString().contains("/favicon.ico")) { //-- browsers being a pain in the rear //throw ApiException.new404NotFound("The requested resource 'favicon.ico' could not be found.", req.getUrl().getOriginal()); res.withStatus(Status.SC_404_NOT_FOUND); res.withJson((JSNode) null); return chain; } String xfp = req.getHeader("X-Forwarded-Proto"); String xfh = req.getHeader("X-Forwarded-Host"); if (xfp != null || xfh != null) { if (xfp != null) url.withProtocol(xfp); if (xfh != null) url.withHost(xfh); } //-- remove any RQL terms that functions with leading "_" as these are internal/restricted if (Chain.getDepth() < 2) { Map urlParams = req.getUrl().getParams(); for (String key : urlParams.keySet()) { if (key.indexOf("_") > 0) { List illegals = Rql.parse(key, urlParams.get(key)).stream().filter(t -> !t.isLeaf() && t.getToken().startsWith("_")).collect(Collectors.toList()); if (illegals.size() > 0) { req.getUrl().clearParams(key); } } } } for (Action filter : filters) { Path path = req.getUrl().getPath().copy(); Path match = filter.match(req.getMethod(), path); if (match != null) { chain.withAction(new ActionMatch(match, path, filter)); } } final Chain finalChain = chain; chain.withAction(new ActionMatch(null, null, new Action() { @Override public void run(Request req, Response res) throws ApiException { service0(finalChain, req, res); } })); //-- causes filters to run then the above anon action the runs the rest of the match/serve process after the filters run. chain.go(); Exception listenerEx = null; for (ApiListener listener : getApiListeners(req)) { try { listener.onAfterRequest(req, res); } catch (Exception ex) { if (listenerEx == null) listenerEx = ex; } } if (listenerEx != null) throw listenerEx; } catch (Throwable ex) { boolean outError = req.isDebug(); if(!outError) outError = !(ex instanceof ApiException) || ((ApiException)ex).getStatusCode() >= 500; if (outError) ex.printStackTrace(); Chain.debug("Uncaught Exception: " + Utils.getShortCause(ex)); JSNode json = buildErrorJson(ex); res.withStatus(json.getString("status")); res.withError(ex); res.withJson(json); for (ApiListener listener : getApiListeners(req)) { try { listener.onAfterError(req, res); } catch (Exception ex2) { log.warn("Error notifying EngineListener.beforeError", ex); } } } finally { if (Chain.isRoot()) { exclude(req, res); } try { for (ApiListener listener : getApiListeners(req)) { try { listener.onBeforeFinally(req, res); } catch (Exception ex) { log.warn("Error notifying EngineListener.onFinally", ex); } } } finally { if (chain != null) Chain.pop(); lastResponse = res; } } return chain; } void service0(Chain chain, Request req, Response res) throws ApiException { Url url = req.getUrl(); if (!matchRequest(req)) { String requestUrl = req.getUrl().getOriginal(); throw ApiException.new400BadRequest("No API or Endpoint was found matching your request '{}':'{}'", req.getMethod(), requestUrl); } if (req.isDebug()) { res.debug(""); res.debug(""); res.debug(">> request --------------"); res.debug(req.getMethod() + ": " + url); String opString = req.getOp().toString();//.replace("\r", " ").replace("\n", " ").replace(" ", " "); res.debug("OPERATION: " + opString); for (String key : req.getHeaders().keySet()) { res.debug(key + " " + Utils.implode(",", req.getAllHeaders(key))); } res.debug(""); List actionNames = new ArrayList(); for (ActionMatch am : req.getActionMatches()) { String name = am.action.getName(); if (name == null) name = am.action.getClass().getSimpleName(); actionNames.add(name); } String msg = req.getMethod() + " " + url.getPath() + " [" + Utils.implode(",", actionNames) + "]"; Chain.debug(msg); } if (req.getApi() == null) { throw ApiException.new400BadRequest("No API found matching URL: '{}'", url); } if (req.getEndpoint() == null) { StringBuilder buff = new StringBuilder(); for (Endpoint e : req.getApi().getEndpoints()) { if (!e.isInternal()) buff.append(e.toString()).append(" | "); } String orig = url.getOriginal(); throw ApiException.new404NotFound("No Endpoint found matching '{}:{}' Valid endpoints are: {}", req.getMethod(), url.getOriginal(), buff.toString()); } List actions = req.getActionMatches(); if (actions.size() == 0) throw ApiException.new404NotFound("No Actions are configured to handle your request. Check your server configuration."); run(chain, actions); } /** * This is specifically pulled out so you can mock Engine invocations * in test cases. * * @param chain the Chain of the running Request * @param actions the Actions that should be run to service the Request * @throws ApiException when something goes wrong */ void run(Chain chain, List actions) throws ApiException { chain.withActions(actions).go(); } public boolean matchApi(Request req) { if (req.getApi() != null) return true; Path reqPath = req.getUrl().getPath(); Path remainder = reqPath == null ? new Path() : reqPath.copy(); Path serverPathMatch = null; Path serverPath = null; Server server = null; Server.ServerMatcher serverMatch = null; Api api = null; Map pathParams = new HashMap(); for (Api a : getApis()) { for (Server serv : a.getServers()) { serverMatch = serv.match(req.getUrl()); if (serverMatch == null) continue; server = serv; api = a; serverPath = serverMatch.getPath().extract(pathParams, remainder); break; } if (api != null) { break; } } if (api != null && server != null) { req.withApi(api); req.withServer(server); req.withServerMatch(serverMatch); req.withServerPath(serverPath); req.withServerPathMatch(serverPathMatch); req.withPathParams(pathParams); req.withOperationPath(remainder); return true; } else { return false; } } boolean matchRequest(Request req) { if (!matchApi(req)) return false; Api api = req.getApi(); if (api == null) return false; Map pathParams = new HashMap<>(); Path remainder = req.getOperationPath().copy(); if (api != null) { for (Op op : api.getOps()) { boolean matched = op.matches(req, remainder); if (matched) { //TODO: need to revalidate for exclude rules...or remove the concept req.withOp(op); req.withEndpoint(op.getEndpoint()); req.withDb(op.getDb()); req.withCollection(op.getCollection()); Path dbPath = op.getDbPathMatch() != null ? op.getDbPathMatch().extract(pathParams, remainder.copy()) : null; Path endpointPath = op.getEndpointPathMatch().extract(pathParams, remainder); Path collectionPath = op.getCollectionPathMatch() != null ? op.getCollectionPathMatch().extract(pathParams, remainder.copy()) : null; req.withEndpointPath(endpointPath); req.withActionPath(remainder); req.withDbPath(dbPath); req.withCollectionPath(collectionPath); req.withPathParams(pathParams); // pathParams.clear(); // for(Parameter param : op.getParameters()){ // if(param.getIn().equalsIgnoreCase("path")){ // String name = param.getKey(); // String value = reqPath.get(param.getIndex()); // pathParams.put(name, value); // } // } String method = req.getMethod(); Path path = req.getPath(); Path subpath = req.getSubpath(); //this will get all actions specifically configured on the endpoint List actions = new ArrayList<>(); for (Action action : req.getEndpoint().getActions()) { Path actionPath = action.match(method, subpath); if (actionPath != null) { actions.add(new ActionMatch(actionPath, new Path(subpath), action)); } } //this matches for actions that can run across multiple endpoints. //this might be something like an authorization or logging action //that acts like a filter for (Action action : req.getApi().getActions()) { Path actionPath = action.match(method, path); if (actionPath != null) { actions.add(new ActionMatch(actionPath, new Path(path), action)); } } Collections.sort(actions); req.withActionMatches(actions); //System.out.println("SELECTING OPERATION: " + op.getMethod() + " " + op.getPath()); return true; } } } return false; } public static JSNode buildErrorJson(Throwable ex) { String status = "500 Internal Server Error"; String message = ex.getMessage(); String error = Utils.getShortCause(ex); if (ex instanceof ApiException) { ApiException apiEx = ((ApiException) ex); status = apiEx.getStatus(); message = apiEx.getMessage(); if(message.indexOf("-") < 25){ //error messages have the status in them...cut this out to get to the user message message = message.substring(message.indexOf("-") + 1).trim(); } if (apiEx.getStatusCode() < 500) error = null; } JSNode json = new JSMap("status", status, "message", message); if (error != null) json.put("error", error); return json; } public boolean isStarted() { return started; } /** * Registers listener to receive Engine, Api, request and error callbacks. * * @param listener the listener to add * @return this */ public Engine withEngineListener(EngineListener listener) { if (!listeners.contains(listener)) listeners.add(listener); return this; } LinkedHashSet getApiListeners(Request req) { LinkedHashSet listeners = new LinkedHashSet<>(); if (req.getApi() != null) { listeners.addAll(req.getApi().getApiListeners()); } listeners.addAll(this.listeners); return listeners; } public List getApis() { return new ArrayList<>(apis); } public synchronized Api getApi(String apiName) { if (apiName == null) return null; //only one api will have a name version pair so return the first one. for (Api api : apis) { if (apiName.equalsIgnoreCase(api.getName())) return api; } return null; } public synchronized Engine withApi(Api api) { if (apis.contains(api)) return this; if (api.isStarted() && api.getEngine() != null) api.getEngine().removeApi(api); List newList = new ArrayList<>(apis); Api existingApi = getApi(api.getName()); if (existingApi != null && existingApi != api) { newList.remove(existingApi); newList.add(api); } else if (existingApi == null) { newList.add(api); } if (existingApi != api && isStarted()) api.startup(this); apis = newList; if (existingApi != null && existingApi != api) { existingApi.shutdown(this); } return this; } protected void startupApi(Api api) { if (started) { try { api.startup(this); } catch (Exception ex) { log.warn("Error starting api '" + api.getName() + "'", ex); } for (EngineListener listener : listeners) { try { listener.onStartup(this, api); } catch (Exception ex) { log.warn("Error starting api '" + api.getName() + "'", ex); } } } } /** * Removes the api, notifies EngineListeners and calls api.shutdown(). * * @param api the api to be removed */ public synchronized void removeApi(Api api) { List newList = new ArrayList<>(apis); newList.remove(api); apis = newList; shutdownApi(api); } protected void shutdownApi(Api api) { if (api.isStarted()) { try { api.shutdown(this); } catch (Exception ex) { log.warn("Error shutting down api '" + api.getName() + "'", ex); } for (EngineListener listener : listeners) { try { listener.onShutdown(this, api); } catch (Exception ex) { log.warn("Error shutting down api '" + api.getName() + "'", ex); } } } } public Engine withAllowHeaders(String allowHeaders) { this.corsAllowHeaders = allowHeaders; return this; } /** * @return the last response serviced by this Engine. */ public Response getLastResponse() { return lastResponse; } public URL getResource(String name) { try { URL url = getClass().getClassLoader().getResource(name); if (url == null) { File file = new File(System.getProperty("user.dir"), name); if (file.exists()) url = file.toURI().toURL(); } return url; } catch (Exception ex) { throw new RuntimeException(ex); } } public String getConfigPath() { return configPath; } public Engine withConfigPath(String configPath) { this.configPath = configPath; return this; } public String getConfigProfile() { return configProfile; } public Engine withConfigProfile(String configProfile) { this.configProfile = configProfile; return this; } public Config getConfig() { if (config == null) { synchronized (this) { if (config == null) { config = Config.getConfig("inversion", getConfigPath(), getConfigProfile(), this); } } } return config; } public Engine withConfig(Config config) { this.config = config; return this; } public Context getContext() { if (context == null) { context = new Context(); Includer includer = context.getEncoder().getIncluder(); context.withNamer(new InversionNamer()); context.withCodec(new ToStringCodec(Path.class)); context.withCodec(new ToStringCodec(Rule.RuleMatcher.class)); context.withCodec(new ToStringCodec(JSNode.class) { @Override public String toString(Object bean) { return ((JSNode) bean).toString(false); } @Override public Object fromString(Type type, String encoded) { return JSParser.parseJson(encoded); } }); } return context; } public Engine withContext(Context context) { this.context = context; return this; } /** * Receives {@code Engine} and {@code Api} lifecycle, * per request and per error callback notifications. */ public interface EngineListener extends ApiListener { /** * Notified when the Engine is starting prior to accepting * any requests which allows listeners to perform additional configuration. * * @param engine the Engine starting */ default void onStartup(Engine engine) { //implement me } /** * Notified when the Engine is shutting down and has stopped receiving requests * allowing listeners to perform any resource cleanup. * * @param engine the Engine stopping */ default void onShutdown(Engine engine) { //implement me } } public Engine withFilters(Action... filters) { for (Action filter : filters) { if (filters != null) { if (!this.filters.contains(filter)) { this.filters.add(filter); } } } Collections.sort(this.filters); return this; } public List getFilters() { return Collections.unmodifiableList(filters); } protected static void exclude(Request req, Response res) { JSList data = res.data(); if (data == null) return; Set includes = getXcludesSet(req.getUrl().getParam("include")); Set excludes = getXcludesSet(req.getUrl().getParam("exclude")); if ((includes != null && includes.size() > 0) || (excludes != null && excludes.size() > 0)) { for (JSMap node : data.asMapList()) { exclude(node, includes, excludes, null); } } } protected static void exclude(JSMap node, Set includes, Set excludes, String path) { for (String key : new LinkedHashSet<>(node.keySet())) { String attrPath = (path != null ? (path + "." + key) : key).toLowerCase(); Object value = node.get(key); if (exclude(attrPath, includes, excludes)) { node.remove(key); } else { if (!(value instanceof JSNode)) continue; if (value instanceof JSList) { JSList arr = (JSList) value; for (int i = 0; i < arr.size(); i++) { if (arr.get(i) instanceof JSMap) { exclude((JSMap) arr.get(i), includes, excludes, attrPath); } } } else { exclude((JSMap) value, includes, excludes, attrPath); } } } } protected static boolean exclude(String path, Set includes, Set excludes) { boolean exclude = false; if (includes != null && includes.size() > 0) if (!find(includes, path, true)) exclude = true; if (excludes != null && excludes.size() > 0) if (find(excludes, path, false)) exclude = true; return exclude; } protected static boolean find(java.util.Collection paths, String path, boolean matchStart) { boolean found = false; if (paths.contains(path)) { found = true; } else { for (String param : paths) { if (matchStart) { if (param.startsWith(path + ".")) { found = true; break; } } if (Utils.wildcardMatch(param, path)) found = true; } } return found; } static Set getXcludesSet(String str) { if (str == null) return null; LinkedHashSet set = new LinkedHashSet(); for (String path : Utils.explode(",", str.toLowerCase())) { int pipe = path.indexOf('|'); if (pipe > -1) { String prefix = ""; String props = path; int dot = path.indexOf('.'); if (dot > -1 && dot < pipe) { prefix = path.substring(0, pipe); prefix = prefix.substring(0, prefix.lastIndexOf('.') + 1); props = path.substring(prefix.length()); } for (String prop : Utils.explode("\\|", props)) { set.add(prefix + prop); } } else { set.add(path); } } return set; } protected void autowire(Context context) { //-- SHORTCUT BOOTSTRAPPING //-- //-- //-- this is a shortcut bootstrapping options for //-- apis configured primarily through configuration if (context.getBeans(Api.class).size() == 0) { Api api = new Api(); context.putBean("api", api); } //-- assign all Apis to the engine for (Api api : context.getBeans(Api.class)) { if (!getApis().contains(api)) withApi(api); } //-- if you have a single Api, you don't have to explicitly assign endpoints/dbs/actions to the Api Api singleApi = getApis().size() == 1 ? getApis().get(0) : null; if (singleApi != null) { //-- assign all dbs to single api for (Db db : context.getBeans(Db.class)) { singleApi.withDb(db); } //-- assign all endpoints to the single Api for (Endpoint ep : context.getBeans(Endpoint.class)) { singleApi.withEndpoint(ep); } //-- make sure the Api has an endpoint if (singleApi.getEndpoints().size() == 0) { Endpoint ep = new Endpoint().withName("endpoint"); singleApi.withEndpoint(ep); context.putBean("endpoint", ep); } //-- assign all unassigned actions to the endpoint if there is one endpoint //-- or to the Api if there are multiple endpoints boolean assignToEndpoint = false; List endpoints = context.getBeans(Endpoint.class); if (endpoints.size() == 1 && endpoints.get(0).getActions().size() == 0) assignToEndpoint = true; for (Action action : context.getBeans(Action.class)) { boolean assigned = false; for (Endpoint ep : singleApi.getEndpoints()) { if (ep.getActions().contains(action)) { assigned = true; break; } } if (!assigned && getFilters().contains(action)) assigned = true; if (!assigned && singleApi.getActions().contains(action)) assigned = true; if (!assigned) { if (assignToEndpoint) endpoints.get(0).withAction(action); else singleApi.withAction(action); } } } //-- AFTER potentially assigning unassigned objects to single api above //-- 1. make sure every api has a server //-- 2. again, make sure every Api has an endpoint //-- 3. add a DbAction if there are no other actions for (Api api : getApis()) { if (api.getServers().size() == 0) api.withServer(new Server()); //-- give all APIs a default endpoint if they don't have one if (api.getEndpoints().size() == 0) { Endpoint ep = new Endpoint(); api.withEndpoint(ep); } if (api.getDbs().size() > 0 && api.getActions().size() == 0) { boolean hasAction = false; for (Endpoint ep : api.getEndpoints()) { if (ep.getActions().size() > 0) { hasAction = true; break; } } if (hasAction == false) { Action dbAction = new DbAction(); if (api.getEndpoints().size() == 1) api.getEndpoints().get(0).withAction(dbAction); else api.withAction(dbAction); } } } //-- //-- //-- //-- END SHORTCUT BOOTSTRAPPING //-- //-- this will cause the Dbs to reflect their data sources and create Collections etc. for (Api api : getApis()) for (Db db : api.getDbs()) db.startup(api); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy