Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright (c) 2015-2019 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.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.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;
/**
* An adapter to an underlying data source.
*
* The goal of the Db abstraction is to allow Actions like DbGet/Put/Post/Patch/DeleteAction to apply the same REST CRUD operations agnostically across multiple backend data storage engines.
*
* The primary job of a Db subclass is to:
*
*
reflectively generate Collections to represent their underlying tables (or buckets, folders, containers etc.) columns, indexes, and relationships, during {@code #doStartup(Api)}.
*
implement REST CRUD support by implementing {@link #select(Collection, Map)}, {@link #upsert(Collection, List)}, {@link #delete(Collection, List)}.
*
*
* Actions such as DbGetAction then:
*
*
translate a REST collection/resource request into columnName based json and RQL,
*
execute the requested CRUD method on the Collection's underlying Db
*
then translate the results back from the Db columnName based data into the approperiate jsonName version for external consumption.
*
*/
public class Db extends Rule {
/**
* These params are specifically NOT passed to the Query for parsing. These are either dirty worlds like sql injection tokens or the are used by actions themselves
*/
protected static final Set reservedParams = Collections.unmodifiableSet(new TreeSet<>(Arrays.asList("select", "insert", "update", "delete", "drop", "union", "truncate", "exec", "explain", "exclude", "expand", "collapse", "q")));
protected final Logger log = LoggerFactory.getLogger(getClass());
/**
* The Collections that are the REST interface to the backend tables (or buckets, folders, containers etc.) this Db exposes through an Api.
*/
protected final ArrayList collections = new ArrayList<>();
/**
* A tableName to collectionName map that can be used by whitelist backend tables that should be included in reflective Collection creation.
*/
protected final HashMap includeTables = new HashMap<>();
/**
* OPTIONAL column names that should be included in RQL queries, upserts and patches.
*
* @see #filterOutJsonProperty(Collection, String)
*/
protected final Set includeColumns = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
/**
* OPTIONAL column names that should be excluded from RQL queries, upserts and patches.
*
* @see #filterOutJsonProperty(Collection, String)
*/
protected final Set excludeColumns = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
final transient Set runningApis = new HashSet<>();
/**
* Indicates that this Db should reflectively create and configure Collections to represent its underlying tables.
*
* This would be false when an Api designer wants to very specifically configure an Api probably when the underlying db does not support the type of
* reflection required. For example, you may want to put specific Property and Relationship structure on top of an unstructured JSON document store.
*/
protected boolean bootstrap = true;
/**
* A property that can be used to disambiguate different backends supported by a single subclass.
*
* For example type might be "mysql" for a JdbcDb.
*/
protected String type = null;
/**
* Used to differentiate which Collection is being referred by a Request when an Api supports Collections with the same name from different Dbs.
*/
//protected Path endpointPath = null;
/**
* When set to true the Db will do everything it can to "work offline" logging commands it would have run but not actually running them.
*/
protected boolean dryRun = false;
transient boolean firstStartup = true;
transient boolean shutdown = false;
public Db() {
}
public Db(String name) {
this.name = name;
}
protected boolean excludeTable(String tableName) {
if (includeTables.size() > 0 && !(includeTables.containsKey(tableName) || includeTables.containsKey(tableName.toLowerCase())))
return true;
return false;
}
public static Object castJsonInput(String type, Object value) {
try {
if (value == null)
return null;
if (type == null) {
try {
if (!value.toString().contains(".")) {
return Long.parseLong(value.toString());
} else {
return Double.parseDouble(value.toString());
}
} catch (Exception ex) {
//must not have been an number
}
return value.toString();
}
switch (type.toLowerCase()) {
case "char":
case "nchar":
case "clob":
return value.toString().trim();
case "s":
case "string":
case "varchar":
case "nvarchar":
case "longvarchar":
case "longnvarchar":
case "json":
return value.toString();
case "n":
case "number":
case "numeric":
case "decimal":
if (!value.toString().contains("."))
return Long.parseLong(value.toString());
else
return Double.parseDouble(value.toString());
case "bool":
case "boolean":
case "bit": {
if ("1".equals(value))
value = "true";
else if ("0".equals(value))
value = "false";
return Boolean.parseBoolean(value.toString());
}
case "tinyint":
return Byte.parseByte(value.toString());
case "smallint":
return Short.parseShort(value.toString());
case "integer":
return Integer.parseInt(value.toString());
case "bigint":
return Long.parseLong(value.toString());
case "float":
case "real":
case "double":
return Double.parseDouble(value.toString());
case "datalink":
return new URL(value.toString());
case "binary":
case "varbinary":
case "longvarbinary":
return Utils.hexToBytes(value.toString());
case "date":
case "datetime":
return new java.sql.Date(Utils.date(value.toString()).getTime());
case "timestamp":
return new java.sql.Timestamp(Utils.date(value.toString()).getTime());
case "array":
if (value instanceof JSList)
return value;
else
return JSParser.asJSList(value + "");
case "object":
if (value instanceof JSNode)
return value;
else {
String json = value.toString().trim();
if (json.length() > 0) {
char c = json.charAt(0);
if (c == '[' || c == '{')
return JSParser.parseJson(value + "");
}
return json;
}
default:
throw ApiException.new500InternalServerError("Error casting '{}' as type '{}'", value, type);
}
} catch (Exception ex) {
Utils.rethrow(ex);
//throw new RuntimeException("Error casting '" + value + "' as type '" + type + "'", ex);
}
return null;
}
/**
* Called by an Api to as part of Api.startup().
*
* This implementation really only manages starting/started state, with the heaving lifting of bootstrapping delegated to {@link #doStartup(Api)}.
*
* @param api the api to start
* @return this
* @see #doStartup(Api)
*/
public final synchronized T startup(Api api) {
if (runningApis.contains(api))
return (T) this;
runningApis.add(api);
doStartup(api);
return (T) this;
}
/**
* Made to be overridden by subclasses or anonymous inner classes to do specific init of an Api.
*
* This method will not be called a second time after for an Api unless the Api is shutdown and then restarted.
*
* The default implementation, when {@link #isBootstrap()} is true, calls {@link #configDb()} once globally and {@link #configApi(Api)} once for each Api passed in.
*
* @param api the api to start
* @see #configDb()
* @see #configApi(Api)
*/
protected void doStartup(Api api) {
try {
if (isBootstrap()) {
if (firstStartup) {
firstStartup = false;
configDb();
}
configApi(api);
}
} catch (Exception ex) {
ex.printStackTrace();
Utils.rethrow(ex);
}
}
/**
* Shutsdown all running Apis.
*
* This is primarily a method used for testing.
*
* @return this
*/
public synchronized T shutdown() {
if (!shutdown) {
shutdown = true;
runningApis.forEach(this::shutdown);
doShutdown();
}
return (T) this;
}
protected void doShutdown() {
}
public synchronized T shutdown(Api api) {
if (runningApis.contains(api)) {
doShutdown(api);
runningApis.remove(api);
}
if (runningApis.size() == 0)
shutdown();
return (T) this;
}
/**
* Made to be overridden by subclasses or anonymous inner classes to do specific cleanup
*
* @param api the api shutting down
*/
protected void doShutdown(Api api) {
//default implementation does nothing, subclass can override if they need to close resources on shutdown
}
public boolean isRunning(Api api) {
return runningApis.contains(api);
}
/**
* Finds all records that match the supplied RQL query terms.
*
* The implementation of this method primarily translates jsonNames to columnNames for RQL inputs and JSON outputs
* delegating the work to {@link #doSelect(Collection, List)} where all ins and outs are based on columnName.
*
* @param collection the collection being queried
* @param params RQL terms that have been translated to use Property jsonNames
* @return A list of maps with keys as Property jsonNames
* @throws ApiException TODO: update/correct this javadoc
*/
public final Results select(Collection collection, Map params) throws ApiException {
List terms = new ArrayList<>();
for (String key : params.keySet()) {
String value = params.get(key);
Term term = Rql.parse(key, value);
List illegalTerms = term.stream().filter(t -> t.isLeaf() && reservedParams.contains(t.getToken())).collect(Collectors.toList());
if (illegalTerms.size() > 0) {
//Chain.debug("Ignoring RQL terms with reserved tokens: " + illegalTerms);
continue;
}
if (term.hasToken("eq") && term.getTerm(0).hasToken("include")) {
//THIS IS AN OPTIMIZATION...the rest action can pull stuff OUT of the results based on
//dotted path expressions. If you don't use dotted path expressions the includes values
//can be used to limit the sql select clause...however if any of the columns are actually
//dotted paths, don't pass on to the Query the extra stuff will be removed by the rest action.
boolean dottedInclude = false;
for (int i = 1; i < term.size(); i++) {
String str = term.getToken(i);
if (str.contains(".")) {
dottedInclude = true;
break;
}
}
if (dottedInclude)
continue;
//-- if the users requests eq(includes, href...) you have to replace "href" with the primary index column names
for (Term child : term.getTerms()) {
if (child.hasToken("href") && collection != null) {
Index pk = collection.getResourceIndex();
if (pk != null) {
term.removeTerm(child);
for (int i = 0; i < pk.size(); i++) {
Property c = pk.getProperty(i);
boolean includesPkCol = false;
for (Term col : term.getTerms()) {
if (col.hasToken(c.getColumnName())) {
includesPkCol = true;
break;
}
}
if (!includesPkCol)
term.withTerm(Term.term(term, c.getColumnName()));
}
}
break;
}
}
}
terms.add(term);
}
//-- this sort is not strictly necessary but it makes the order of terms in generated
//-- query text dependable so you can write better tests.
Collections.sort(terms);
List mappedTerms = new ArrayList<>();
terms.forEach(term -> mappedTerms.addAll(mapToColumnNames(collection, term.copy())));
Results results = doSelect(collection, mappedTerms);
if (results.size() > 0) {
for (int i = 0; i < results.size(); i++) {
//convert the map into a JSNode
Map row = results.getRow(i);
if (collection == null) {
JSMap node = new JSMap(row);
results.setRow(i, node);
} else {
JSMap node = new JSMap();
results.setRow(i, node);
//------------------------------------------------
//copy over defined attributes first, if the select returned
//extra columns they will be copied over last
for (Property attr : collection.getProperties()) {
String attrName = attr.getJsonName();
String colName = attr.getColumnName();
boolean rowHas = row.containsKey(colName);
if (rowHas)
//if (resourceKey != null || rowHas)
{
//-- if the resourceKey was null don't create
//-- empty props for fields that were not
//-- returned from the db
Object val = row.remove(colName);
//if (!node.containsKey(attrName))
{
val = castDbOutput(attr, val);
node.put(attrName, val);
}
}
}
//------------------------------------------------
// next, if the db returned extra columns that
// are not mapped to attributes, just straight copy them
List sorted = new ArrayList(row.keySet());
Collections.sort(sorted);
for (String key : sorted) {
if (!key.equalsIgnoreCase("href") && !node.containsKey(key)) {
Object value = row.get(key);
node.put(key, value);
}
}
//------------------------------------------------
// put any primary key fields at the top of the object
Index idx = collection.getResourceIndex();
if (idx != null) {
for (int j = idx.size() - 1; j >= 0; j--) {
Property prop = idx.getProperty(j);
if (node.containsKey(prop.getJsonName()))
node.putFirst(prop.getJsonName(), node.get(prop.getJsonName()));
}
}
//if(links.size() > 0)
// node.putFirst("_links", links);
}
}
} // end if results.size() > 0
//------------------------------------------------
//the "next" params come from the db encoded with db col names
//have to convert them to their attribute equivalents
for (Term term : ((List) results.getNext())) {
mapToJsonNames(collection, term);
}
return results;
}
/**
* Finds all records that match the supplied RQL query terms.
*
* @param collection the collection to query
* @param queryTerms RQL terms that have been translated to use Property columnNames not jsonNames
* @return A list of maps with keys as Property columnNames not jsonNames
*/
public Results doSelect(Collection collection, List queryTerms) throws ApiException{
return new Results(null);
}
public final List upsert(Collection collection, List