/**
* SusiTransfer
* Copyright 14.07.2016 by Michael Peter Christen, @0rb1t3r
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program in the file lgpl21.txt
* If not, see <http://www.gnu.org/licenses/>.
*/
package org.loklak.susi;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.json.JSONArray;
import org.json.JSONObject;
import com.google.common.util.concurrent.AtomicDouble;
/**
* Transfer is the ability to perceive a given thought in a different representation
* in such a way that it applies on a skill or a skill set.
*/
public class SusiTransfer {
private LinkedHashMap<String, String> selectionMapping;
/**
* Create a new transfer. The mapping must be given in the same way as SQL column selection
* statements. The selection of sub-objects of json object can be done using dot-notion.
* Arrays can be accessed using brackets '[' and ']'. An example is:
* mapping = "location.lon AS longitude, location.lat AS latitude"
* or
* mapping = "names[0] AS firstname"
* As a reference, the mappingExpression shall be superset of a list of
* https://mariadb.com/kb/en/mariadb/select/#select-expressions
* @param mapping
*/
public SusiTransfer(String mappingExpression) {
this.selectionMapping = parse(mappingExpression);
}
/**
* get the set of transfer keys
* @return
*/
public Set<String> keys() {
return this.selectionMapping.keySet();
}
/**
* transfer mappings can be used to extract specific information from a json object to
* create a new json object. In the context of Susi this is applied on choices from thought data
* @param choice one 'row' of a SusiThought data array
* @return a choice where the elements of the given choice are extracted according to the given mapping
*/
public JSONObject extract(JSONObject choice) {
if (this.selectionMapping == null) return choice;
JSONObject json = new JSONObject(true);
for (Map.Entry<String, String> c: selectionMapping.entrySet()) {
String key = c.getKey();
int p = key.indexOf('.');
if (p > 0) {
// sub-element
String k0 = key.substring(0, p);
String k1 = key.substring(p + 1);
if (choice.has(k0)) {
if (k1.equals("length") || k1.equals("size()")) {
Object a = choice.get(k0);
if (a instanceof String[]) {
json.put(c.getValue(),((String[]) a).length);
} else if (a instanceof JSONArray) {
json.put(c.getValue(),((JSONArray) a).length());
}
} else {
JSONObject o = choice.getJSONObject(k0);
if (o.has(k1)) json.put(c.getValue(), o.get(k1));
}
}
} else if ((p = key.indexOf('[')) > 0) {
// array
int q = key.indexOf("]", p);
if (q > 0) {
String k0 = key.substring(0, p);
int i = Integer.parseInt(key.substring(p + 1, q));
if (choice.has(k0)) {
JSONArray a = choice.getJSONArray(k0);
if (i < a.length()) json.put(c.getValue(), a.get(i));
}
}
} else {
// flat
if (choice.has(key)) json.put(c.getValue(), choice.get(key));
}
}
return json;
}
/**
* A conclusion from choices is done by the application of a function on the choice set.
* This may be done by i.e. counting the number of choices or extracting a maximum element.
* @param choices the given set of json objects from the data object of a SusiThought
* @returnan array of json objects which are the extraction of given choices according to the given mapping
*/
public JSONArray conclude(JSONArray choices) {
JSONArray a = new JSONArray();
if (this.selectionMapping != null && this.selectionMapping.size() == 1) {
// test if this has an aggregation key: AVG, COUNT, MAX, MIN, SUM
final String aggregator = this.selectionMapping.keySet().iterator().next();
final String aggregator_as = this.selectionMapping.get(aggregator);
if (aggregator.startsWith("COUNT(") && aggregator.endsWith(")")) { // TODO: there should be a special pattern for this to make it more efficient
return a.put(new JSONObject().put(aggregator_as, choices.length()));
}
if (aggregator.startsWith("MAX(") && aggregator.endsWith(")")) {
final AtomicDouble max = new AtomicDouble(Double.MIN_VALUE); String c = aggregator.substring(4, aggregator.length() - 1);
choices.forEach(json -> max.set(Math.max(max.get(), ((JSONObject) json).getDouble(c))));
return a.put(new JSONObject().put(aggregator_as, max.get()));
}
if (aggregator.startsWith("MIN(") && aggregator.endsWith(")")) {
final AtomicDouble min = new AtomicDouble(Double.MAX_VALUE); String c = aggregator.substring(4, aggregator.length() - 1);
choices.forEach(json -> min.set(Math.min(min.get(), ((JSONObject) json).getDouble(c))));
return a.put(new JSONObject().put(aggregator_as, min.get()));
}
if (aggregator.startsWith("SUM(") && aggregator.endsWith(")")) {
final AtomicDouble sum = new AtomicDouble(0.0d); String c = aggregator.substring(4, aggregator.length() - 1);
choices.forEach(json -> sum.addAndGet(((JSONObject) json).getDouble(c)));
return a.put(new JSONObject().put(aggregator_as, sum.get()));
}
if (aggregator.startsWith("AVG(") && aggregator.endsWith(")")) {
final AtomicDouble sum = new AtomicDouble(0.0d); String c = aggregator.substring(4, aggregator.length() - 1);
choices.forEach(json -> sum.addAndGet(((JSONObject) json).getDouble(c)));
return a.put(new JSONObject().put(aggregator_as, sum.get() / choices.length()));
}
}
if (this.selectionMapping != null && this.selectionMapping.size() == 2) {
Iterator<String> ci = this.selectionMapping.keySet().iterator();
String aggregator = ci.next(); String column = ci.next();
if (column.indexOf('(') >= 0) {String s = aggregator; aggregator = column; column = s;}
final String aggregator_as = this.selectionMapping.get(aggregator);
final String column_as = this.selectionMapping.get(column);
final String column_final = column;
if (aggregator.startsWith("PERCENT(") && aggregator.endsWith(")")) {
final AtomicDouble sum = new AtomicDouble(0.0d); String c = aggregator.substring(8, aggregator.length() - 1);
choices.forEach(json -> sum.addAndGet(((JSONObject) json).getDouble(c)));
choices.forEach(json -> a.put(new JSONObject()
.put(aggregator_as, 100.0d * ((JSONObject) json).getDouble(c) / sum.get())
.put(column_as, ((JSONObject) json).get(column_final))));
return a;
}
}
for (Object json: choices) {
JSONObject extraction = this.extract((JSONObject) json);
if (extraction.length() > 0) a.put(extraction);
}
return a;
}
private static LinkedHashMap<String, String> parse(String mapping) {
LinkedHashMap<String, String> columns;
String[] column_list = mapping.trim().split(",");
if (column_list.length == 1 && column_list[0].equals("*")) {
columns = null;
} else {
columns = new LinkedHashMap<>();
for (String column: column_list) {
String c = column.trim();
int p = c.indexOf(" AS ");
if (p < 0) {
c = trimQuotes(c);
columns.put(c, c);
} else {
columns.put(trimQuotes(c.substring(0, p).trim()), trimQuotes(c.substring(p + 4).trim()));
}
}
}
return columns;
}
private static String trimQuotes(String s) {
if (s.length() == 0) return s;
if (s.charAt(0) == '\'' || s.charAt(0) == '\"') s = s.substring(1);
if (s.charAt(s.length() - 1) == '\'' || s.charAt(s.length() - 1) == '\"') s = s.substring(0, s.length() - 1);
return s;
}
public String toString() {
return this.selectionMapping == null ? "NULL" : this.selectionMapping.toString();
}
}