Skip to content

Instantly share code, notes, and snippets.

@yrodiere
Last active January 30, 2017 10:16
Show Gist options
  • Save yrodiere/9ff8fe8a8c7f59c1a051b36db20fbd4d to your computer and use it in GitHub Desktop.
Save yrodiere/9ff8fe8a8c7f59c1a051b36db20fbd4d to your computer and use it in GitHub Desktop.
Thoughts around the "document builder" (which isn't a builder) in Hibernate Search 6.0
/**
* Requirements addressed by this code:
* - Support multiple types of input entities (POJO, JsonObject, ...), each having its own metadata
=> Abstracted by the "ValueProcessor" interface
* - Support multiple types of output documents (Lucene, ES, JMS/JGroups byte arrays, ...)
=> Abstracted by the "DocumentBuilder" interface
* - Do not tie types of input entities to certain types of output documents (the two concepts are independent)
=> The process of walking through an entity is handled separately from the process of producing a document
* - Do as little checks as possible at runtime when inspecting the input source
=> The value processors are built from the metadata when bootstrapping
*/
/*
* TODO: rename ValueProcessor to something less generic.
* Maybe ValueDisassembler? Something else?
*/
/*
* "Facade" exposed to the rest of Hibernate Search (WorkPlanner and so on)
* Could also be named "EntityWalker" or "DocumentBuilder", but I find those names
* less fitting, considering what we expect from this class.
*/
public interface EntityDocumentConverter<E, D> {
D convert(E entity);
}
// Internal, engine impl
class EntityDocumentConverterImpl<E, D> implements EntityDocumentConverter<E, D> {
private ValueProcessor<? super E> rootProcessor;
private Supplier<? extends DocumentBuilder<? extends D>> builderSupplier;
public D convert(E entity) {
DocumentBuilder<? extends D> builder = builderSupplier.get();
rootProcessor.process(entity, builder);
return builder.build();
}
}
// -------------------------------------------------------
// Document building SPI - 1 impl for each output type (Lucene, ES, ...)
public interface DocumentContext {
void addFieldValue(FieldId<T> fieldId, T value);
DocumentContext nest(SingleValueNestingId nestingId);
DocumentContext nest(IndexedValueNestingId nestingId, int index);
}
public interface DocumentBuilder<D> extends DocumentContext {
D build();
}
// Instances are passed to SPI, both when building metadata and when building documents
public interface FieldId<T> {
// Just equals/hashCode here: these IDs are just used as keys
}
// Instances are passed to SPI, both when building metadata and when building documents
public interface SingleValueNestingId {
// Just equals/hashCode here: these IDs are just used as keys
}
// Instances are passed to SPI
public interface IndexedValueNestingId {
// Just equals/hashCode here: these IDs are just used as keys
}
// -------------------------------------------------------
// Entity processing SPI - 1 impl for each input entity type (POJO, com.google.gson.JsonObject, ...)
public interface ValueProcessor<V> {
void process(V value, DocumentContext documentContext);
}
// -------------------------------------------------------
// Bridge SPI
// SPI
// TODO declare the document value type somehow?
public interface SimpleBridge<A extends Annotation> {
void initialize(A annotation) default {};
Object convert(Object value);
}
// SPI
public interface ComplexBridge<A extends Annotation> {
void initialize(MetadataHandle handle, A annotation);
/*
* We probably won't want to expose the full context here, but rather a specific interface,
* which would enable us to expose only the features we actually want to expose
* (no nesting, etc.), and maybe only expose them later.
* Anyway, I left the full context for now.
*/
void set(DocumentContext documentContext, Object value);
}
// Instances are passed to the ComplexBridge SPI
public interface MetadataHandle {
<T> FieldId<T> registerField(String relativeName, Class<T> valueType); // Or maybe some kind of more complex builder, but that's the spirit.
/*
* Those methods could allow complex bridges to define nesting in a very fine-grained fashion.
* We can wait and only implement that kind of thing in 6.1, though.
*/
SingleValueNestingId registerSingleValueNesting(String prefix); // Or maybe some kind of more complex builder, but that's the spirit.
IndexedValueNestingId registerIndexedValueNesting(String prefix); // Or maybe some kind of more complex builder, but that's the spirit.
}
// -------------------------------------------------------
// Internal implementation (may be exposed to service providers as a util)
final class CompositeProcessor<V> implements ValueProcessor<V> {
private final Collection<ValueProcessor<? super V>> processors;
public void process(V value, DocumentContext documentContext) {
for (ValueProcessor<? super V> processor : processors) {
processor.process(value, documentContext);
}
}
}
// Internal implementation
final class SimpleBridgeValueProcessor<V> implements ValueProcessor<V> {
private final SimpleBridge<?> bridge;
private final FieldId<Object> fieldId;
public void process(V value, DocumentContext documentContext) {
documentContext.addFieldValue(fieldId, bridge.convert(value));
}
}
// Internal implementation
final class ComplexBridgeValueProcessor<V> implements ValueProcessor<V> {
private final ComplexBridge<?> bridge;
public void process(V value, DocumentContext documentContext) {
bridge.set(documentContext, value);
}
}
// -------------------------------------------------------
// Implementations of the entity processing SPI for POJO (samples)
// H: property holder type, P: property value type
class JavaPropertyValueProcessor<H, P> implements ValueProcessor<H> {
private final XProperty<H, P> property;
// The processor below makes use of the field bridge (see ComplexBridgeProcessor for instance)
private final ValueProcessor<P> propertyValueProcessor;
public void process(H holderValue, DocumentContext documentContext) {
if ( holderValue != null ) {
propertyValueProcessor.process(property.get(holderValue), documentContext);
}
}
}
// H: property holder type, P: property value type
class GsonPropertyValueProcessor implements ValueProcessor<JsonObject> {
private final String propertyName;
// The processor below makes use of the field bridge (see ComplexBridgeProcessor for instance)
private final ValueProcessor<JsonElement> propertyValueProcessor;
public void process(JsonObject holderValue, DocumentContext documentContext) {
if (holderValue.has(propertyName)) {
propertyValueProcessor.process(holderValue.get(propertyName), documentContext);
}
}
}
// Use for single-value @IndexedEmbeddeds
class SingleNestedValueProcessor<V> implements ValueProcessor<V> {
// The processor below is composed of all processors for nested fields
private final CompositeValueProcessor<V> fieldsProcessor;
private final SingleValueNestingId nestingId;
public void process(V value, DocumentContext documentContext) {
if ( value != null ) {
DocumentContext nestedContext = documentContext.nest(nestingId);
fieldsProcessor.process(value, nestedContext);
}
}
}
// Use for multi-value, Iterable-implementing @IndexedEmbeddeds
class IterableNestedValueProcessor<I extends Iterable<V>, V> implements ValueProcessor<I> {
// The processor below is composed of all processors for nested fields
private final CompositeValueProcessor<V> fieldsProcessor;
private final IndexedValueNestingId nestingId;
public void process(I iterable, DocumentContext documentContext) {
if ( iterable != null ) {
int index = 0;
for ( V value : iterable ) {
DocumentContext nestedContext = documentContext.nest(nestingId, index);
fieldsProcessor.process(value, nestedContext);
++index;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment