/**
* TableService
* Copyright 13.06.2015 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 .
*/
package org.loklak.api.search;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.elasticsearch.search.sort.SortOrder;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.loklak.data.DAO;
import org.loklak.geo.GeoMark;
import org.loklak.http.ClientConnection;
import org.loklak.objects.AccountEntry;
import org.loklak.objects.QueryEntry;
import org.loklak.objects.ResultList;
import org.loklak.objects.Timeline;
import org.loklak.objects.UserEntry;
import org.loklak.server.APIException;
import org.loklak.server.APIHandler;
import org.loklak.server.BaseUserRole;
import org.loklak.server.AbstractAPIHandler;
import org.loklak.server.Authorization;
import org.loklak.server.Query;
import org.loklak.susi.SusiThought;
import com.google.common.util.concurrent.AtomicDouble;
import org.loklak.tools.storage.JSONObjectWithDefault;
/* examples:
* http://localhost:9000/api/console.json?q=SELECT%20text,%20screen_name,%20user.name%20AS%20user%20FROM%20messages%20WHERE%20query=%271%27;
* http://localhost:9000/api/console.json?q=SELECT%20*%20FROM%20messages%20WHERE%20id=%27742384468560912386%27;
* http://localhost:9000/api/console.json?q=SELECT%20link,screen_name%20FROM%20messages%20WHERE%20id=%27742384468560912386%27;
* http://localhost:9000/api/console.json?q=SELECT%20COUNT(*)%20AS%20count,%20screen_name%20AS%20twitterer%20FROM%20messages%20WHERE%20query=%27loklak%27%20GROUP%20BY%20screen_name;
* http://localhost:9000/api/console.json?q=SELECT%20PERCENT(count)%20AS%20percent,%20screen_name%20FROM%20(SELECT%20COUNT(*)%20AS%20count,%20screen_name%20FROM%20messages%20WHERE%20query=%27loklak%27%20GROUP%20BY%20screen_name)%20WHERE%20screen_name%20IN%20(%27leonmakk%27,%27Daminisatya%27,%27sudheesh001%27,%27shiven_mian%27);
* http://localhost:9000/api/console.json?q=SELECT%20query,%20query_count%20AS%20count%20FROM%20queries%20WHERE%20query=%27auto%27;
* http://localhost:9000/api/console.json?q=SELECT%20*%20FROM%20users%20WHERE%20screen_name=%270rb1t3r%27;
* http://localhost:9000/api/console.json?q=SELECT%20place[0]%20AS%20place,%20population,%20location[0]%20AS%20lon,%20location[1]%20AS%20lat%20FROM%20locations%20WHERE%20location=%27Berlin%27;
* http://localhost:9000/api/console.json?q=SELECT%20*%20FROM%20locations%20WHERE%20location=%2753.1,13.1%27;
* http://localhost:9000/api/console.json?q=SELECT%20description%20FROM%20wikidata%20WHERE%20query=%27football%27;
* http://localhost:9000/api/console.json?q=SELECT%20*%20FROM%20meetup%20WHERE%20url=%27http://www.meetup.com/?q=Women-Who-Code-Delhi%27;
* http://localhost:9000/api/console.json?q=SELECT%20*%20FROM%20rss%20WHERE%20url=%27https://www.reddit.com/search.rss?q=loklak%27;
* http://localhost:9000/api/console.json?q=SELECT%20*%20FROM%20eventbrite%20WHERE%20url=%27url=https://www.eventbrite.com/e/?q=global-health-security-focus-africa-tickets-25740798421%27;
*/
public class ConsoleService extends AbstractAPIHandler implements APIHandler {
private static final long serialVersionUID = 8578478303032749879L;
@Override
public BaseUserRole getMinimalBaseUserRole() { return BaseUserRole.ANONYMOUS; }
@Override
public JSONObject getDefaultPermissions(BaseUserRole baseUserRole) {
return null;
}
public String getAPIPath() {
return "/api/console.json";
}
private static LinkedHashMap parseCommaList(String cl) {
LinkedHashMap columns;
String[] column_list = cl.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;
}
private static class Columns {
private LinkedHashMap columns;
public Columns(String columnString) {
this.columns = parseCommaList(columnString);
}
public JSONObject extractRow(JSONObject message) {
if (this.columns == null) return message;
JSONObject json = new JSONObject(true);
for (Map.Entry c: columns.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 (message.has(k0)) {
if (k1.equals("length") || k1.equals("size()")) {
Object a = message.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 = message.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 (message.has(k0)) {
JSONArray a = message.getJSONArray(k0);
if (i < a.length()) json.put(c.getValue(), a.get(i));
}
}
} else {
// flat
if (message.has(key)) json.put(c.getValue(), message.get(key));
}
}
return json;
}
public JSONArray extractTable(JSONArray rows) {
JSONArray a = new JSONArray();
if (this.columns != null && this.columns.size() == 1) {
// test if this has an aggregation key: AVG, COUNT, MAX, MIN, SUM
final String aggregator = this.columns.keySet().iterator().next();
final String aggregator_as = this.columns.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, rows.length()));
}
if (aggregator.startsWith("MAX(") && aggregator.endsWith(")")) {
final AtomicDouble max = new AtomicDouble(Double.MIN_VALUE); String c = aggregator.substring(4, aggregator.length() - 1);
rows.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);
rows.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);
rows.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);
rows.forEach(json -> sum.addAndGet(((JSONObject) json).getDouble(c)));
return a.put(new JSONObject().put(aggregator_as, sum.get() / rows.length()));
}
}
if (this.columns != null && this.columns.size() == 2) {
Iterator ci = this.columns.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.columns.get(aggregator);
final String column_as = this.columns.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);
rows.forEach(json -> sum.addAndGet(((JSONObject) json).getDouble(c)));
rows.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: rows) a.put(this.extractRow((JSONObject) json));
return a;
}
public String toString() {
return this.columns == null ? "NULL" : this.columns.toString();
}
}
private final static LinkedHashMap> pattern = new LinkedHashMap<>();
static {
pattern.put(Pattern.compile("SELECT\\h+?(.*?)\\h+?FROM\\h+?\\(\\h??SELECT\\h+?(.*?)\\h??\\)\\h+?WHERE\\h+?(.*?)\\h?+IN\\h?+\\((.*?)\\)\\h??;"), matcher -> {
Columns columns = new Columns(matcher.group(1));
String subquery = matcher.group(2).trim();
if (!subquery.endsWith(";")) subquery = subquery + ";";
String filter_name = matcher.group(3);
Set filter_set = parseCommaList(matcher.group(4)).keySet();
JSONArray a0 = console("SELECT " + subquery).getJSONArray("data");
JSONArray a1 = new JSONArray();
a0.forEach(o -> {
JSONObject j = (JSONObject) o;
if (j.has(filter_name) && filter_set.contains(j.getString(filter_name))) a1.put(j);
});
return new SusiThought()
.setOffset(0).setHits(a0.length())
.setData(columns.extractTable(a1));
});
pattern.put(Pattern.compile("SELECT\\h+?(.*?)\\h+?FROM\\h+?messages\\h+?WHERE\\h+?id\\h??=\\h??'(.*?)'\\h??;"), matcher -> {
Columns columns = new Columns(matcher.group(1));
JSONObject message = DAO.messages.readJSON(matcher.group(2));
return message == null ? null : new SusiThought()
.setOffset(0).setHits(1)
.setData((new JSONArray()).put(columns.extractRow(message)));
});
pattern.put(Pattern.compile("SELECT\\h+?(.*?)\\h+?FROM\\h+?messages\\h+?WHERE\\h+?query\\h??=\\h??'(.*?)'\\h?+GROUP\\h?+BY\\h?+(.*?)\\h??;"), matcher -> {
Columns columns = new Columns(matcher.group(1));
String group = matcher.group(3);
DAO.SearchLocalMessages messages = new DAO.SearchLocalMessages(matcher.group(2), Timeline.Order.CREATED_AT, 0, 0, 100, group);
JSONArray array = new JSONArray();
JSONObject aggregation = messages.getAggregations().getJSONObject(group);
for (String key: aggregation.keySet()) array.put(new JSONObject(true).put(group, key).put("COUNT(*)", aggregation.get(key)));
SusiThought json = messages.timeline.toSusi(true);
return json.setData(columns.extractTable(array));
});
pattern.put(Pattern.compile("SELECT\\h+?(.*?)\\h+?FROM\\h+?messages\\h+?WHERE\\h+?query\\h??=\\h??'(.*?)'\\h??;"), matcher -> {
Columns columns = new Columns(matcher.group(1));
DAO.SearchLocalMessages messages = new DAO.SearchLocalMessages(matcher.group(2), Timeline.Order.CREATED_AT, 0, 100, 0);
SusiThought json = messages.timeline.toSusi(true);
return json.setData(columns.extractTable(json.getJSONArray("data")));
});
pattern.put(Pattern.compile("SELECT\\h+?(.*?)\\h+?FROM\\h+?queries\\h+?WHERE\\h+?query\\h??=\\h??'(.*?)'\\h??;"), matcher -> {
Columns columns = new Columns(matcher.group(1));
ResultList queries = DAO.SearchLocalQueries(matcher.group(2), 100, "retrieval_next", "date", SortOrder.ASC, null, new Date(), "retrieval_next");
SusiThought json = queries.toSusi();
json.setQuery(matcher.group(2));
return json.setData(columns.extractTable(json.getJSONArray("data")));
});
pattern.put(Pattern.compile("SELECT\\h+?(.*?)\\h+?FROM\\h+?users\\h+?WHERE\\h+?screen_name\\h??=\\h??'(.*?)'\\h??;"), matcher -> {
Columns columns = new Columns(matcher.group(1));
UserEntry user_entry = DAO.searchLocalUserByScreenName(matcher.group(2));
SusiThought json = new SusiThought();
json.setQuery(matcher.group(2));
if (user_entry == null) {
json.setHits(0).setData(new JSONArray());
} else {
json.setHits(1).setData(new JSONArray().put(user_entry.toJSON()));
}
return json.setData(columns.extractTable(json.getJSONArray("data")));
});
pattern.put(Pattern.compile("SELECT\\h+?(.*?)\\h+?FROM\\h+?accounts\\h+?WHERE\\h+?screen_name\\h??=\\h??'(.*?)'\\h??;"), matcher -> {
Columns columns = new Columns(matcher.group(1));
AccountEntry account_entry = DAO.searchLocalAccount(matcher.group(2));
SusiThought json = new SusiThought();
json.setQuery(matcher.group(2));
if (account_entry == null) {
json.setHits(0).setData(new JSONArray());
} else {
json.setHits(1).setData(new JSONArray().put(account_entry.toJSON()));
}
return json.setData(columns.extractTable(json.getJSONArray("data")));
});
pattern.put(Pattern.compile("SELECT\\h+?(.*?)\\h+?FROM\\h+?locations\\h+?WHERE\\h+?location\\h??=\\h??'(.*?)'\\h??;"), matcher -> {
Columns columns = new Columns(matcher.group(1));
GeoMark loc = DAO.geoNames.analyse(matcher.group(2), null, 5, Long.toString(System.currentTimeMillis()));
SusiThought json = new SusiThought();
json.setQuery(matcher.group(2));
if (loc == null) {
json.setHits(0).setData(new JSONArray());
} else {
json.setHits(1).setData(new JSONArray().put(loc.toJSON(false)));
}
return json.setData(columns.extractTable(json.getJSONArray("data")));
});
pattern.put(Pattern.compile("SELECT\\h+?(.*?)\\h+?FROM\\h+?wikidata\\h+?WHERE\\h+?query\\h??=\\h??'(.*?)'\\h??;"), matcher -> {
JSONObject wikidata;
try {
ClientConnection cc = new ClientConnection("https://www.wikidata.org/w/api.php?action=wbsearchentities&format=json&language=en&search=" + URLEncoder.encode(matcher.group(2), "UTF-8"));
wikidata = new JSONObject(new JSONTokener(cc.inputStream));
cc.close();
} catch (IOException | JSONException e) {wikidata = new JSONObject();}
SusiThought json = new SusiThought();
json.setQuery(matcher.group(2));
Columns columns = new Columns(matcher.group(1));
json.setData(columns.extractTable(wikidata.getJSONArray("search")));
json.setHits(json.getCount());
return json;
});
pattern.put(Pattern.compile("SELECT\\h+?(.*?)\\h+?FROM\\h+?meetup\\h+?WHERE\\h+?url\\h??=\\h??'(.*?)'\\h??;"), matcher -> {
SusiThought json = MeetupsCrawlerService.crawlMeetups(matcher.group(2));
Columns columns = new Columns(matcher.group(1));
json.setData(columns.extractTable(json.getData()));
return json;
});
pattern.put(Pattern.compile("SELECT\\h+?(.*?)\\h+?FROM\\h+?rss\\h+?WHERE\\h+?url\\h??=\\h??'(.*?)'\\h??;"), matcher -> {
SusiThought json = RSSReaderService.readRSS(matcher.group(2));
Columns columns = new Columns(matcher.group(1));
json.setData(columns.extractTable(json.getData()));
return json;
});
pattern.put(Pattern.compile("SELECT\\h+?(.*?)\\h+?FROM\\h+?20eventbrite\\h+?WHERE\\h+?url\\h??=\\h??'(.*?)'\\h??;"), matcher -> {
SusiThought json = EventBriteCrawlerService.crawlEventBrite(matcher.group(2));
Columns columns = new Columns(matcher.group(1));
json.setData(columns.extractTable(json.getData()));
return json;
});
}
public static SusiThought console(String q) {
if (q == null) return new SusiThought();
SusiThought json = null;
q = q.trim();
find_matcher: for (Map.Entry> pe: pattern.entrySet()) {
Pattern p = pe.getKey();
Matcher m = p.matcher(q);
if (m.find()) {
json = pe.getValue().apply(m);
if (json != null) break find_matcher;
}
}
// return json
if (json == null) json = new SusiThought();
return json;
}
@Override
public JSONObject serviceImpl(Query post, Authorization rights, final JSONObjectWithDefault permissions) throws APIException {
// parameters
String q = post.get("q", "");
//int timezoneOffset = post.get("timezoneOffset", 0);
return console(q);
}
}