/*************************************************************************
* *
* This file is part of the 20n/act project. *
* 20n/act enables DNA prediction for synthetic biology/bioengineering. *
* Copyright (C) 2017 20n Labs, Inc. *
* *
* Please direct all queries to act@20n.com. *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program 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 General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*************************************************************************/
package act.server;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class PubmedEntry {
abstract class JSONVal {
// a disjoint union of either List<String> or List<JSONObj>
public abstract String toString(int indent);
}
class JSONValString extends JSONVal {
// .size() == 1 implies a singular String element; not to be encoded as [ 'a' ] but just as 'a'
List<String> jsonVal;
public JSONValString(List<String> strVal) {
this.jsonVal = strVal;
}
@Override
public String toString(int indent) {
// maybe it is just a list with a single element...
String s = "\"" + escape(this.jsonVal.get(0)) + "\"";
if (this.jsonVal.size() == 1)
return s;
// if OTOH it is really is a list and not just a list with a singleton element then write it as such:
for (int i = 1; i < this.jsonVal.size(); i++)
s += ", " + "\"" + escape(this.jsonVal.get(i)) + "\"";
return "[" + s + "]";
}
private String escape(String s) {
// This is a little complicated. replaceAll(regex, replacementStr)
// regex for \ is \\
// So the arguments to replaceAll have to conform to regex requirements...
// which means the java encoding "\\" is really the string(\) which means nothing to regex
// on the other hand the java encoding "\\\\" is really the string(\\) which is the regex representation of \
if (s.contains("\"")) // handle the case of """. This is wrongly quoted, so we need to escape the " into "\""
return s.replaceAll("\"", "\\\\\""); // the replacement has java_string(\\\\) = regex_string(\) followed by java_string(\")=regex(")
if (s.contains("\\")) // char \ need to be escaped...
return s.replaceAll("\\\\", "\\\\\\\\"); // replacement has java_string(\\\\\\\\) = regex_string(\\)
return s;
}
}
class JSONValObj extends JSONVal {
// .size() == 1 implies a singular object element; not to be encoded as [ { k: 'v' } ] but just as { k: 'v' }
List<JSONObj> jsonRecVal;
public JSONValObj(List<JSONObj> nestedVal) {
this.jsonRecVal = nestedVal;
}
@Override
public String toString(int indent) {
// maybe it is just a list with a single element...
String s = this.jsonRecVal.get(0).toString(indent + 1);
if (this.jsonRecVal.size() == 1)
return s;
// if OTOH it truly is a list of objects then write them out as a list.
for (int i = 1; i < this.jsonRecVal.size(); i++)
s += ", " + this.jsonRecVal.get(i).toString(indent + 1);
return "[" + s + "]";
}
}
/*
* Encodes things such as { name: 'ava'; address: { num: '124', street: 'Mulholland Drive' } }
* Data stored will be as:
* name -> JSONVal('ava')
* address -> JSONVal( JSONObj( num -> '124', street -> 'Mulholland Drive' ) )
*/
class JSONObj {
HashMap<String, JSONVal> obj;
public JSONObj(HashMap<String, List<String>> xmlTagsVal) {
this.obj = aggregateByKeys(xmlTagsVal);
}
private JSONObj(HashMap<String, JSONVal> toBeWrappedMap, boolean dummyArg) {
this.obj = toBeWrappedMap;
}
private HashMap<String, JSONVal> aggregateByKeys(HashMap<String, List<String>> xml) {
List<String> terminalKeys = new ArrayList<String>();
HashMap<String, HashMap<String, List<String>>> subKeys = new HashMap<String, HashMap<String, List<String>>>();
for (String k : xml.keySet()) {
if (!k.contains("/")) { // terminal key...
terminalKeys.add(k);
continue;
}
// non-terminal key.. see if already has a map else create...
int parentKeyLen = k.indexOf('/');
String parent = k.substring(0, parentKeyLen);
if (!subKeys.containsKey(parent))
subKeys.put(parent, new HashMap<String, List<String>>());
// add the mapping for this subkey...
String unprefixedKey = k.substring(parentKeyLen + 1);
subKeys.get(parent).put(unprefixedKey, xml.get(k));
}
HashMap<String, JSONVal> parsed = new HashMap<String, JSONVal>();
// for terminal keys we have a direct mapping in the JSON.
for (String k : terminalKeys)
parsed.put(k, new JSONValString(xml.get(k)));
// for non-terminal keys we have a recursive JSON object...
for (String k : subKeys.keySet()) {
String parentKey = k;
// the projected hashMap, of the keyset within xml, that shared this parentKey is the lookup value:
HashMap<String, List<String>> projectedMap = subKeys.get(parentKey);
List<JSONObj> l = new ArrayList<JSONObj>();
l.add(new JSONObj(aggregateByKeys(projectedMap), true));
// potential bug here.... we never create a real list of JSONObjs; so we will never have
// a : [ { k1: 'v1' } , { k1: 'v2' } ] in our final JSON....
// TODO: why is this not a problem???? Or maybe it is; and we have a bug here...
JSONVal subJSON = new JSONValObj(l);
parsed.put(parentKey, subJSON);
}
return parsed;
}
@Override
public String toString() {
return toString(0);
}
private String toString(int indent) {
String s = "";
String indentation = new String(new char[indent]).replace("\0", " ");
for (String k : this.obj.keySet()) {
s += "\n" + indentation + k + " : " + this.obj.get(k).toString(indent) + ",";
}
return "{" + s + "\n" + indentation + "}";
}
public JSONVal getXPath(int startIndex, List<String> xPath) {
String key = xPath.get(startIndex);
JSONVal v = this.obj.get(key);
if (startIndex == xPath.size() - 1) {
// end of path.
return v;
} else {
if (v instanceof JSONValString) {
// we still have fragments of a path to recurse down under; but our object here is a List<String>... so cannot dive into it...
System.err.println("Xpath too long for this object..." + xPath);
System.exit(-1);
}
JSONValObj subObj = (JSONValObj) v;
if (subObj.jsonRecVal.size() == 1) {
return subObj.jsonRecVal.get(0).getXPath(startIndex + 1, xPath);
} else {
String keyNum = xPath.get(startIndex + 1);
int index = -1;
try {
index = Integer.parseInt(keyNum);
} catch (NumberFormatException e) {
System.err.format("Xpath needed a list index at position (%d); got path element...", startIndex + 1, xPath.toString());
System.exit(-1);
}
return subObj.jsonRecVal.get(index).getXPath(startIndex + 2, xPath);
}
}
}
}
JSONObj xmlJsonObj;
public PubmedEntry(HashMap<String, List<String>> allXML) {
this.xmlJsonObj = new JSONObj(allXML);
}
public String toJSON() {
return this.xmlJsonObj.toString();
}
@Override
public String toString() {
return toJSON();
}
public JSONVal getXPath(List<String> xPath) {
return this.xmlJsonObj.getXPath(0, xPath);
}
public String getXPathString(List<String> xPath) {
JSONVal v = this.xmlJsonObj.getXPath(0, xPath);
if (v instanceof JSONValString) {
List<String> s = ((JSONValString) v).jsonVal;
if (s.size() == 1)
return s.get(0);
}
return null; // either list is too long or JSONValObj found here...
}
}