package alien4cloud.dao; import java.beans.IntrospectionException; import java.io.IOException; import java.lang.reflect.Modifier; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; import javax.annotation.Resource; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.elasticsearch.action.ActionFuture; import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder; import org.elasticsearch.action.admin.indices.create.CreateIndexResponse; import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsRequest; import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsResponse; import org.elasticsearch.client.Client; import org.elasticsearch.mapping.ElasticSearchClient; import org.elasticsearch.mapping.MappingBuilder; import org.elasticsearch.util.MapUtil; import alien4cloud.exception.IndexingServiceException; import alien4cloud.rest.utils.JsonUtil; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Lists; import com.google.common.collect.Maps; /** * Manages one or multiple indexes and the related java and elastic search types. * * @author luc boutier */ @Slf4j public abstract class ESIndexMapper { /** Custom score field for alien. */ public static final String ALIEN_SCORE = "alienScore"; @Resource private ElasticSearchClient esClient; @Resource @Getter private MappingBuilder mappingBuilder; @Getter private final Map<String, String> typesToIndices = Maps.newHashMap(); @Getter private final Map<String, Class<?>> typesToClasses = Maps.newHashMap(); @Getter private String[] allIndexes; @Getter @Setter private ObjectMapper jsonMapper = new ObjectMapper(); /** * Initialize the array of all indices managed by this dao. */ public void initCompleted() { Set<String> indices = new HashSet<>(typesToIndices.values()); allIndexes = indices.toArray(new String[indices.size()]); esClient.waitForGreenStatus(allIndexes); } /** * Create if not exist indices. * A TTL can be defined for all indices under this index (ESearch TTL notation) * * @param indexName The index to initialize * @param classes An array of classes to map to this index. */ @SneakyThrows({ IOException.class, IntrospectionException.class }) public void initIndices(String indexName, String ttl, Class<?>... classes) { if (indexExist(indexName)) { addToMappedClasses(indexName, classes); } else { // create the index and add the mapping CreateIndexRequestBuilder createIndexRequestBuilder = esClient.getClient().admin().indices().prepareCreate(indexName); for (Class<?> clazz : classes) { String typeName = addToMappedClasses(indexName, clazz); if (Modifier.isAbstract(clazz.getModifiers())) { continue; // no mapping to register for abstract classes. } String typeMapping = mappingBuilder.getMapping(clazz); Map<String, Object> typesMap = JsonUtil.toMap(typeMapping); addAlienScore(typesMap); addTTL(typesMap, ttl); String mapping = jsonMapper.writeValueAsString(typesMap); createIndexRequestBuilder.addMapping(typeName, mapping); } // TODO: fixme !!! try { final CreateIndexResponse createResponse = createIndexRequestBuilder.execute().actionGet(); if (!createResponse.isAcknowledged()) { throw new IndexingServiceException("Failed to create index <" + indexName + ">"); } } catch (Exception e) { log.warn("Not able to init indice for index {}, maybe it has been created elsewhere", indexName); } } } /** * Add the alien score field for each type in the map. * * @param typesMap The type map. */ private void addAlienScore(Map<String, Object> typesMap) { for (Object typeMappingObject : typesMap.values()) { @SuppressWarnings("unchecked") Map<String, Object> typeMappingMap = (Map<String, Object>) typeMappingObject; Map<String, Object> propertiesMap = (Map<String, Object>) typeMappingMap.get("properties"); Map<String, Object> scoreMapping = MapUtil.getMap( new String[] { "include_in_all", "precision_step", "index", "boost", "store", "ignore_malformed", "type" }, new String[] { "false", "4", "not_analyzed", "1.0", "false", "false", "long" }); propertiesMap.put(ALIEN_SCORE, scoreMapping); } } /** * Add the ttl field for each type in the map. * * @param typesMap The type map. */ private void addTTL(Map<String, Object> typesMap, String ttl) { if (ttl == null) { // if no ttl value is provided then just return. return; } for (Object typeMappingObject : typesMap.values()) { @SuppressWarnings("unchecked") Map<String, Object> typeMappingMap = (Map<String, Object>) typeMappingObject; Map<String, Object> ttlMapping = MapUtil.getMap(new String[] { "enabled", "default" }, new String[] { "true", ttl }); typeMappingMap.put("_ttl", ttlMapping); } } @SneakyThrows({ ExecutionException.class, InterruptedException.class }) private boolean indexExist(String indexName) { // check if existing before final ActionFuture<IndicesExistsResponse> indexExistFuture = esClient.getClient().admin().indices().exists(new IndicesExistsRequest(indexName)); IndicesExistsResponse response; response = indexExistFuture.get(); return response.isExists(); } private void addToMappedClasses(String indexName, Class<?>[] classes) { for (Class<?> clazz : classes) { addToMappedClasses(indexName, clazz); } } private String addToMappedClasses(String indexName, Class<?> clazz) { log.info("Mapping class <" + clazz.getName() + "> to index <" + indexName + ">"); String typeName = MappingBuilder.indexTypeFromClass(clazz); typesToIndices.put(typeName, indexName); typesToClasses.put(typeName, clazz); return typeName; } /** * Get the index in which the given type lies. * * @param clazz The type for which to get the index. * @return The index in which the given type lies. */ public String getIndexForType(Class<?> clazz) { String typeName = MappingBuilder.indexTypeFromClass(clazz); String index = typesToIndices.get(typeName); if (index == null) { log.error("Class <" + clazz.getName() + "> is not registered in any indexes."); throw new IndexingServiceException("Requested type <" + typeName + "> is not registered in any indexes."); } return index; } /** * Return a class from the given elastic search type. * * @param type The elastic search type. * @return The class matching the given type if any, null if no class is matching the type. */ public Class<?> getClassFromType(String type) { return this.typesToClasses.get(type); } /** * Get the class from the class name. * * @param className The name of the class to get. * @param allowNull If a null class name is allowed. If className is null and allowNull is true then null is returned, if className is null and allowNull is * false then an {@link IndexingServiceException} is thrown. * @return The class matching the given class name, or null if className is null and allowNull is true. */ public Class<?> getClassFromName(String className, boolean allowNull) { if (className == null || className.trim().isEmpty()) { if (allowNull) { return null; } throw new IndexingServiceException("Class type <" + className + "> not found."); } else { try { return Class.forName(className); } catch (ClassNotFoundException e) { log.error("Error while trying to perform search operation.", e); throw new IndexingServiceException("Class type <" + className + "> not found.", e); } } } public String[] getTypesFromClass(Class<?> clazz) { List<String> types = Lists.newArrayList(); Collection<Class<?>> allManagedClasses = typesToClasses.values(); for (Class<?> managedClass : allManagedClasses) { if (clazz.isAssignableFrom(managedClass)) { types.add(MappingBuilder.indexTypeFromClass(managedClass)); } } return types.toArray(new String[types.size()]); } /** * Get the elastic search client linked to the index mapper. * * @return The elastic search client linked to the index mapper. */ public Client getClient() { return this.esClient.getClient(); } public static org.slf4j.Logger getLog() { return log; } }