Skip to content

Commit

Permalink
feat: ddi indexing (#9)
Browse files Browse the repository at this point in the history
* feat: add utility methods

* feat: indexing class for ddi objects

* feat: typed get methods in index

* chore: bump version
  • Loading branch information
nsenave authored Jul 19, 2024
1 parent 074c133 commit bd13e7d
Show file tree
Hide file tree
Showing 11 changed files with 498 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ env:

jobs:
test:
if: ${{ (github.event.pull_request.draft == false) && !contains(github.event.pull_request.labels.*.name, 'publish-snapshot')
if: ${{ (github.event.pull_request.draft == false) && !contains(github.event.pull_request.labels.*.name, 'publish-snapshot') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,35 @@ This example can be applied to any DDI Lifecycle object: `CodeList`, `Sequence`,
Useful link: [DDI model documentation](https://ddialliance.github.io/ddimodel-web/DDI-L-3.3/)
## Other features
### Indexing of a DDI object
You can index the objects contained within a DDI object.
```java
DDIIndex ddiIndex = new DDIIndex();
// Perform indexing
ddiIndex.indexDDIObject(someDDIObject);
// Get an object within the index
AbstractIdentifiableType innerObject = ddiIndex.get("some-inner-object-id");
// The result can be typed
VariableType variable = ddiIndex.get("some-variable-id", VariableType.class);
// Get the parent object in the hierarchy
VariableSchemeType variableScheme = ddiIndex.getParent("some-variable-id", VariableScheme.class);
```
### Utilities
XMLBeans API is pretty verbose, a utility class of the lib offers some QOE methods:
```java
VariableType variable = VariableType.Factory.newInstance();
DDIUtils.setIdValue(variable, "foo-id");
DDIUtils.getIdValue(variable) // "foo-id"
DDIUtils.ddiToString(variable) // "VariableTypeImpl[id=foo-id]"
```
## Requests
If you have a question or bug report, feel free to open an issue.
If you have a question, request or bug report, feel free to open an issue.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins {

allprojects {
group = "fr.insee.ddi"
version = "1.0.0"
version = "1.1.0"
}

tasks.register("printVersion") {
Expand Down
5 changes: 3 additions & 2 deletions model/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Generated sources
src/main/java/*
src/main/java/fr/insee/ddi/lifecycle33/*
src/main/java/org/*
# Generated resources
src/main/resources/org/apache/xmlbeans/*
src/main/resources/org/apache/xmlbeans/*
2 changes: 2 additions & 0 deletions model/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ dependencies {
api("org.apache.xmlbeans:xmlbeans:5.2.0")
// This dependency is used internally, and not exposed to consumers on their own compile classpath.
implementation("org.apache.logging.log4j:log4j-core:2.21.1")
//
implementation("org.springframework:spring-beans:6.1.11")

// Use JUnit Jupiter for testing.
testImplementation("org.junit.jupiter:junit-jupiter:5.9.3")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package fr.insee.ddi.exception;

/**
* Exception to be thrown if duplicate identifiers are detected in a DDI object.
*/
public class DuplicateIdException extends RuntimeException {

public DuplicateIdException(String message) {
super(message);
}

}
16 changes: 16 additions & 0 deletions model/src/main/java/fr/insee/ddi/exception/IndexingException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package fr.insee.ddi.exception;

/**
* Exception to be thrown if an unexpected exception occurs during DDI indexing.
*/
public class IndexingException extends RuntimeException {

public IndexingException(String message) {
super(message);
}

public IndexingException(String message, Exception e) {
super(message, e);
}

}
194 changes: 194 additions & 0 deletions model/src/main/java/fr/insee/ddi/index/DDIIndex.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package fr.insee.ddi.index;

import fr.insee.ddi.exception.DuplicateIdException;
import fr.insee.ddi.exception.IndexingException;
import fr.insee.ddi.lifecycle33.instance.DDIInstanceDocument;
import fr.insee.ddi.lifecycle33.reusable.AbstractIdentifiableType;
import fr.insee.ddi.utils.DDIUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.core.convert.TypeDescriptor;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.util.*;

/**
* Class designed to store all DDI identifiable objects within a DDI object in a flat map.
* Also contains a parents map that make the link between each object and its parent.
*/
public class DDIIndex {

private static final Logger log = LogManager.getLogger();

/** Index of DDI identifiable objects.
* Key: a DDI object identifier
* Value: the DDI object with corresponding identifier. */
private Map<String, AbstractIdentifiableType> index;

/** Map containing nesting relationships between objects in index.
* Key: a DDI object identifier
* Value: the identifier of its parent object */
private Map<String, String> parentsMap;

private void setup() {
index = new HashMap<>();
parentsMap = new HashMap<>();
}

/**
* Stores all DDI identifiable objects that are in the given DDI document in the index.
* @param ddiInstanceDocument A DDIInstanceDocument.
* @throws DuplicateIdException if two objects with the same identifier are found.
*/
public void indexDDI(DDIInstanceDocument ddiInstanceDocument) throws DuplicateIdException {
indexDDIObject(ddiInstanceDocument.getDDIInstance());
}

/**
* Stores all DDI identifiable objects that are in the given DDI object in the index.
* @param ddiObject A DDI identifiable object.
* @throws DuplicateIdException if two objects with the same identifier are found.
*/
public void indexDDIObject(AbstractIdentifiableType ddiObject) throws DuplicateIdException {
log.info("Indexing DDI object {}...", DDIUtils.ddiToString(ddiObject));
setup();
recursiveIndexing(ddiObject);
log.info("Finished indexing of DDI object.");
}

/**
* In DDI, the most generic object is the AbstractIdentifiableType
* (i.e. each DDI object that has an id is an AbstractIdentifiableType).
* This method recursively path through all AbstractIdentifiableType objects within the given object.
* @param ddiObject A AbstractIdentifiableType object.
*/
private void recursiveIndexing(AbstractIdentifiableType ddiObject) throws IndexingException, DuplicateIdException {

// Get the DDI object id
String ddiObjectId = !ddiObject.getIDList().isEmpty() ? ddiObject.getIDArray(0).getStringValue() : null;
if (ddiObjectId == null)
throw new IndexingException("DDI object with null identifier encountered while indexing.");

// Put the object in the map under the id (there should never be duplicate ids in DDI documents)
index.merge(ddiObjectId, ddiObject, (oldDDIObject, newDDIObject) -> {
throw new DuplicateIdException(String.format("Duplicate ID \"%s\" found in given DDI.", ddiObjectId));
});

// Use Spring BeanWrapper to iterate on object property descriptors
BeanWrapper beanWrapper = new BeanWrapperImpl(ddiObject);
Iterator<PropertyDescriptor> iterator = Arrays.stream(beanWrapper.getPropertyDescriptors())
.filter(propertyDescriptor -> !propertyDescriptor.getName().equals("class"))
.iterator();
while (iterator.hasNext()) {
PropertyDescriptor propertyDescriptor = iterator.next();

// In DDI classes, everything is in a list
if (List.class.isAssignableFrom(propertyDescriptor.getPropertyType())) {

// Use Spring TypeDescriptor to determine the content type of the list
TypeDescriptor typeDescriptor = beanWrapper.getPropertyTypeDescriptor(propertyDescriptor.getName());
assert typeDescriptor != null;
Class<?> listContentType = typeDescriptor.getResolvableType().getGeneric(0).getRawClass();

// In some DDI objects, there are some methods (generated by xmlbeans) that does not interest us
assert listContentType != null || propertyDescriptor.getName().equals("listValue")
|| propertyDescriptor.getName().equals("limitArrayIndex");

// Check that the list content applies for AbstractIdentifiableType
if (listContentType != null && AbstractIdentifiableType.class.isAssignableFrom(listContentType)) {

// Now that we have what we want, index list content
indexListContent(ddiObject, ddiObjectId, propertyDescriptor);
}
}
}

}

private void indexListContent(AbstractIdentifiableType ddiObject, String ddiObjectId, PropertyDescriptor propertyDescriptor) {
try {
// Iteration on each object in the list.
@SuppressWarnings("unchecked") // https://stackoverflow.com/a/4388173/13425151
Collection<AbstractIdentifiableType> ddiCollection = (Collection<AbstractIdentifiableType>) propertyDescriptor.getReadMethod().invoke(ddiObject);
for (AbstractIdentifiableType ddiObject2 : ddiCollection) {
// Keep track of link between nested objects
parentsMap.put(ddiObject2.getIDArray(0).getStringValue(), ddiObjectId);
// Recursive call of the function
recursiveIndexing(ddiObject2);
}
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IndexingException(String.format(
"Error when calling read method from property descriptor '%s' in class %s.",
propertyDescriptor.getName(), ddiObject.getClass()),
e);
}
}

/** Returns the inner index. */
public Map<String, AbstractIdentifiableType> getIndex() {
return index;
}

/**
* Returns the DDI object corresponding to the given identifier.
* @param ddiObjectId String identifier value.
* @return The DDI object corresponding to the given identifier.
* @throws NoSuchElementException if there is no object under given identifier.
*/
public AbstractIdentifiableType get(String ddiObjectId) {
AbstractIdentifiableType object = index.get(ddiObjectId);
if (object == null)
throw new NoSuchElementException(String.format("Index has no object with id '%s'.", ddiObjectId));
return object;
}

/**
* Returns the DDI object corresponding to the given identifier, cast in the given type.
* @param ddiObjectId String identifier value.
* @param clazz Class into which the result should be cast.
* @return The DDI object corresponding to the given identifier, cast in the given type.
* @param <T> Subtype of DDI AbstractIdentifiableType.
* @throws NoSuchElementException if there is no object under given identifier.
* @throws ClassCastException if the given object cannot be cast to the given type.
*/
public <T extends AbstractIdentifiableType> T get(String ddiObjectId, Class<T> clazz) {
AbstractIdentifiableType object = this.get(ddiObjectId);
if (! clazz.isInstance(object))
throw new ClassCastException(String.format(
"Index object with id '%s' is of type %s that cannot be cast to %s.",
ddiObjectId, ddiObjectId.getClass(), clazz));
return clazz.cast(object);
}

/**
* Checks if the given identifier is present in the index.
* @param ddiObjectId String identifier value.
* @return True if the index contains the given identifier.
*/
public boolean containsId(String ddiObjectId) {
return index.containsKey(ddiObjectId);
}

/**
* Returns the parent object of DDI object with given identifier.
* @param ddiObjectId String identifier value.
* @throws NoSuchElementException if the parent object for given identifier cannot be found.
*/
public AbstractIdentifiableType getParent(String ddiObjectId) {
return this.get(parentsMap.get(ddiObjectId));
}

/**
* Returns the parent object of DDI object with given identifier.
* @param ddiObjectId String identifier value.
* @throws NoSuchElementException if the parent object for given identifier cannot be found.
* @throws ClassCastException if the given object cannot be cast to the given type.
*/
public <T extends AbstractIdentifiableType> T getParent(String ddiObjectId, Class<T> clazz) {
return this.get(parentsMap.get(ddiObjectId), clazz);
}

}
49 changes: 49 additions & 0 deletions model/src/main/java/fr/insee/ddi/utils/DDIUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package fr.insee.ddi.utils;

import fr.insee.ddi.lifecycle33.reusable.AbstractIdentifiableType;
import fr.insee.ddi.lifecycle33.reusable.IDType;

/**
* Utility class that provide some methods for DDI objects.
*/
public class DDIUtils {

private DDIUtils() {}

/**
* Returns a better representation than the "toString" method for a DDI object.
* @param ddiObject A DDI object.
* @return String representation of the object.
*/
public static String ddiToString(Object ddiObject) {
String className = ddiObject.getClass().getSimpleName();
if (! (ddiObject instanceof AbstractIdentifiableType ddiIdentifiableObject))
return className;
if (ddiIdentifiableObject.getIDList().isEmpty())
return className + "[id=null]";
return className + "[id=" + ddiIdentifiableObject.getIDArray(0).getStringValue() + "]";
}

/**
* Returns the identifier of the given DDI object.
* @param ddiIdentifiableObject A DDI identifiable object.
* @return String value of the object identifier.
*/
public static String getIdValue(AbstractIdentifiableType ddiIdentifiableObject) {
if (ddiIdentifiableObject.getIDList().isEmpty())
return null;
return ddiIdentifiableObject.getIDArray(0).getStringValue();
}

/**
* Sets the identifier value of the given DDI object.
* @param ddiIdentifiableObject A DDI identifiable object.
* @param id String identifier value.
*/
public static void setIdValue(AbstractIdentifiableType ddiIdentifiableObject, String id) {
if (ddiIdentifiableObject.getIDList().isEmpty())
ddiIdentifiableObject.getIDList().add(IDType.Factory.newInstance());
ddiIdentifiableObject.getIDArray(0).setStringValue(id);
}

}
Loading

0 comments on commit bd13e7d

Please sign in to comment.