--- /dev/null
+package org.argeo.cms.jcr.internal;
+
+import java.util.GregorianCalendar;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.EventIterator;
+import javax.jcr.observation.EventListener;
+import javax.jcr.version.VersionManager;
+
+import org.apache.jackrabbit.api.JackrabbitValue;
+import org.apache.jackrabbit.core.RepositoryImpl;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+
+/** Ensure consistency of files, folder and last modified nodes. */
+class CmsWorkspaceIndexer implements EventListener {
+ private final static CmsLog log = CmsLog.getLog(CmsWorkspaceIndexer.class);
+
+// private final static String MIX_ETAG = "mix:etag";
+ private final static String JCR_ETAG = "jcr:etag";
+// private final static String JCR_LAST_MODIFIED = "jcr:lastModified";
+// private final static String JCR_LAST_MODIFIED_BY = "jcr:lastModifiedBy";
+// private final static String JCR_MIXIN_TYPES = "jcr:mixinTypes";
+ private final static String JCR_DATA = "jcr:data";
+ private final static String JCR_CONTENT = "jcr:data";
+
+ private String cn;
+ private String workspaceName;
+ private RepositoryImpl repositoryImpl;
+ private Session session;
+ private VersionManager versionManager;
+
+ private LinkedBlockingDeque<Event> toProcess = new LinkedBlockingDeque<>();
+ private IndexingThread indexingThread;
+ private AtomicBoolean stopping = new AtomicBoolean(false);
+
+ public CmsWorkspaceIndexer(RepositoryImpl repositoryImpl, String cn, String workspaceName)
+ throws RepositoryException {
+ this.cn = cn;
+ this.workspaceName = workspaceName;
+ this.repositoryImpl = repositoryImpl;
+ }
+
+ public void init() {
+ session = KernelUtils.openAdminSession(repositoryImpl, workspaceName);
+ try {
+ String[] nodeTypes = { NodeType.NT_FILE, NodeType.MIX_LAST_MODIFIED };
+ session.getWorkspace().getObservationManager().addEventListener(this,
+ Event.NODE_ADDED | Event.PROPERTY_CHANGED, "/", true, null, nodeTypes, true);
+ versionManager = session.getWorkspace().getVersionManager();
+
+ indexingThread = new IndexingThread();
+ indexingThread.start();
+ } catch (RepositoryException e1) {
+ throw new IllegalStateException(e1);
+ }
+ }
+
+ public void destroy() {
+ stopping.set(true);
+ indexingThread.interrupt();
+ // TODO make it configurable
+ try {
+ indexingThread.join(10 * 60 * 1000);
+ } catch (InterruptedException e1) {
+ log.warn("Indexing thread interrupted. Will log out session.");
+ }
+
+ try {
+ session.getWorkspace().getObservationManager().removeEventListener(this);
+ } catch (RepositoryException e) {
+ if (log.isTraceEnabled())
+ log.warn("Cannot unregistered JCR event listener", e);
+ } finally {
+ JcrUtils.logoutQuietly(session);
+ }
+ }
+
+ private synchronized void processEvents(EventIterator events) {
+ long begin = System.currentTimeMillis();
+ long count = 0;
+ while (events.hasNext()) {
+ Event event = events.nextEvent();
+ try {
+ toProcess.put(event);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+// processEvent(event);
+ count++;
+ }
+ long duration = System.currentTimeMillis() - begin;
+ if (log.isTraceEnabled())
+ log.trace("Processed " + count + " events in " + duration + " ms");
+ notifyAll();
+ }
+
+ protected synchronized void processEvent(Event event) {
+ try {
+ String eventPath = event.getPath();
+ if (event.getType() == Event.NODE_ADDED) {
+ if (!versionManager.isCheckedOut(eventPath))
+ return;// ignore checked-in nodes
+ if (log.isTraceEnabled())
+ log.trace("NODE_ADDED " + eventPath);
+// session.refresh(true);
+ session.refresh(false);
+ Node node = session.getNode(eventPath);
+ Node parentNode = node.getParent();
+ if (parentNode.isNodeType(NodeType.NT_FILE)) {
+ if (node.isNodeType(NodeType.NT_UNSTRUCTURED)) {
+ if (!node.isNodeType(NodeType.MIX_LAST_MODIFIED))
+ node.addMixin(NodeType.MIX_LAST_MODIFIED);
+ Property property = node.getProperty(Property.JCR_DATA);
+ String etag = toEtag(property.getValue());
+ session.save();
+ node.setProperty(JCR_ETAG, etag);
+ if (log.isTraceEnabled())
+ log.trace("ETag and last modified added to new " + node);
+ } else if (node.isNodeType(NodeType.NT_RESOURCE)) {
+// if (!node.isNodeType(MIX_ETAG))
+// node.addMixin(MIX_ETAG);
+// session.save();
+// Property property = node.getProperty(Property.JCR_DATA);
+// String etag = toEtag(property.getValue());
+// node.setProperty(JCR_ETAG, etag);
+// session.save();
+ }
+// setLastModifiedRecursive(parentNode, event);
+// session.save();
+// if (log.isTraceEnabled())
+// log.trace("ETag and last modified added to new " + node);
+ }
+
+// if (node.isNodeType(NodeType.NT_FOLDER)) {
+// setLastModifiedRecursive(node, event);
+// session.save();
+// if (log.isTraceEnabled())
+// log.trace("Last modified added to new " + node);
+// }
+ } else if (event.getType() == Event.PROPERTY_CHANGED) {
+ String propertyName = extractItemName(eventPath);
+ // skip if last modified properties are explicitly set
+ if (!propertyName.equals(JCR_DATA))
+ return;
+// if (propertyName.equals(JCR_LAST_MODIFIED))
+// return;
+// if (propertyName.equals(JCR_LAST_MODIFIED_BY))
+// return;
+// if (propertyName.equals(JCR_MIXIN_TYPES))
+// return;
+// if (propertyName.equals(JCR_ETAG))
+// return;
+
+ if (log.isTraceEnabled())
+ log.trace("PROPERTY_CHANGED " + eventPath);
+
+ if (!session.propertyExists(eventPath))
+ return;
+ session.refresh(false);
+ Property property = session.getProperty(eventPath);
+ Node node = property.getParent();
+ if (property.getType() == PropertyType.BINARY && propertyName.equals(JCR_DATA)
+ && node.isNodeType(NodeType.NT_UNSTRUCTURED)) {
+ String etag = toEtag(property.getValue());
+ node.setProperty(JCR_ETAG, etag);
+ Node parentNode = node.getParent();
+ if (parentNode.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
+ setLastModified(parentNode, event);
+ }
+ if (log.isTraceEnabled())
+ log.trace("ETag and last modified updated for " + node);
+ }
+// setLastModified(node, event);
+// session.save();
+// if (log.isTraceEnabled())
+// log.trace("ETag and last modified updated for " + node);
+ } else if (event.getType() == Event.NODE_REMOVED) {
+ String removeNodePath = eventPath;
+ String nodeName = extractItemName(eventPath);
+ if (JCR_CONTENT.equals(nodeName)) // parent is a file, deleted anyhow
+ return;
+ if (log.isTraceEnabled())
+ log.trace("NODE_REMOVED " + eventPath);
+// String parentPath = JcrUtils.parentPath(removeNodePath);
+// session.refresh(true);
+// setLastModified(parentPath, event);
+// session.save();
+ if (log.isTraceEnabled())
+ log.trace("Last modified updated for parents of removed " + removeNodePath);
+ }
+ } catch (Exception e) {
+ if (log.isTraceEnabled())
+ log.warn("Cannot process event " + event, e);
+ } finally {
+// try {
+// session.refresh(true);
+// if (session.hasPendingChanges())
+// session.save();
+//// session.refresh(false);
+// } catch (RepositoryException e) {
+// if (log.isTraceEnabled())
+// log.warn("Cannot refresh JCR session", e);
+// }
+ }
+
+ }
+
+ private String extractItemName(String path) {
+ if (path == null || path.length() <= 1)
+ return null;
+ int lastIndex = path.lastIndexOf('/');
+ if (lastIndex >= 0) {
+ return path.substring(lastIndex + 1);
+ } else {
+ return path;
+ }
+ }
+
+ @Override
+ public void onEvent(EventIterator events) {
+ processEvents(events);
+// Runnable toRun = new Runnable() {
+//
+// @Override
+// public void run() {
+// processEvents(events);
+// }
+// };
+// Future<?> future = Activator.getInternalExecutorService().submit(toRun);
+// try {
+// // make the call synchronous
+// future.get(60, TimeUnit.SECONDS);
+// } catch (TimeoutException | ExecutionException | InterruptedException e) {
+// // silent
+// }
+ }
+
+ static String toEtag(Value v) {
+ if (v instanceof JackrabbitValue) {
+ JackrabbitValue value = (JackrabbitValue) v;
+ return '\"' + value.getContentIdentity() + '\"';
+ } else {
+ return null;
+ }
+
+ }
+
+ protected synchronized void setLastModified(Node node, Event event) throws RepositoryException {
+ GregorianCalendar calendar = new GregorianCalendar();
+ calendar.setTimeInMillis(event.getDate());
+ node.setProperty(Property.JCR_LAST_MODIFIED, calendar);
+ node.setProperty(Property.JCR_LAST_MODIFIED_BY, event.getUserID());
+ if (log.isTraceEnabled())
+ log.trace("Last modified set on " + node);
+ }
+
+ /** Recursively set the last updated time on parents. */
+ protected synchronized void setLastModifiedRecursive(Node node, Event event) throws RepositoryException {
+ if (versionManager.isCheckedOut(node.getPath())) {
+ if (node.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
+ setLastModified(node, event);
+ }
+ if (node.isNodeType(NodeType.NT_FOLDER) && !node.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
+ node.addMixin(NodeType.MIX_LAST_MODIFIED);
+ if (log.isTraceEnabled())
+ log.trace("Last modified mix-in added to " + node);
+ }
+
+ }
+
+ // end condition
+ if (node.getDepth() == 0) {
+// try {
+// node.getSession().save();
+// } catch (RepositoryException e) {
+// log.warn("Cannot index workspace", e);
+// }
+ return;
+ } else {
+ Node parent = node.getParent();
+ setLastModifiedRecursive(parent, event);
+ }
+ }
+
+ /**
+ * Recursively set the last updated time on parents. Useful to use paths when
+ * dealing with deletions.
+ */
+ protected synchronized void setLastModifiedRecursive(String path, Event event) throws RepositoryException {
+ // root node will always exist, so end condition is delegated to the other
+ // recursive setLastModified method
+ if (session.nodeExists(path)) {
+ setLastModifiedRecursive(session.getNode(path), event);
+ } else {
+ setLastModifiedRecursive(JcrUtils.parentPath(path), event);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Indexer for workspace " + workspaceName + " of repository " + cn;
+ }
+
+ class IndexingThread extends Thread {
+
+ public IndexingThread() {
+ super(CmsWorkspaceIndexer.this.toString());
+ // TODO Auto-generated constructor stub
+ }
+
+ @Override
+ public void run() {
+ life: while (session != null && session.isLive()) {
+ try {
+ Event nextEvent = toProcess.take();
+ processEvent(nextEvent);
+ } catch (InterruptedException e) {
+ // silent
+ interrupted();
+ }
+
+ if (stopping.get() && toProcess.isEmpty()) {
+ break life;
+ }
+ }
+ if (log.isDebugEnabled())
+ log.debug(CmsWorkspaceIndexer.this.toString() + " has shut down.");
+ }
+
+ }
+
+}
\ No newline at end of file