org.jahia.services.content.JCRObservationManager.java Source code

Java tutorial

Introduction

Here is the source code for org.jahia.services.content.JCRObservationManager.java

Source

/**
 * ==========================================================================================
 * =                   JAHIA'S DUAL LICENSING - IMPORTANT INFORMATION                       =
 * ==========================================================================================
 *
 *                                 http://www.jahia.com
 *
 *     Copyright (C) 2002-2017 Jahia Solutions Group SA. All rights reserved.
 *
 *     THIS FILE IS AVAILABLE UNDER TWO DIFFERENT LICENSES:
 *     1/GPL OR 2/JSEL
 *
 *     1/ GPL
 *     ==================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE GPL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 *
 *     2/ JSEL - Commercial and Supported Versions of the program
 *     ===================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE JSEL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     Alternatively, commercial and supported versions of the program - also known as
 *     Enterprise Distributions - must be used in accordance with the terms and conditions
 *     contained in a separate written agreement between you and Jahia Solutions Group SA.
 *
 *     If you are unsure which license is appropriate for your use,
 *     please contact the sales department at sales@jahia.com.
 */
package org.jahia.services.content;

import javax.jcr.NamespaceRegistry;
import javax.jcr.RepositoryException;
import javax.jcr.observation.*;
import javax.jcr.observation.EventListener;

import org.apache.commons.lang.StringUtils;
import org.apache.jackrabbit.api.observation.JackrabbitEvent;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.AdditionalEventInfo;
import org.jahia.services.content.nodetypes.NodeTypeRegistry;
import org.slf4j.Logger;

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Pattern;

/**
 * Observation manager implementation
 * <p/>
 * Execute listener synchronously after session.save()
 */
public class JCRObservationManager implements ObservationManager {
    public static final int SESSION_SAVE = 1;
    public static final int WORKSPACE_MOVE = 2;
    public static final int WORKSPACE_COPY = 3;
    public static final int WORKSPACE_CLONE = 4;
    public static final int WORKSPACE_CREATE_ACTIVITY = 5;
    public static final int NODE_CHECKIN = 6;
    public static final int NODE_CHECKOUT = 7;
    public static final int NODE_CHECKPOINT = 8;
    public static final int NODE_RESTORE = 9;
    public static final int NODE_UPDATE = 10;
    public static final int NODE_MERGE = 11;
    public static final int EXTERNAL_SYNC = 12;
    public static final int IMPORT = 13;

    private static Logger logger = org.slf4j.LoggerFactory.getLogger(JCRObservationManager.class);

    private static ThreadLocal<Boolean> eventListenersAvailableDuringPublishOnly = new ThreadLocal<Boolean>();
    private static ThreadLocal<Boolean> allEventListenersDisabled = new ThreadLocal<Boolean>();
    private static ThreadLocal<JCRSessionWrapper> currentSession = new ThreadLocal<JCRSessionWrapper>();
    private static ThreadLocal<Integer> lastOp = new ThreadLocal<Integer>();
    private static ThreadLocal<Map<JCRSessionWrapper, List<EventWrapper>>> events = new ThreadLocal<Map<JCRSessionWrapper, List<EventWrapper>>>();
    private static List<EventConsumer> listeners = new CopyOnWriteArrayList<EventConsumer>();

    private JCRWorkspaceWrapper ws;

    public JCRObservationManager(JCRWorkspaceWrapper ws) {
        this.ws = ws;
    }

    /**
     * Adds an event listener that listens for the specified <code>eventTypes</code> (a combination of one or more
     * event types encoded as a bit mask value).
     * <p/>
     * The set of events can be filtered by specifying restrictions based on characteristics of the node associated
     * with the event. In the case of  event types <code>NODE_ADDED</code> and <code>NODE_REMOVED</code>, the node
     * associated with an event is the node at (or formerly at) the path returned by <code>Event.getPath</code>.
     * In the case of  event types <code>PROPERTY_ADDED</code>,  <code>PROPERTY_REMOVED</code> and
     * <code>PROPERTY_CHANGED</code>, the node associated with an event is the parent node of the property at
     * (or formerly at) the path returned by <code>Event.getPath</code>:
     * <ul>
     * <li>
     * <code>absPath</code>, <code>isDeep</code>: Only events whose associated node is at
     * <code>absPath</code> (or within its subtree, if <code>isDeep</code> is <code>true</code>) will be received.
     * It is permissible to register a listener for a path where no node currently exists. The path can also be
     * a regular expression (as the parameter is an absolute path, Jahia will automatically add the begin/end line
     * character and if isDeep is true, Jahia appends to the regular expression, to include the path and subtree). 
     * If this parameter is <code>null</code> then no path-related restriction is placed on events received. 
     * </li>
     * <li>
     * <code>uuid</code>: Only events whose associated node has one of the UUIDs in this list will be
     * received. If his parameter is <code>null</code> then no UUID-related restriction is placed on events
     * received.
     * </li>
     * <li>
     * <code>nodeTypeName</code>: Only events whose associated node has one of the node types
     * (or a subtype of one of the node types) in this list will be received. If this parameter is
     * <code>null</code> then no node type-related restriction is placed on events received. 
     * WARNING: if a listener only filters on nodeTypeName, then this can slow down the system, as for all
     * events we need to determine the nodeType of the node. If possible you should use another filter like
     * the path to reduce the number of events, where nodetype needs to be determined. 
     * </li>
     * </ul>
     * The restrictions are "ANDed" together. In other words, for a particular node to be "listened to" it must meet all the restrictions.
     * <p/>
     * Additionally, if <code>noLocal</code> is <code>true</code>, then events generated by the session through which
     * the listener was registered are ignored. Otherwise, they are not ignored.
     * <p/>
     * The filters of an already-registered <code>EventListener</code> can be changed at runtime by re-registering the
     * same <code>EventListener</code> object (i.e. the same actual Java object) with a new set of filter arguments.
     * The implementation must ensure that no events are lost during the changeover.
     *
     * @param listener     an {@link javax.jcr.observation.EventListener} object.
     * @param eventTypes   A combination of one or more event type constants encoded as a bitmask.
     * @param absPath      an absolute path.
     * @param isDeep       a <code>boolean</code>.
     * @param uuid         array of UUIDs.
     * @param nodeTypeName array of node type names.
     * @param noLocal      a <code>boolean</code>.
     * @throws javax.jcr.RepositoryException If an error occurs.
     */
    @Override
    public void addEventListener(EventListener listener, int eventTypes, String absPath, boolean isDeep,
            String[] uuid, String[] nodeTypeName, boolean noLocal) throws RepositoryException {
        listeners.add(new EventConsumer(ws.getSession(), listener, eventTypes, absPath, isDeep, nodeTypeName, uuid,
                listener instanceof ExternalEventListener));
    }

    /**
     * Deregisters an event listener.
     * <p/>
     * A listener may be deregistered while it is being executed. The
     * deregistration method will block until the listener has completed
     * executing. An exception to this rule is a listener which deregisters
     * itself from within the <code>onEvent</code> method. In this case, the
     * deregistration method returns immediately, but deregistration will
     * effectively be delayed until the listener completes.
     *
     * @param listener The listener to deregister.
     * @throws javax.jcr.RepositoryException If an error occurs.
     */
    @Override
    public void removeEventListener(EventListener listener) throws RepositoryException {
        EventConsumer e = null;
        for (EventConsumer eventConsumer : listeners) {
            if (eventConsumer.listener == listener) {
                e = eventConsumer;
                break;
            }
        }
        if (e != null) {
            listeners.remove(e);
        }
    }

    /**
     * Returns all event listeners that have been registered through this session.
     * If no listeners have been registered, an empty iterator is returned.
     *
     * @return an <code>EventListenerIterator</code>.
     * @throws javax.jcr.RepositoryException
     */
    @Override
    public EventListenerIterator getRegisteredEventListeners() throws RepositoryException {
        return new EventListenerIteratorImpl(listeners.iterator(), listeners.size());
    }

    @Override
    public void setUserData(String userData) throws RepositoryException {
        // do nothing
    }

    @Override
    public EventJournal getEventJournal() throws RepositoryException {
        return null;
    }

    @Override
    public EventJournal getEventJournal(int i, String s, boolean b, String[] strings, String[] strings1)
            throws RepositoryException {
        return null;
    }

    public static void setEventListenersAvailableDuringPublishOnly(Boolean eventsDisabled) {
        JCRObservationManager.eventListenersAvailableDuringPublishOnly.set(eventsDisabled);
    }

    public static void setAllEventListenersDisabled(Boolean eventsDisabled) {
        JCRObservationManager.allEventListenersDisabled.set(eventsDisabled);
    }

    public static void addEvent(Event event, String mountPoint, String relativeRoot) {
        try {
            if (!event.getPath().startsWith("/jcr:system")
                    && (event.getPath().equals(relativeRoot) || event.getPath().startsWith(relativeRoot + '/'))) {
                if (event.getType() == Event.NODE_ADDED && isExtensionNode(event.getPath())
                        && hasMatchingUuidBeenSet(event.getPath())) {
                    return;
                }
                Map<JCRSessionWrapper, List<EventWrapper>> map = events.get();
                if (map == null) {
                    events.set(new HashMap<JCRSessionWrapper, List<EventWrapper>>());
                }
                map = events.get();
                JCRSessionWrapper session = currentSession.get();
                if (session != null) {
                    if (!map.containsKey(session)) {
                        map.put(session, new ArrayList<EventWrapper>());
                    }
                    List<EventWrapper> list = map.get(session);
                    list.add(getEventWrapper(event, session, mountPoint, relativeRoot));
                }
            }
        } catch (RepositoryException e) {
            logger.error(e.getMessage(), e);
        }
    }

    public static EventWrapper getEventWrapper(Event event, JCRSessionWrapper session, String mountPoint,
            String relativeRoot) {
        return new EventWrapper(event,
                event.getType() != Event.NODE_REMOVED ? null : getNodeTypesForRemovedNode(event, session),
                mountPoint, relativeRoot, session);
    }

    private static List<String> getNodeTypesForRemovedNode(Event event, JCRSessionWrapper session) {
        List<String> typeNames = new LinkedList<String>();
        try {
            NamespaceRegistry nsRegistry = session.getWorkspace().getNamespaceRegistry();
            String ntName = null;
            Map<?, ?> info = event.getInfo();
            if (info != null && !info.isEmpty()) {
                ntName = (String) info.get("primaryType");
                if (ntName != null) {
                    typeNames.add(JCRContentUtils.getJCRName(ntName, nsRegistry));
                }
                String mixins = (String) info.get("mixinTypes");
                if (mixins != null && mixins.length() > 0) {
                    if (mixins.indexOf(' ') == -1) {
                        typeNames.add(JCRContentUtils.getJCRName(mixins, nsRegistry));
                    } else {
                        for (String m : StringUtils.split(mixins, ' ')) {
                            typeNames.add(JCRContentUtils.getJCRName(m, nsRegistry));
                        }
                    }
                }
            }
            if (ntName == null && (event instanceof AdditionalEventInfo)) {
                AdditionalEventInfo advEvent = (AdditionalEventInfo) event;
                ntName = advEvent.getPrimaryNodeTypeName().toString();
                typeNames.add(JCRContentUtils.getJCRName(ntName, nsRegistry));

                if (typeNames.size() == 1) {
                    for (Name name : advEvent.getMixinTypeNames()) {
                        typeNames.add(JCRContentUtils.getJCRName(name.toString(), nsRegistry));
                    }
                }
            }
        } catch (RepositoryException e) {
            if (logger.isDebugEnabled()) {
                logger.warn("Cannot parse type for event " + event, e);
            } else {
                logger.warn("Cannot parse type for event {}. Cause: {}", event, e.getMessage());
            }
        }

        return typeNames;
    }

    private static void consume(JCRSessionWrapper session, int lastOperationType) throws RepositoryException {
        int operationType = lastOp.get();

        Map<JCRSessionWrapper, List<EventWrapper>> map = events.get();
        events.set(null);
        currentSession.set(null);
        if (map != null && map.containsKey(session)) {
            List<EventWrapper> list = map.get(session);
            consume(list, session, operationType, lastOperationType);
        }
    }

    public static void consume(List<EventWrapper> list, JCRSessionWrapper session, int operationType,
            int lastOperationType) throws RepositoryException {
        if (Boolean.TRUE.equals(allEventListenersDisabled.get())) {
            return;
        }
        String wspName = session.getWorkspace().getName();
        boolean duringPublicationOnly = Boolean.TRUE.equals(eventListenersAvailableDuringPublishOnly.get());
        for (EventConsumer consumer : listeners) {
            DefaultEventListener castListener = consumer.listener instanceof DefaultEventListener
                    ? (DefaultEventListener) consumer.listener
                    : null;
            // check if the required workspace condition is matched
            // check if the events are not disabled or the listener is still available during publication
            // check if the event is not external or consumer accepts external events
            if (consumer.session.getWorkspace().getName().equals(wspName)
                    && (!duringPublicationOnly || castListener == null || castListener.isAvailableDuringPublish())
                    && (consumer.useExternalEvents || operationType != EXTERNAL_SYNC)) {
                List<EventWrapper> filteredEvents = new ArrayList<EventWrapper>();
                for (EventWrapper event : list) {
                    if ((consumer.eventTypes & event.getType()) != 0
                            && (consumer.pathPattern == null
                                    || consumer.pathPattern.matcher(event.getPath()).matches())
                            && (consumer.uuid == null || checkUuids(event.getIdentifier(), consumer.uuid))
                            && (castListener == null || castListener.isSupportedOperationType(operationType))
                            && (consumer.nodeTypeName == null
                                    || checkNodeTypeNames(session, event, consumer.nodeTypeName))) {
                        filteredEvents.add(event);
                    }
                }
                try {
                    if (!filteredEvents.isEmpty()) {
                        consumer.listener.onEvent(new JCREventIterator(session, operationType, lastOperationType,
                                filteredEvents.iterator(), filteredEvents.size()));
                    }
                } catch (Exception e) {
                    logger.warn("Error processing event by listener. Cause: " + e.getMessage(), e);
                }

            }
        }
    }

    private static boolean checkNodeTypeNames(JCRSessionWrapper session, EventWrapper event,
            String[] requiredNodeTypes) throws RepositoryException {
        if (event.getNodeTypes() == null) {
            String nodePath = (event.getType() == Event.PROPERTY_REMOVED
                    || event.getType() == Event.PROPERTY_CHANGED || event.getType() == Event.PROPERTY_ADDED
                            ? StringUtils.substringBeforeLast(event.getPath(), "/")
                            : event.getPath());
            try {
                JCRNodeWrapper node = session.getNode(nodePath);
                event.setNodeTypes(node.getNodeTypes());
            } catch (RepositoryException e) {
                logger.debug("Could not retrieve node (type)", e);
                event.setNodeTypes(Collections.<String>emptyList());
            }
        }
        if (event.getNodeTypes() != null) {
            for (String requiredNodeType : requiredNodeTypes) {
                for (String nodeType : event.getNodeTypes()) {
                    if (NodeTypeRegistry.getInstance().getNodeType(nodeType).isNodeType(requiredNodeType)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    private static boolean checkUuids(String identifier, String[] uuids) throws RepositoryException {
        if (identifier != null) {
            for (String uuid : uuids) {
                if (identifier.equals(uuid)) {
                    return true;
                }
            }
        }
        return false;
    }

    public static <X> X doWorkspaceWriteCall(JCRSessionWrapper session, int operationType, JCRCallback<X> callback)
            throws RepositoryException {
        currentSession.set(session);
        X res;
        try {
            res = callback.doInJCR(session);
        } finally {
            boolean x = lastOp.get() == null;
            try {
                if (x) {
                    lastOp.set(operationType);
                }
                consume(session, operationType);
            } finally {
                currentSession.set(null);
                if (x) {
                    lastOp.set(null);
                }
            }
        }
        return res;
    }

    public static <X> X doWithOperationType(JCRSessionWrapper session, int operationType, JCRCallback<X> callback)
            throws RepositoryException {
        boolean x = lastOp.get() == null;
        try {
            if (x) {
                lastOp.set(operationType);
            }
            return callback.doInJCR(session);
        } finally {
            currentSession.set(null);
            if (x) {
                lastOp.set(null);
            }
        }
    }

    public static Integer getCurrentOperationType() {
        return lastOp.get();
    }

    /**
     * Check if the denoted path is in a mounted folder
     *
     * @param path
     * @return
     * @throws RepositoryException
     */
    public static boolean isExtensionNode(String path) throws RepositoryException {
        return JCRSessionFactory.getInstance().getProvider(path, false) != null;
    }

    private static boolean hasMatchingUuidBeenSet(String path) throws RepositoryException {
        if (events.get() != null && currentSession.get() != null) {
            List<EventWrapper> currentEvents = events.get().get(currentSession.get());
            for (EventWrapper previousEvent : currentEvents) {
                if (previousEvent.getPath().equals(path + "/j:externalNodeIdentifier")) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Returns a list of node types for deleted node, if this information is available in the provided event object.
     * 
     * @param event
     *            the event for deleted node
     * @return a list of node types for deleted node, if this information is available in the provided event object
     */
    public static List<String> getNodeTypesForDeletedNode(Event event) {
        return (event instanceof EventWrapper) ? ((EventWrapper) event).getNodeTypes() : null;
    }

    class EventConsumer {
        private JCRSessionWrapper session;
        private EventListener listener;
        private int eventTypes;
        private String absPath;
        private boolean isDeep;
        private String[] nodeTypeName;
        private String[] uuid;
        private boolean useExternalEvents;
        private Pattern pathPattern;

        EventConsumer(JCRSessionWrapper session, EventListener listener, int eventTypes, String absPath,
                boolean isDeep, String[] nodeTypeName, String[] uuid, boolean useExternalEvents) {
            this.session = session;
            this.listener = listener;
            this.eventTypes = eventTypes;
            this.absPath = absPath;
            this.isDeep = isDeep;
            this.nodeTypeName = nodeTypeName;
            this.uuid = uuid;
            this.useExternalEvents = useExternalEvents;

            if (this.absPath != null) {
                pathPattern = Pattern.compile("^" + this.absPath
                        + (this.isDeep ? (this.absPath.endsWith("/") ? "(.*)*" : "(/.*)*") : "") + "$");
            }
        }
    }

    class EventListenerIteratorImpl extends RangeIteratorImpl implements EventListenerIterator {
        EventListenerIteratorImpl(Iterator<EventConsumer> iterator, long size) {
            super(iterator, size);
        }

        /**
         * Returns the next <code>EventListener</code> in the iteration.
         *
         * @return the next <code>EventListener</code> in the iteration.
         * @throws java.util.NoSuchElementException
         *          if iteration has no more <code>EventListener</code>s.
         */
        public EventListener nextEventListener() {
            return ((EventConsumer) next()).listener;
        }
    }

    public static class EventWrapper implements Event {
        private Event event;
        private List<String> nodeTypes;
        private String identifier;
        private JCRSessionWrapper session;
        private String mountPoint;
        private String relativeRoot;
        private String effectivePath;

        EventWrapper(Event event, List<String> nodeTypes, String mountPoint, String relativeRoot,
                JCRSessionWrapper session) {
            this.event = event;
            this.nodeTypes = nodeTypes;
            this.session = session;
            this.mountPoint = mountPoint;
            this.relativeRoot = relativeRoot;
        }

        public int getType() {
            return event.getType();
        }

        public String getPath() throws RepositoryException {
            if (effectivePath == null) {
                effectivePath = !mountPoint.equals("/")
                        ? (mountPoint + event.getPath().substring(relativeRoot.length()))
                        : event.getPath();
            }

            return effectivePath;
        }

        public String getUserID() {
            return event.getUserID();
        }

        public String getIdentifier() throws RepositoryException {
            if (identifier == null) {
                if (isExtensionNode(getPath())) {
                    String path = getPath();
                    if (event.getType() == PROPERTY_ADDED || event.getType() == PROPERTY_REMOVED
                            || event.getType() == PROPERTY_CHANGED) {
                        path = StringUtils.substringBeforeLast(path, "/");
                    }
                    try {
                        identifier = session.getNode(path).getIdentifier();
                    } catch (RepositoryException e) {
                        identifier = null;
                    }
                } else {
                    identifier = event.getIdentifier();
                }
            }

            return identifier;
        }

        @SuppressWarnings("rawtypes")
        public Map getInfo() throws RepositoryException {
            return event.getInfo();
        }

        public String getUserData() throws RepositoryException {
            return event.getUserData();
        }

        public long getDate() throws RepositoryException {
            return event.getDate();
        }

        public void setNodeTypes(List<String> nodeTypes) {
            this.nodeTypes = nodeTypes;
        }

        public List<String> getNodeTypes() {
            return nodeTypes;
        }

        public boolean isExternal() {
            return event instanceof JackrabbitEvent ? ((JackrabbitEvent) event).isExternal() : false;
        }

        /**
         * Returns <code>true</code> if this <code>Event</code> is equal to another
         * object.
         * <p/>
         * Two <code>Event</code> instances are equal if their respective
         * <code>EventState</code> instances are equal and both <code>Event</code>
         * instances are intended for the same <code>Session</code> that registerd
         * the <code>EventListener</code>.
         *
         * @param o the reference object with which to compare.
         * @return <code>true</code> if this <code>Event</code> is equal to another
         *         object.
         */
        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || this.getClass() != o.getClass())
                return false;

            return event.equals(((EventWrapper) o).event);
        }

        @Override
        public int hashCode() {
            return event != null ? event.hashCode() : 0;
        }

        @Override
        public String toString() {
            return event.toString();
        }
    }
}