
sirius.search.properties.Property Maven / Gradle / Ivy
Show all versions of sirius-search Show documentation
/*
* Made with all the love in the world
* by scireum in Remshalden, Germany
*
* Copyright by scireum GmbH
* http://www.scireum.de - [email protected]
*/
package sirius.search.properties;
import org.elasticsearch.common.xcontent.XContentBuilder;
import sirius.kernel.commons.Explain;
import sirius.kernel.commons.Value;
import sirius.kernel.di.Injector;
import sirius.kernel.health.Exceptions;
import sirius.kernel.nls.NLS;
import sirius.search.Entity;
import sirius.search.EntityDescriptor;
import sirius.search.IndexAccess;
import sirius.search.annotations.Analyzed;
import sirius.search.annotations.IndexMode;
import sirius.search.annotations.Indexed;
import sirius.search.annotations.NotNull;
import sirius.search.annotations.Transient;
import sirius.web.http.WebContext;
import sirius.web.security.UserContext;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* A property describes how a field is persisted into the database and loaded back.
*
* It also takes care of creating an appropriate mapping. Properties are generated by {@link PropertyFactory}
* instances which generate a property for a given field of a class.
*
* Field-specific options are investigated in the following order:
*
* - subclasses have overridden the isXXX() method
* - fields have the value set in the annotation (!= {@link ESOption#DEFAULT})
* - subclasses have overridden the isDefaultXXX() method
* - the default values provided in this class' isDefaultXXX() methods
*
*/
public abstract class Property {
/**
* Contains the underlying field for which the property was created
*/
private final Field field;
/**
* Determines if null is accepted as a value for this property
*/
private final boolean nullAllowed;
/**
* Determines whether the property should be exclude from the _source field
*/
private final boolean excludeFromSource;
/**
* Determines whether the property should be stored separately
*/
private final ESOption stored;
/**
* Determines whether the property is indexed (e.g. searchable)
*/
private final ESOption indexed;
/**
* Determines whether the property should be included into the _all field
*/
protected final ESOption includeInAll;
/**
* Determines whether doc_values should be enabled for the property
*/
protected final ESOption docValuesEnabled;
/**
* Determines whether the property is an inner property (e.g. {@link #indexed} and {@link #stored} should not be
* written to the mapping)
*/
private boolean innerProperty;
/**
* Generates a new property for the given field
*
* @param field the underlying field from which the property was created
*/
protected Property(Field field) {
this.field = field;
this.field.setAccessible(true);
this.nullAllowed =
!field.getType().isPrimitive() && !field.isAnnotationPresent(NotNull.class) && isDefaultNullAllowed();
this.excludeFromSource = field.isAnnotationPresent(IndexMode.class) ?
field.getAnnotation(IndexMode.class).excludeFromSource() :
isDefaultExcludeFromSource();
this.indexed = readAnnotationValue(IndexMode.class, IndexMode::indexed, this::isDefaultIndexed);
this.stored = readAnnotationValue(IndexMode.class, IndexMode::stored, this::isDefaultStored);
this.includeInAll = readAnnotationValue(IndexMode.class, IndexMode::includeInAll, this::isDefaultIncludeInAll);
this.docValuesEnabled =
readAnnotationValue(IndexMode.class, IndexMode::docValues, this::isDefaultDocValuesEnabled);
}
/**
* Initializes the property (field) of the given entity.
*
* @param entity the entity to initialize
* @throws IllegalAccessException in case of an error when initializing the entity
*/
public void init(Entity entity) throws IllegalAccessException {
// Empty by default as this is optional
}
/**
* Provides access to the underlying java field
*
* @return the field upon which the property was created
*/
public Field getField() {
return field;
}
/**
* Returns a translated title for the property by resolving
* SimpleClassName.fieldName via {@link sirius.kernel.nls.NLS}.
*
* @return the translated name or label for this property.
*/
public String getFieldTitle() {
return NLS.get(field.getDeclaringClass().getSimpleName() + "." + getName());
}
/**
* Some properties auto-create a value and therefore no setter for the given field should be defined.
*
* @return false if no setter for this property should be present, true otherwise
*/
public boolean acceptsSetter() {
return true;
}
/**
* Returns the name of the property
*
* @return the name of the property, which is normally just the field name
*/
public String getName() {
return field.getName();
}
public boolean isInnerProperty() {
return innerProperty;
}
public void setInnerProperty(boolean innerProperty) {
this.innerProperty = innerProperty;
}
/**
* Determines if null values are accepted by this property.
*
* Subclasses may override this method to ignore the value from the {@link NotNull} annotation.
*
* @return true if the property accepts null values, false otherwise
*/
public boolean isNullAllowed() {
return nullAllowed;
}
/**
* Determines whether this property should be excluded from _source field.
*
* Subclasses may override this method to ignore the value from the {@link IndexMode} annotation.
*
* @return true if the property should be excluded from the _source field, false otherwise.
*/
public boolean isExcludeFromSource() {
return excludeFromSource;
}
/**
* Determines if this property is stored as separate field in its original form.
*
* Subclasses may override this method to ignore the value from the {@link IndexMode} annotation.
*
* @return true if the raw values is stored as extra field in ElasticSearch, false otherwise
*/
protected ESOption isStored() {
return stored;
}
/**
* Determines if this property is stored as separate field in its original form.
*
* Subclasses may override this method to ignore the value from the {@link IndexMode} annotation.
*
* @return true if the raw value is stored as extra field in ElasticSearch, false otherwise
*/
protected ESOption isIndexed() {
return indexed;
}
/**
* Determines if this value should not be included in the _all field.
*
* Subclasses may override this method to ignore the value from the {@link IndexMode} annotation.
*
* @return true if the value should not be included in the all field, false otherwise.
*/
protected ESOption isIncludeInAll() {
return includeInAll;
}
/**
* Determines whether doc_values should be enabled for this property.
*
* Subclasses may override this method to ignore the value from the {@link IndexMode} annotation.
*
* @return true if doc_values should be enabled for this property, false otherwise.
*/
public ESOption isDocValuesEnabled() {
return docValuesEnabled;
}
/**
* Determines if null values are accepted by this property by default (if the {@link NotNull} annotation is
* not present).
*
* Subclasses may override this method to set the default value for their specific property type.
*
* @return true if the property accepts null values by default, false otherwise
*/
public boolean isDefaultNullAllowed() {
return true;
}
/**
* Determines whether this property should be excluded from _source field by default (if the {@link IndexMode}
* annotation is not present).
*
* Subclasses may override this method to set the default value for their specific property type.
*
* @return true if the property should be excluded from the _source field, false otherwise.
*/
public boolean isDefaultExcludeFromSource() {
return false;
}
/**
* Determines if this property is stored as separate field in its original form by default (if the {@link IndexMode}
* annotation is not present).
*
* Subclasses may override this method to set the default value for their specific property type.
*
* @return true if the raw values is stored as extra field in ElasticSearch, false otherwise
*/
protected ESOption isDefaultStored() {
return ESOption.FALSE;
}
/**
* Determines if this property is stored as separate field in its original form by default (if the {@link IndexMode}
* annotation is not present).
*
* Subclasses may override this method to set the default value for their specific property type.
*
* @return true if the raw values is stored as extra field in ElasticSearch, false otherwise
*/
protected ESOption isDefaultIndexed() {
return ESOption.TRUE;
}
/**
* Determines whether norms should be enabled for this property.
*
* Subclasses may override this method to set the default value for their specific property type.
*
* @return true if the value should not be included in the all field, false otherwise.
*/
protected ESOption isDefaultIncludeInAll() {
return ESOption.FALSE;
}
/**
* Determines whether doc_values should be enabled for this property.
*
* Subclasses may override this method to set the default value for their specific property type.
*
* @return true if doc_values should be enabled for this property, false otherwise.
*/
protected ESOption isDefaultDocValuesEnabled() {
// disable doc_values in not indexed properties by default
if (isIndexed() == ESOption.FALSE) {
return ESOption.FALSE;
}
return ESOption.ES_DEFAULT;
}
/**
* Returns the data type used in the mapping
*
* @return the name of the data type used in the mapping
*/
@SuppressWarnings("squid:S3400")
@Explain("To be overwritten by subclasses.")
protected String getMappingType() {
return "keyword";
}
protected void addMappingProperties(XContentBuilder builder) throws IOException {
builder.field("type", getMappingType());
if (!isInnerProperty()) {
if (isStored() != ESOption.ES_DEFAULT) {
builder.field("store", isStored());
}
if (isIndexed() != ESOption.ES_DEFAULT) {
builder.field("index", isIndexed());
}
}
if (!isMapProperty() && isIncludeInAll() == ESOption.TRUE) {
builder.field("copy_to", "custom_all");
}
if (isDocValuesEnabled() != ESOption.ES_DEFAULT) {
builder.field("doc_values", isDocValuesEnabled());
}
}
private boolean isMapProperty() {
return this instanceof StringMapProperty || this instanceof StringListMapProperty;
}
/**
* Generates the representation of the entities field value to be stored in the database.
*
* @param entity the entity which field value is to be stored
* @return the storable representation of the value
*/
public Object writeToSource(Entity entity) {
try {
return transformToSource(field.get(entity));
} catch (IllegalAccessException e) {
Exceptions.handle(IndexAccess.LOG, e);
return null;
}
}
/**
* Transforms the given field value to the representation which is stored in the database.
*
* @param o the value to transform
* @return the storable representation of the value
*/
protected Object transformToSource(Object o) {
return o;
}
/**
* Converts the given value back to its original form and stores it as the given entities field value.
*
* @param entity the entity to update
* @param value the stored value from the database
*/
public void readFromSource(Entity entity, Object value) {
try {
Object val = transformFromSource(value);
field.set(entity, val);
entity.setSource(field.getName(), val);
} catch (IllegalAccessException e) {
Exceptions.handle(IndexAccess.LOG, e);
}
}
/**
* Transforms the given field value from the representation which is stored in the database.
*
* @param value the value to transform
* @return the original representation of the value
*/
protected Object transformFromSource(Object value) {
return value;
}
/**
* Generates the mapping used by this property
*
* @param builder the builder used to generate JSON
* @throws IOException in case of an io error while generating the mapping
*/
public final void createMapping(XContentBuilder builder) throws IOException {
builder.startObject(getName());
addMappingProperties(builder);
builder.endObject();
}
/**
* Permits to create an dynamic mapping templates.
*
* @param builder the builder used to generate JSON
* @throws IOException in case of an io error while generating the mapping
*/
public void createDynamicTemplates(XContentBuilder builder) throws IOException {
// Empty by default as this is optional
}
/**
* Returns and converts the field value from the given request and writes it into the given entity.
*
* @param entity the entity to update
* @param ctx the request to read the data from
*/
public void readFromRequest(Entity entity, WebContext ctx) {
try {
if (ctx.get(getName()).isNull()) {
return;
}
field.set(entity, transformFromRequest(getName(), ctx));
} catch (IllegalAccessException e) {
Exceptions.handle(IndexAccess.LOG, e);
}
}
/**
* Extracts and converts the value from the given request.
*
* @param name name of the parameter to read
* @param ctx the request to read the data from
* @return the converted value which can be assigned to the field
*/
protected Object transformFromRequest(String name, WebContext ctx) {
Value value = ctx.get(name);
if (value.isEmptyString() && !field.getType().isPrimitive()) {
return null;
}
Object result = value.coerce(field.getType(), null);
if (result == null) {
UserContext.setFieldError(name, value.get());
throw Exceptions.createHandled()
.withNLSKey("Property.invalidInput")
.set("field", NLS.get(field.getDeclaringClass().getSimpleName() + "." + name))
.set("value", value.asString())
.handle();
}
return result;
}
protected ESOption readAnnotationValue(Class annotation,
Function annotationField,
Supplier defaultSupplier) {
if (!field.isAnnotationPresent(annotation)) {
return defaultSupplier.get();
}
ESOption value = annotationField.apply(field.getAnnotation(annotation));
return value == ESOption.DEFAULT ? defaultSupplier.get() : value;
}
/**
* Creates mappings for inner fields in "nested" or "object" types
*
* @param builder output
* @param nestedClass model class that is used as nested object
* @throws IOException in case of an io error
*/
protected void addNestedMappingProperties(XContentBuilder builder, Class> nestedClass) throws IOException {
if (nestedClass.isAnnotationPresent(Indexed.class)) {
for (Property property : new EntityDescriptor(nestedClass).getProperties()) {
builder.startObject(property.getName());
property.addMappingProperties(builder);
builder.endObject();
}
} else {
createFieldMappings(builder, nestedClass);
}
}
private void createFieldMappings(XContentBuilder builder, Class> nestedClass) throws IOException {
for (Field innerField : nestedClass.getDeclaredFields()) {
if (!innerField.isAnnotationPresent(Transient.class) && !Modifier.isStatic(innerField.getModifiers())) {
createMapping(builder, innerField);
}
}
}
private void createMapping(XContentBuilder builder, Field innerField) throws IOException {
for (PropertyFactory f : Injector.context().getPartCollection(PropertyFactory.class)) {
if (f.accepts(innerField)) {
Property p = f.create(innerField);
p.setInnerProperty(true);
p.createMapping(builder);
return;
}
}
IndexAccess.LOG.WARN("Cannot create property %s in type %s - found no matching property factory",
innerField.getName(),
innerField.getDeclaringClass().getSimpleName());
}
}