Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ddi indexing #9

Merged
merged 6 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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