All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sirius.search.EntityDescriptor Maven / Gradle / Ivy

Go to download

Provides a thin layer above Elasticsearch (Fluent Query API, Automatic Mapping, Utilities)

The newest version!
/*
 * 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;

import com.google.common.base.Objects;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import sirius.kernel.commons.Reflection;
import sirius.kernel.commons.Strings;
import sirius.kernel.health.Exceptions;
import sirius.search.annotations.Indexed;
import sirius.search.annotations.RefField;
import sirius.search.annotations.RefType;
import sirius.search.annotations.Transient;
import sirius.search.properties.Property;
import sirius.search.properties.PropertyFactory;

import javax.annotation.Nullable;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Contains metadata collected by inspecting an entity class (subclass of {@link Entity}).
 * 

* For each entity class an EntityDescriptor is automatically created and filled by the framework. Most of * this data is used by the framework to define the mapping (schema) in ElasticSearch, etc. */ public class EntityDescriptor { private final String indexName; private final String annotatedIndexName; private String typeName; private String routing; private boolean useAllField; private final Class clazz; protected List properties; protected List foreignKeys; protected List remoteForeignKeys = new ArrayList<>(); /** * Creates a new EntityDescriptor for the given entity class. * * @param clazz the entity class to be inspected */ public EntityDescriptor(Class clazz) { this.clazz = clazz; if (!clazz.isAnnotationPresent(Indexed.class)) { throw new IllegalArgumentException("Missing @Indexed-Annotation: " + clazz.getName()); } Indexed indexedAnnotation = clazz.getAnnotation(Indexed.class); this.annotatedIndexName = indexedAnnotation.index(); if (annotatedIndexName.isEmpty()) { this.indexName = clazz.getSimpleName().toLowerCase(); } else { this.indexName = annotatedIndexName + "-" + clazz.getSimpleName().toLowerCase(); } this.useAllField = clazz.getAnnotation(Indexed.class).useAllField(); this.typeName = Strings.firstFilled(indexedAnnotation.type(), clazz.getSimpleName()); this.routing = indexedAnnotation.routing(); if (Strings.isEmpty(routing)) { routing = null; } } /** * Returns a list of properties which will be stored in the database * * @return the list of persisted properties */ public List getProperties() { if (properties == null) { List props = new ArrayList<>(); List keys = new ArrayList<>(); addProperties(clazz, clazz, props, keys); properties = props; foreignKeys = keys; } return properties; } /* * Adds all properties of the given class (and its superclasses) */ private void addProperties(Class rootClass, Class clazz, List props, List keys) { for (Field field : clazz.getDeclaredFields()) { if (!field.isAnnotationPresent(Transient.class) && !Modifier.isStatic(field.getModifiers())) { addProperty(rootClass, clazz, props, keys, field); } } if (clazz.getSuperclass() != null && !Object.class.equals(clazz.getSuperclass())) { addProperties(rootClass, clazz.getSuperclass(), props, keys); } } @SuppressWarnings("unchecked") private void addProperty(Class rootClass, Class clazz, List props, List keys, Field field) { Property p = createProperty(rootClass, props, field); if (p == null) { IndexAccess.LOG.WARN("Cannot create property %s in type %s", field.getName(), clazz.getSimpleName()); } else { if (field.isAnnotationPresent(RefType.class)) { keys.add(new ForeignKey(field, (Class) rootClass)); } if (field.isAnnotationPresent(RefField.class)) { ForeignKey key = findForeignKey(keys, field.getAnnotation(RefField.class).localRef()); if (key == null) { IndexAccess.LOG.WARN("No foreign key %s found for field reference %s ", field.getAnnotation(RefField.class).localRef(), field.getName()); } else { key.addReference(p, field.getAnnotation(RefField.class).remoteField()); } } } } private Property createProperty(Class rootClass, List props, Field field) { for (PropertyFactory f : Schema.factories.getParts()) { if (f.accepts(field)) { Property p = f.create(field); if (propertyAlreadyExists(props, p)) { IndexAccess.LOG.SEVERE(Strings.apply( "A property named '%s' already exists for the type '%s'. Cannot transform field %s", p.getName(), rootClass.getSimpleName(), field)); return null; } props.add(p); if (!p.acceptsSetter() && hasSetter(field)) { IndexAccess.LOG.WARN("Property %s in type %s does not accept a setter method to be present", field.getName(), rootClass.getSimpleName()); } return p; } } return null; } private boolean propertyAlreadyExists(List props, Property check) { for (Property prop : props) { if (Strings.areEqual(prop.getName(), check.getName())) { return true; } } return false; } /* * Determines if there is a setter method (setXXX) for the given field. */ private boolean hasSetter(Field field) { try { field.getDeclaringClass().getMethod("set" + Reflection.toFirstUpper(field.getName()), field.getType()); return true; } catch (NoSuchMethodException e) { Exceptions.ignore(e); return false; } } /* * Searches for the foreign key with the given name */ private ForeignKey findForeignKey(List keys, String key) { for (ForeignKey k : keys) { if (Objects.equal(k.getName(), key)) { return k; } } return null; } /** * Converts the data of the given entity into the given source map. * * @param entity the entity to load the data form * @param source the target map to store the converted values into */ protected void writeTo(Entity entity, Map source) { for (Property p : getProperties()) { source.put(p.getName(), p.writeToSource(entity)); } } /** * Converts the data in the given source map and stores it in the given entity. * * @param entity the entity to store the loaded data * @param source the source map to read the data from */ public void readSource(Entity entity, Map source) { for (Property p : getProperties()) { p.readFromSource(entity, source.get(p.getName())); } } /** * Converts the data of the given Entity object into json format * * @param entity the entity to load the data from * @param objectName a name for the given json object or null if no name should be added * @return the entity json as string */ public String toJson(Entity entity, @Nullable String objectName) { try (XContentBuilder builder = XContentFactory.jsonBuilder()) { if (Strings.isFilled(objectName)) { builder.startObject(objectName); } else { builder.startObject(); } for (Property p : getProperties()) { builder.field(p.getName(), p.writeToSource(entity)); } builder.endObject(); return builder.string(); } catch (IOException e) { Exceptions.handle(e); } return null; } /** * Returns the name of the index which is used to store the data. *

* Note that this is an ElasticSearch index and not to be confused with a database index. This would be more or * less something like a schema in a SQL db * * @return the name of the ES index used to store entities */ public String getIndex() { return indexName; } /** * Returns the name of the index field that was given in the {@link Indexed annotation}. * * @return the name of the index field that was given in the {@link Indexed annotation}. */ public String getAnnotatedIndex() { return annotatedIndexName; } /** * Returns the type name used to store entities related to this descriptor. *

* A type in ElasticSearch can be compared to a table in a SQL db. * * @return the type name used to store entities related to this descriptor */ public String getType() { return typeName; } /** * Creates the mapping required by ElasticSearch to store entities related to this descriptor. * * @return a JSON structure defining the mapping of this descriptor * @throws IOException in case of an IO error during the JSON encoding */ public XContentBuilder createMapping() throws IOException { String[] excludes = getProperties().stream() .filter(Property::isExcludeFromSource) .map(Property::getName) .toArray(String[]::new); try (XContentBuilder builder = XContentFactory.jsonBuilder()) { builder.startObject().startObject(getType()); if (excludes.length > 0) { builder.startObject("_source"); builder.array("excludes", excludes); builder.endObject(); } builder.startObject("properties"); if (useAllField) { createCustomAllField(builder); } for (Property p : getProperties()) { p.createMapping(builder); } builder.endObject(); builder.startArray("dynamic_templates"); for (Property p : getProperties()) { p.createDynamicTemplates(builder); } builder.endArray(); return builder.endObject().endObject(); } } private void createCustomAllField(XContentBuilder builder) throws IOException { builder.startObject("custom_all") .field("type", "text") .field("store", "false") .field("index", "true") .field("doc_values", "false") .field("index_options", "docs") .field("analyzer", "standard") .field("norms", "false") .endObject(); } /** * Returns all foreign keys defined by this descriptor. * * @return a list of all foreign keys defined by this descriptor. */ public List getForeignKeys() { if (foreignKeys == null) { getProperties(); } return foreignKeys; } /** * Returns the property with the given name * * @param name the name of the requested property * @return the property with the requested name or null if no property with the given name was found */ public Property getProperty(String name) { for (Property p : properties) { if (p.getName().equals(name)) { return p; } } return null; } /** * Determines if a routing should be specified for this type of entities. * * @return true if a routing is required, false otherwise */ public boolean hasRouting() { return routing != null; } /** * Returns the field used for routing. * * @return the name of the field used for routing (shard selection) or null to indicate that there is * no special routing. */ public String getRouting() { return routing; } /** * Determines if entities have foreign keys. * * @return true if there are foreign keys, false otherwise */ public boolean hasForeignKeys() { return !foreignKeys.isEmpty(); } public boolean isUseAllField() { return useAllField; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy