package models;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.fge.jsonschema.core.exceptions.ProcessingException;
import com.github.fge.jsonschema.core.report.ListProcessingReport;
import com.github.fge.jsonschema.core.report.ProcessingMessage;
import com.github.fge.jsonschema.core.report.ProcessingReport;
import com.github.fge.jsonschema.main.JsonSchema;
import com.github.fge.jsonschema.main.JsonSchemaFactory;
import helpers.FilesConfig;
import helpers.JsonLdConstants;
import org.apache.commons.io.IOUtils;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;
import org.apache.jena.riot.Lang;
import org.apache.jena.riot.RDFDataMgr;
import play.Logger;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
public class Resource extends HashMap<String, Object>implements Comparable<Resource> {
/**
*
*/
private static final long serialVersionUID = -6177433021348713601L;
// identified ("primary") data types that get an ID
private static final List<String> mIdentifiedTypes = new ArrayList<>(Arrays.asList(
"Organization", "Event", "Person", "Action", "WebPage", "Article", "Service", "ConceptScheme", "Concept",
"Comment"));
private static JsonNode mSchemaNode = null;
static {
try {
mSchemaNode = new ObjectMapper().readTree(Paths.get(FilesConfig.getSchema()).toFile());
} catch (IOException e) {
Logger.error("Could not read schema", e);
}
}
/**
* Constructor which sets up a random UUID.
*
* @param type
* The type of the resource.
*/
public Resource(final String type) {
this(type, null);
}
/**
* Constructor.
*
* @param aType
* The type of the resource.
* @param aId
* The id of the resource.
*/
public Resource(final String aType, final String aId) {
if (null != aType) {
this.put(JsonLdConstants.TYPE, aType);
}
if (null != aId) {
this.put(JsonLdConstants.ID, aId);
} else if (mIdentifiedTypes.contains(aType)) {
this.put(JsonLdConstants.ID, generateId());
}
}
private static String generateId() {
return "urn:uuid:" + UUID.randomUUID().toString();
}
/**
* Convert a Map of String/Object to a Resource, assuming that all Object
* values of the map are properly represented by the toString() method of
* their class.
*
* @param aProperties
* The map to create the resource from
* @return a Resource containing all given properties
*/
@SuppressWarnings("unchecked")
public static Resource fromMap(Map<String, Object> aProperties) {
if (aProperties == null) {
return null;
}
String type = (String) aProperties.get(JsonLdConstants.TYPE);
String id = (String) aProperties.get(JsonLdConstants.ID);
Resource resource = new Resource(type, id);
for (Map.Entry<String, Object> entry : aProperties.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (key.equals(JsonLdConstants.ID) && !mIdentifiedTypes.contains(type)) {
continue;
}
if (value instanceof Map<?, ?>) {
resource.put(key, Resource.fromMap((Map<String, Object>) value));
} else if (value instanceof List<?>) {
List<Object> vals = new ArrayList<>();
for (Object v : (List<?>) value) {
if (v instanceof Map<?, ?>) {
vals.add(Resource.fromMap((Map<String, Object>) v));
} else {
vals.add(v);
}
}
resource.put(key, vals);
} else {
resource.put(key, value);
}
}
return resource;
}
public static Resource fromJson(JsonNode aJson) {
Map<String, Object> resourceMap = new ObjectMapper().convertValue(aJson,
new TypeReference<HashMap<String, Object>>() {
});
return fromMap(resourceMap);
}
public static Resource fromJson(String aJsonString) {
try {
return fromJson(new ObjectMapper().readTree(aJsonString));
} catch (IOException e) {
Logger.error("Could not read resource from JSON", e);
return null;
}
}
public static Resource fromJson(InputStream aInputStream) throws IOException {
String json = IOUtils.toString(aInputStream, "UTF-8");
aInputStream.close();
return fromJson(json);
}
public ProcessingReport validate() {
ProcessingReport report = new ListProcessingReport();
try {
String type = this.getAsString(JsonLdConstants.TYPE);
if (null == type) {
report.error(new ProcessingMessage()
.setMessage("No type found for ".concat(this.toString()).concat(", cannot validate")));
} else if (null != mSchemaNode) {
JsonSchema schema = JsonSchemaFactory.byDefault().getJsonSchema(mSchemaNode, "/definitions/".concat(type));
report = schema.validate(toJson());
} else {
Logger.warn("No JSON schema present, validation disabled.");
}
} catch (ProcessingException e) {
e.printStackTrace();
}
return report;
}
/**
* Get a JsonNode representation of the resource.
*
* @return JSON JsonNode
*/
public JsonNode toJson() {
return new ObjectMapper().convertValue(this, JsonNode.class);
}
/**
* Get an RDF representation of the resource.
*
* @return Model The RDF Model
*/
public Model toModel() {
Model model = ModelFactory.createDefaultModel();
InputStream stream = new ByteArrayInputStream(this.toString().getBytes(StandardCharsets.UTF_8));
RDFDataMgr.read(model, stream, Lang.JSONLD);
return model;
}
/**
* Get a JSON string representation of the resource.
*
* @return JSON string
*/
@Override
public String toString() {
ObjectMapper mapper = new ObjectMapper();
String output;
try {
output = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(toJson());
} catch (JsonProcessingException e) {
output = toJson().toString();
e.printStackTrace();
}
return output;
}
public String getAsString(final Object aKey) {
Object result = get(aKey);
return (result == null) ? null : result.toString();
}
public List<Resource> getAsList(final Object aKey) {
List<Resource> list = new ArrayList<>();
Object result = get(aKey);
if (null == result || !(result instanceof List<?>)) {
return list;
}
for (Object value : (List<?>) result) {
if (value instanceof Resource) {
list.add((Resource) value);
}
}
return list;
}
public List<String> getIdList(final Object aKey) {
List<String> ids = new ArrayList<>();
Object result = get(aKey);
if (null == result || !(result instanceof List<?>)) {
return ids;
}
for (Object value : (List<?>) result) {
if (value instanceof Resource) {
ids.add(((Resource) value).getAsString(JsonLdConstants.ID));
}
}
return ids;
}
public Resource getAsResource(final Object aKey) {
Object result = get(aKey);
return (null == result || !(result instanceof Resource)) ? null : (Resource) result;
}
public Map<?, ?> getAsMap(final String aKey) {
Object result = get(aKey);
return (null == result || !(result instanceof Map<?, ?>)) ? null : (Resource) result;
}
@SuppressWarnings("unchecked")
@Override
public boolean equals(final Object aOther) {
if (!(aOther instanceof Resource)) {
return false;
}
final Resource other = (Resource) aOther;
if (other.size() != this.size()) {
return false;
}
final Iterator<Map.Entry<String, Object>> thisIt = this.entrySet().iterator();
while (thisIt.hasNext()) {
final Map.Entry<String, Object> pair = thisIt.next();
if (pair.getValue() instanceof List<?>) {
if (!(other.get(pair.getKey()) instanceof List<?>)) {
return false;
}
List<Object> list = (List<Object>) pair.getValue();
List<Object> otherList = (List<Object>) other.get(pair.getKey());
if (list.size() != otherList.size() || !list.containsAll(otherList) || !otherList.containsAll(list)) {
return false;
}
} else if (!pair.getValue().equals(other.get(pair.getKey()))) {
return false;
}
}
return true;
}
public boolean hasId() {
return containsKey(JsonLdConstants.ID);
}
public String getId() {
return getAsString(JsonLdConstants.ID);
}
public String getType() {
return getAsString(JsonLdConstants.TYPE);
}
public void merge(Resource aOther) {
for (Entry<String, Object> entry : aOther.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
@Override
public int compareTo(Resource aOther) {
if (hasId() && aOther.hasId()) {
return getAsString(JsonLdConstants.ID).compareTo(aOther.getAsString(JsonLdConstants.ID));
}
return toString().compareTo(aOther.toString());
}
/**
* Get a flat String representation of this Resource, whereby any keys are
* dismissed. Furthermore, value information can be dropped by specifying its
* fields respectively keys names. This is useful for "static" values like e.
* g. of the field "type".
*
* @param aFieldSeparator
* a String indicating the beginning of new information, resulting
* from a new Resource's field. Note, that this separator might also
* appear within the Resource's fields themselves.
* @param aDropFields
* a List<String> specifying, which field's values should be excluded
* from the resulting String representation
* @return a flat String representation of this Resource. In case there is no
* information to be returned, the result is an empty String.
*/
public String getValuesAsFlatString(String aFieldSeparator, List<String> aDropFields) {
String fieldSeparator = null == aFieldSeparator ? "" : aFieldSeparator;
StringBuffer result = new StringBuffer();
for (Entry<String, Object> entry : entrySet()) {
if (!aDropFields.contains(entry.getKey())) {
Object value = entry.getValue();
if (value instanceof String) {
if (!"".equals(value)) {
result.append(value).append(fieldSeparator).append(" ");
}
} //
else if (value instanceof Resource) {
result.append(((Resource) value).getValuesAsFlatString(fieldSeparator, aDropFields));
} //
else if (value instanceof List<?>) {
result.append("[");
for (Object innerValue : (List<?>) value) {
if (innerValue instanceof String) {
if (!"".equals(innerValue)) {
result.append(innerValue).append(fieldSeparator).append(" ");
}
} //
else if (innerValue instanceof Resource) {
result.append(
((Resource) innerValue).getValuesAsFlatString(fieldSeparator, aDropFields));
}
}
if (result.length() > 1 && result.charAt(result.length() - 1) != '[') {
result.delete(result.length() - 2, result.length());
}
result.append("]").append(fieldSeparator).append(" ");
}
}
}
// delete last separator
if (result.length() > 1) {
result.delete(result.length() - 2, result.length());
}
return result.toString();
}
public String getNestedFieldValue(final String aNestedKey, final Locale aPreferredLocale){
final String[] split = aNestedKey.split("\\.", 2);
if (split.length == 0){
return null;
}
if (split.length == 1){
Object o = get(split[0]);
if (o != null) {
return o.toString();
}
return null;
}
// split.length == 2
final Object o = get(split[0]);
if (o instanceof ArrayList<?>){
String next = getNestedValueOfList(split[1], (ArrayList<?>) o, aPreferredLocale);
if (next != null) return next;
} //
else if (o instanceof Resource){
Resource resource = (Resource) o;
if (resource.size() == 0){
return null;
}
return resource.getNestedFieldValue(split[1], aPreferredLocale);
}
return null;
}
private String getNestedValueOfList(final String aKey, final ArrayList<?> aList, final Locale aPreferredLocale) {
Object next;
final Locale fallbackLocale = Locale.ENGLISH;
String fallback1 = null;
String fallback2 = null;
String fallback3 = null;
for (Iterator it = aList.iterator(); it.hasNext(); ){
next = it.next();
if (next instanceof Resource){
Resource resource = (Resource) next;
Object language = resource.get("@language");
if (language.equals(aPreferredLocale.getLanguage())){
return resource.getNestedFieldValue(aKey, aPreferredLocale);
}
if (language == null){
fallback1 = resource.getNestedFieldValue(aKey, aPreferredLocale);
}
else if (language.equals(fallbackLocale.getLanguage())){
fallback2 = resource.getNestedFieldValue(aKey, fallbackLocale);
}
else {
fallback3 = resource.getNestedFieldValue(aKey, Locale.forLanguageTag(language.toString()));
}
}
}
return (fallback1 != null) ? fallback1 : (fallback2 != null) ? fallback2 : fallback3;
}
/**
* Counts the number of subfields matching the argument string.
* A simple wildcard ("*") defines 1 level of arbitrary path specifiers.
* A double wildcard ("**") defines 0-n levels of arbitrary path specifiers.
* Wildcard string combinations ("*xyz" or "xyz*" etc.) are not supported so far.
* Arrays can not be specified by position
* @param aSubfieldPath Specifier for the subfields to be counted.
* @return The number of specified subfields.
*/
public Integer getNumberOfSubFields(String aSubfieldPath) {
String[] pathElements = aSubfieldPath.split("\\.");
return getNumberOfSubFields(pathElements);
}
private Integer getNumberOfSubFields(String[] aPathElements) {
int count = 0;
if (aPathElements.length == 0){
return count;
}
String matchElement = null;
String pathElement = aPathElements[0];
String[] remainingElements;
if (pathElement.equals("**")){
remainingElements = aPathElements;
if (aPathElements.length < 3){
matchElement = remainingElements[remainingElements.length-1];
}
}
else{
remainingElements = Arrays.copyOfRange(aPathElements, 1, aPathElements.length);
if (remainingElements.length == 0){
matchElement = pathElement;
}
}
for (Entry<String, Object> entry : entrySet()) {
if (entry.getValue() instanceof Resource){
Resource innerResource = ((Resource) entry.getValue());
count += innerResource.getNumberOfSubFields(remainingElements);
} //
else if (entry.getValue() instanceof List<?>) {
for (Object innerObject : (List<?>) entry.getValue()) {
if (innerObject instanceof Resource){
count += ((Resource)innerObject).getNumberOfSubFields(remainingElements);
}
}
}
if (entry.getKey().equals(matchElement) || matchElement.equals("**")){
count++;
}
}
return count;
}
}