package org.tynamo.model.jpa.internal;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PostPersist;
import javax.persistence.PostRemove;
import javax.persistence.PostUpdate;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.tapestry5.ioc.services.RegistryShutdownHub;
import org.eclipse.persistence.descriptors.DescriptorEvent;
import org.eclipse.persistence.descriptors.DescriptorEventAdapter;
import org.eclipse.persistence.sessions.Session;
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingResponse;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.Requests;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.indices.IndexAlreadyExistsException;
import org.elasticsearch.node.Node;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tynamo.descriptor.TynamoClassDescriptor;
import org.tynamo.model.elasticsearch.descriptor.ElasticSearchExtension;
import org.tynamo.model.elasticsearch.mapping.MapperFactory;
import org.tynamo.services.DescriptorService;
import org.tynamo.services.PersistenceService;
public class ElasticSearchIndexMaintainer {
private final Node node;
private final EntityManager entityManager;
private final DescriptorService descriptorService;
private final Logger logger = LoggerFactory.getLogger(ElasticSearchIndexMaintainer.class);
private final PersistenceService persistenceService;
private final MapperFactory mapperFactory;
private volatile boolean running = false;
public ElasticSearchIndexMaintainer(RegistryShutdownHub hub, PersistenceService persistenceService,
ConfigurableEntityManagerProvider configurableEntityManagerProvider, Node node,
DescriptorService descriptorService,
MapperFactory mapperFactory) {
this.persistenceService = persistenceService;
this.entityManager = configurableEntityManagerProvider.getEntityManager();
this.node = node;
this.descriptorService = descriptorService;
this.mapperFactory = mapperFactory;
hub.addRegistryShutdownListener(new Runnable() {
public void run() {
running = false;
}
});
}
/**
* Triggered after a JPA entity is persisted or updated
*
* @param entity
* The entity to index
*/
@PostPersist
@PostUpdate
public void postWrite(Object entity) {
Client client = node.client();
try {
indexEntity(client, entity, getElasticSearchDescriptor(entity));
} catch (Exception e) {
logger.error(String.format("Failed to index entity %s of type %s", entity, entity.getClass().getSimpleName()), e);
}
}
ElasticSearchExtension getElasticSearchDescriptor(Object entity) {
return descriptorService.getClassDescriptor(entity.getClass()).getExtension(ElasticSearchExtension.class);
}
/**
* Triggered after a JPA entity is deleted
*
* @param entity
* The entity to remove from the index
*/
@PostRemove
public void postDelete(Object entity) {
removeEntityFromIndex(node.client(), entity, getElasticSearchDescriptor(entity));
}
public void removeEntityFromIndex(Client client, Object entity, ElasticSearchExtension elasticSearchExtension) {
logger.debug("Delete entity %s of type %s from elasticsearch index", entity, entity.getClass().getName());
ElasticSearchExtension descriptor = getElasticSearchDescriptor(entity);
client.prepareDelete(descriptor.getIndexName(), descriptor.getTypeName(), descriptor.getDocumentId(entity))
.execute();
}
public void start() {
try {
Class.forName("org.eclipse.persistence.sessions.Session");
} catch (ClassNotFoundException e) {
logger.warn("Elasticsearch integration is currently for EclipseLink only. Only JPA search is available ");
return;
}
Thread indexCreator = new Thread(new Runnable() {
public void run() {
running = true;
createIndices();
}
});
indexCreator.start();
}
protected void createIndices() {
Client client = node.client();
client.admin().cluster().prepareHealth().setWaitForYellowStatus().execute().actionGet();
// List<String> indexNames = new ArrayList<String>(descriptorService.getAllDescriptors().size());
// we can't use IndicesExists for all at the same time since it returns a simple boolean, rather than an array
// IndicesExistsResponse response = client.admin().indices()
// .exists(new IndicesExistsRequest(indexNames.toArray(new String[0]))).actionGet();
for (TynamoClassDescriptor descriptor : descriptorService.getAllDescriptors()) {
if (!running) {
logger.info(String.format(
"%s isn't in a running state anymore, indexing stopped while processing descriptor for type %s",
getClass().getSimpleName(), descriptor.getBeanType().getName()));
break;
}
if (!descriptor.supportsExtension(ElasticSearchExtension.class)) continue;
// register to listen to events of each entity separately
// http://eclipse.1072660.n5.nabble.com/Specific-EntityListener-Instance-td4159.html
entityManager.unwrap(Session.class).getDescriptor(descriptor.getBeanType())
.getEventManager().addListener(new EclipseLinkDescriptorEventListener());
ElasticSearchExtension descriptorExtension = descriptor.getExtension(ElasticSearchExtension.class);
if (!client.admin().indices().prepareExists(descriptorExtension.getIndexName()).execute().actionGet().isExists()) {
// create index and start indexing entity
createIndex(client, descriptorExtension);
if (!indexEntities(client, descriptor.getBeanType(), descriptorExtension)) {
if (!running) {
logger.info(String.format(
"%s isn't in a running state anymore, indexing stopped while processing descriptor for type %s",
getClass().getSimpleName(), descriptor.getBeanType().getName()));
break;
} else {
logger.info(String.format(
"%s failed batch indexing type %s, skipping over remaining entities of the same type",
getClass().getSimpleName(), descriptor.getBeanType().getName()));
}
}
}
}
}
protected boolean indexEntities(Client client, Class beanType, ElasticSearchExtension descriptorExtension) {
List entities = persistenceService.getInstances(beanType);
try {
for (Object entity : entities) {
if (!running) return false;
indexEntity(client, entity, descriptorExtension);
}
} catch (Exception e) {
// FIXME should we delete index?
// TODO Auto-generated catch block
e.printStackTrace();
return false;
}
return true;
}
protected void indexEntity(Client client, Object model, ElasticSearchExtension descriptor) throws Exception {
logger.debug("Index Model: %s", model);
XContentBuilder contentBuilder = null;
// Index Model
try {
contentBuilder = XContentFactory.jsonBuilder().prettyPrint();
descriptor.addModel(model, contentBuilder, mapperFactory);
logger.debug("Index json: %s", contentBuilder.string());
IndexResponse response = client
.prepareIndex(descriptor.getIndexName(), descriptor.getTypeName(), descriptor.getDocumentId(model))
.setSource(contentBuilder).execute()
.actionGet();
logger.debug("Index Response: %s", response);
} finally {
if (contentBuilder != null) contentBuilder.close();
}
}
private void createIndex(Client client, ElasticSearchExtension descriptor) {
String indexName = descriptor.getIndexName();
try {
logger.debug("Starting Elastic Search Index %s", indexName);
CreateIndexResponse response = client.admin().indices().create(new CreateIndexRequest(indexName)).actionGet();
logger.debug("Response: %s", response);
} catch (IndexAlreadyExistsException iaee) {
logger.debug("Index already exists: %s", indexName);
return;
} catch (Throwable t) {
logger.warn(ExceptionUtils.getStackTrace(t));
return;
}
createType(client, descriptor);
}
private void createType(Client client, ElasticSearchExtension descriptor) {
String indexName = descriptor.getIndexName();
String typeName = descriptor.getTypeName();
try {
logger.debug("Create Elastic Search Type %s/%s", indexName, typeName);
PutMappingRequest request = Requests.putMappingRequest(indexName).type(typeName);
XContentBuilder contentBuilder = XContentFactory.jsonBuilder().prettyPrint();
descriptor.addMapping(contentBuilder, mapperFactory);
logger.debug("Type mapping: \n %s", contentBuilder.string());
request.source(contentBuilder);
PutMappingResponse response = client.admin().indices().putMapping(request).actionGet();
logger.debug("Response: %s", response);
} catch (IndexAlreadyExistsException iaee) {
logger.debug("Index already exists: %s", indexName);
} catch (Throwable t) {
logger.warn(ExceptionUtils.getStackTrace(t));
}
}
class EclipseLinkDescriptorEventListener extends DescriptorEventAdapter {
@Override
public void postInsert(DescriptorEvent event) {
ElasticSearchIndexMaintainer.this.postWrite(event.getSource());
}
@Override
public void postUpdate(DescriptorEvent event) {
ElasticSearchIndexMaintainer.this.postWrite(event.getSource());
}
@Override
public void postDelete(DescriptorEvent event) {
ElasticSearchIndexMaintainer.this.postDelete(event.getSource());
}
}
}