package com.mwmd.aem.search.core.indexing.impl;
import com.mwmd.aem.search.core.annotation.Indexer;
import com.mwmd.aem.search.core.indexing.IndexOperation;
import com.mwmd.aem.search.core.indexing.IndexServer;
import com.mwmd.aem.search.core.indexing.IndexService;
import com.mwmd.aem.search.core.indexing.ResourceIndexer;
import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.dam.api.DamConstants;
import com.day.cq.replication.ReplicationStatus;
import com.day.cq.replication.Replicator;
import com.day.cq.wcm.api.NameConstants;
import com.mwmd.aem.search.core.indexing.IndexTask;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import javax.jcr.Session;
import lombok.AccessLevel;
import lombok.Getter;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.PropertyUnbounded;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.settings.SlingSettingsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author Matthias Wermund
*/
@Service
@Properties({
@Property(name = IndexService.PROPERTY_PATHFILTER, unbounded = PropertyUnbounded.ARRAY,
label = "Path filters", description = "Will only index resources whose path start with an item in this list."),
@Property(name = IndexService.PROPERTY_INCLUDE_NON_ACTIVATED, label = "Include non-activated",
description = "If active, a full index operation will ignore the activation status of the resources.", boolValue = false)
})
@Component(label = "External Search Index Service", description = "Maintains indexing tasks for external search.", metatype = true)
public class IndexServiceImpl implements IndexService {
private static final Logger LOG = LoggerFactory.getLogger(IndexServiceImpl.class);
@Reference
@Getter(AccessLevel.PACKAGE)
private ResourceResolverFactory resolverFactory;
@Reference(cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, bind = "registerResourceIndexer",
unbind = "deregisterResourceIndexer", referenceInterface = ResourceIndexer.class, policy = ReferencePolicy.DYNAMIC)
private Map<String, ResourceIndexer> indexerMap = new HashMap<String, ResourceIndexer>();
@Reference
private SlingSettingsService slingSettings;
@Reference(cardinality = ReferenceCardinality.OPTIONAL_UNARY, policy = ReferencePolicy.DYNAMIC)
private IndexServer indexServer;
@Reference
private Replicator replicator;
private ResourceResolver resolver;
private List<String> pathFilter;
private boolean includeNonActivated;
private Thread queueWriterThread;
private IndexQueueWriter queueWriter;
@Getter(AccessLevel.PACKAGE)
private BlockingQueue<IndexTask> tasks;
private Thread queueTransferThread;
private IndexTransfer queueTransfer;
@Activate
protected void activate(Map<String, Object> properties) {
// create backgroud resolver
try {
this.resolver = resolverFactory.getAdministrativeResourceResolver(null);
} catch (LoginException e) {
LOG.error("Error creating resource resolver.", e);
this.resolver = null;
}
// path filters
String[] paths = (String[]) properties.get(PROPERTY_PATHFILTER);
if (paths != null) {
this.pathFilter = Arrays.asList(paths);
} else {
this.pathFilter = new ArrayList<String>();
}
Boolean nonActivated = (Boolean) properties.get(PROPERTY_INCLUDE_NON_ACTIVATED);
this.includeNonActivated = nonActivated != null && nonActivated.booleanValue();
boolean isAuthor = slingSettings.getRunModes().contains("author");
if (isAuthor) {
this.tasks = new LinkedBlockingQueue<IndexTask>();
this.queueWriter = new IndexQueueWriter(this);
this.queueWriterThread = new Thread(this.queueWriter);
this.queueWriterThread.start();
this.queueTransfer = new IndexTransfer(this);
this.queueTransferThread = new Thread(this.queueTransfer);
this.queueTransferThread.start();
}
}
@Deactivate
protected void deactivate() {
if (resolver != null) {
resolver.close();
}
if (queueWriterThread != null) {
queueWriter.stop();
queueWriterThread.interrupt();
}
if (queueTransferThread != null) {
queueTransfer.stop();
queueTransferThread.interrupt();
}
}
@Override
public void add(String path, String revision) {
enqueue(new IndexTask(IndexOperation.ADD, path, revision));
}
@Override
public void remove(String path) {
enqueue(new IndexTask(IndexOperation.REMOVE, path, null));
}
private void enqueue(IndexTask task) {
String path = task.getPath();
if (path == null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Empty path");
}
return;
}
boolean matchesPath = false;
// filter by path
for (String filter : this.pathFilter) {
if (path.startsWith(filter)) {
matchesPath = true;
break;
}
}
if (!matchesPath) {
if (LOG.isDebugEnabled()) {
LOG.debug("Item not matching path filter, ignoring: {})", path);
}
return;
}
// operation seems valid by first filters, hand over async to queueWriter
this.tasks.add(task);
}
protected void registerResourceIndexer(ResourceIndexer indexer) {
String[] resourceTypes = getResourceTypes(indexer);
if (resourceTypes != null) {
for (String resourceType : resourceTypes) {
ResourceIndexer oldIndexer = this.indexerMap.put(resourceType, indexer);
if (oldIndexer != null) {
LOG.warn("Duplicate indexer registration ({},{}) ignoring {}",
new Object[]{
indexer.getClass().getName(),
oldIndexer.getClass().getName(),
oldIndexer.getClass().getName()
});
}
}
LOG.debug("Registering Indexer for {} , {} registered", Arrays.toString(resourceTypes), this.indexerMap.size());
}
}
protected void deregisterResourceIndexer(ResourceIndexer indexer) {
String[] resourceTypes = getResourceTypes(indexer);
if (resourceTypes != null) {
for (String resourceType : resourceTypes) {
this.indexerMap.remove(resourceType);
}
LOG.debug("Deregistering Indexer for {} , {} registered", Arrays.toString(resourceTypes), this.indexerMap.size());
}
}
@Override
public ResourceIndexer getIndexer(String resourceType) {
return this.indexerMap.get(resourceType);
}
@Override
public ResourceIndexer getIndexer(Resource resource) {
String resourceType = resource.getResourceType();
if (JcrConstants.NT_FROZENNODE.equals(resourceType)) {
resourceType = resource.adaptTo(ValueMap.class).get(JcrConstants.JCR_FROZENPRIMARYTYPE, String.class);
}
return getIndexer(resourceType);
}
@Override
public IndexServer getServer() {
return this.indexServer;
}
/**
* Indexes all active content matching the path filter. Currently limited to Pages and Assets.
*/
@Override
public void all() {
if (LOG.isInfoEnabled()) {
LOG.info("Starting full index");
}
try {
indexServer.clear();
Session session = resolver.adaptTo(Session.class);
for (String path : this.pathFilter) {
if (LOG.isDebugEnabled()) {
LOG.debug("Starting full index at {}", path);
}
Resource res = resolver.getResource(path);
if (res != null) {
all(session, res.listChildren());
}
}
} catch (Exception e) {
LOG.error("Error during full index.", e);
}
}
private void all(Session session, Iterator<Resource> items) {
while (items.hasNext()) {
Resource res = items.next();
if (NameConstants.NT_PAGE.equals(res.getResourceType()) || DamConstants.NT_DAM_ASSET.equals(res.getResourceType())) {
if (!this.includeNonActivated) {
ReplicationStatus status = replicator.getReplicationStatus(session, res.getPath());
if (status != null && (status.isActivated())) {
add(res.getPath(), null);
}
} else {
add(res.getPath(), null);
}
}
all(session, res.listChildren());
}
}
private static String[] getResourceTypes(ResourceIndexer indexer) {
Indexer annotation = indexer.getClass().getAnnotation(Indexer.class);
return annotation == null ? null : annotation.resourceTypes();
}
void notifyTransfer() {
if (this.queueTransfer != null) {
synchronized (this.queueTransfer) {
this.queueTransfer.notify();
}
}
}
}