org.icij.ftm.Model Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ftm.java Show documentation
Show all versions of ftm.java Show documentation
ICIJ implementation of the Aleph Follow the Money ontology.
The newest version!
package org.icij.ftm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static java.lang.String.format;
import static java.util.stream.Stream.concat;
/**
* Java encapsulation of Map of Map YAML models. It makes easier to manipulate models and centralize code generation rules.
*
* parents instance is the same reference for all Model objects event if it is not static because harder to test and initialize.
*
*
* yaml object is the original yaml map read from FtM models. We use this object for equal/hash methods.
*
*
* mixins instance is here to "help" the mapping of Java classes with multiple inheritance FtM models.
*
*/
public class Model {
private final static Logger logger = LoggerFactory.getLogger(Model.class);
public enum Mode {REQUIRED, FEATURED, FULL;}
final Map parents;
private final Mode mode;
private final Map yaml;
private static final Set mixins = new LinkedHashSet<>(List.of("Asset", "Folder", "PlainText", "HyperText"));
public Model(Map yamlContent) {
this(yamlContent, new HashMap<>());
}
public Model(Map modelMap, Map parents) {
this(modelMap, parents, Mode.REQUIRED);
}
public Model(Map modelMap, Map parents, Mode mode) {
if (modelMap.size() > 1) {
throw new IllegalStateException(format("model should contain one definition, found %s", modelMap.keySet()));
}
this.yaml = Collections.unmodifiableMap(modelMap);
this.parents = Collections.unmodifiableMap(parents);
this.mode = mode;
}
public String name() {
return yaml.keySet().iterator().next();
}
/**
* Recursive method to find a concrete parent (java class) for the current model.
* @return the parent string model name with isConcrete = true from the inheritance tree.
*/
public Optional concreteParent() {
List extendz = getExtends();
List concreteParents = extendz.stream().filter(p -> parents.get(p) == null || parents.get(p).isConcrete()).collect(Collectors.toList());
if (concreteParents.size()>1) {
logger.warn("got 2 concrete parents ({}) for {}, using the first", concreteParents, label());
return Optional.of(concreteParents.get(0));
} else if (concreteParents.isEmpty()) {
// this is fragile. It works because the multiple inheritance is ending with diamonds
logger.debug("got no concrete parent for {}, searching in grand-parents", label());
Set> concreteGrandParents = extendz.stream()
.map(parents::get)
.map(Model::concreteParent)
.filter(Optional::isPresent).collect(Collectors.toSet());
if (!concreteGrandParents.isEmpty()) {
if (concreteGrandParents.size() > 1) {
logger.warn("got {} concrete grand-parents, returning first", concreteGrandParents);
}
return concreteGrandParents.iterator().next();
}
return Optional.empty();
} else {
return Optional.of(concreteParents.get(0));
}
}
public Optional concreteParentModel() {
return concreteParent().map(parents::get);
}
/**
* get the attributes of the model.
*
* - It gets the required attributes if Mode.REQUIRED is provided to constructor (default)
* - It gets the required attributes concatenated to featured attributes if in Mode.FEATURED
* - It gets all the attributes from properties starting with required and featured attributes if in Mode.FULL
*
*
* All duplicates are removed.
*
* @return the list of attributes depending on Model's mode
*/
public List attributes() {
switch (mode) {
case REQUIRED -> {
return required();
}
case FEATURED -> {
return concat(required().stream(), featured().stream()).distinct().toList();
}
default -> {
return concat(concat(required().stream(), featured().stream()), properties().keySet().stream()).distinct().toList();
}
}
}
public List parentsAttributes() {
return new LinkedList<>(parentsAttributes(this));
}
public Map property(String prop) {
return property(prop, this);
}
public String type(String prop) {
Map property = property(prop);
if ("entity".equals(property.get("type"))) {
return (String) property.get("range");
} else {
return (String) property.getOrDefault("type", "string");
}
}
public Map description() {
return (Map) yaml.get(name());
}
public List required() {
return (List) description().getOrDefault("required", new ArrayList<>());
}
private List featured() {
return (List) description().getOrDefault("featured", new ArrayList<>());
}
public List getExtends() {
return (List) description().getOrDefault("extends", new ArrayList<>());
}
public Map properties() {
return (Map) description().getOrDefault("properties", new HashMap<>());
}
public boolean isAbstract() {
return (boolean) description().getOrDefault("abstract", false);
}
public String label() {
return (String) description().get("label");
}
public List getImplementsList() {
return getExtends().stream().filter(p -> parents.get(p) == null || !parents.get(p).isConcrete()).collect(Collectors.toList());
}
/**
* Warning that isConcrete is not !isAbstract. isConcrete is more in a Java sense
* a structure (Class, Record) that can hold values whereas isAbstract is the exact
* value from FtM models.
* This could change in the future: do we need to harmonize this?
* If so how could we avoid to store for example Thing.name in all daughters of Thing?
*
* @return true if the model can be a Class or Record
*/
public boolean isConcrete() {
return !(mixins.contains(name()) || (required().isEmpty() && concreteParent().isEmpty()));
}
private Map property(String prop, Model model) {
Map property = (Map) model.properties().get(prop);
if (property == null) {
List extendz = model.getExtends();
return extendz.stream().map(s -> property(prop, parents.get(s))).filter(Objects::nonNull).findFirst().orElse(null);
} else {
return property;
}
}
private LinkedHashSet parentsAttributes(Model model) {
Optional parentName = model.concreteParent();
if (parentName.isPresent()) {
LinkedHashSet grandParentsAttributes = parentsAttributes(parents.get(parentName.get()));
List parentAttributes = parents.get(parentName.get()).attributes();
grandParentsAttributes.addAll(parentAttributes);
return grandParentsAttributes;
} else {
return new LinkedHashSet<>();
}
}
@Override
public String toString() {
return name();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Model model = (Model) o;
return Objects.equals(yaml, model.yaml);
}
@Override
public int hashCode() {
return Objects.hash(yaml);
}
}