| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package org.apache.jackrabbit.oak.plugins.observation; |
| |
| import static java.util.Collections.addAll; |
| import static org.apache.jackrabbit.oak.plugins.observation.filter.VisibleFilter.VISIBLE_FILTER; |
| |
| import java.util.Map; |
| import java.util.Set; |
| |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Sets; |
| import org.apache.jackrabbit.oak.api.PropertyState; |
| import org.apache.jackrabbit.oak.api.Type; |
| import org.apache.jackrabbit.oak.commons.PathUtils; |
| import org.apache.jackrabbit.oak.namepath.impl.GlobalNameMapper; |
| import org.apache.jackrabbit.oak.namepath.NamePathMapper; |
| import org.apache.jackrabbit.oak.namepath.impl.NamePathMapperImpl; |
| import org.apache.jackrabbit.oak.plugins.tree.factories.RootFactory; |
| import org.apache.jackrabbit.oak.spi.commit.CommitInfo; |
| import org.apache.jackrabbit.oak.spi.commit.Observer; |
| import org.apache.jackrabbit.oak.spi.state.NodeState; |
| import org.apache.jackrabbit.oak.commons.PerfLogger; |
| import org.jetbrains.annotations.NotNull; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Base class for {@code Observer} instances that group changes |
| * by node instead of tracking them down to individual properties. |
| */ |
| public abstract class NodeObserver implements Observer { |
| |
| private static final PerfLogger PERF_LOGGER = new PerfLogger( |
| LoggerFactory.getLogger(NodeObserver.class.getName() |
| + ".perf")); |
| private static final Logger LOG = LoggerFactory.getLogger(NodeObserver.class); |
| |
| private final String path; |
| private final Set<String> propertyNames = Sets.newHashSet(); |
| |
| private NodeState previousRoot; |
| |
| /** |
| * Create a new instance for observing the given path. |
| * @param path |
| * @param propertyNames names of properties to report even without a change |
| */ |
| protected NodeObserver(String path, String... propertyNames) { |
| this.path = path; |
| addAll(this.propertyNames, propertyNames); |
| } |
| |
| /** |
| * A node at {@code path} has been added. |
| * @param path Path of the added node. |
| * @param added Names of the added properties. |
| * @param deleted Names of the deleted properties. |
| * @param changed Names of the changed properties. |
| * @param properties Properties as specified in the constructor |
| * @param commitInfo commit info associated with this change. |
| */ |
| protected abstract void added( |
| @NotNull String path, |
| @NotNull Set<String> added, |
| @NotNull Set<String> deleted, |
| @NotNull Set<String> changed, |
| @NotNull Map<String, String> properties, |
| @NotNull CommitInfo commitInfo); |
| |
| /** |
| * A node at {@code path} has been deleted. |
| * @param path Path of the deleted node. |
| * @param added Names of the added properties. |
| * @param deleted Names of the deleted properties. |
| * @param changed Names of the changed properties. |
| * @param properties Properties as specified in the constructor |
| * @param commitInfo commit info associated with this change. |
| */ |
| protected abstract void deleted( |
| @NotNull String path, |
| @NotNull Set<String> added, |
| @NotNull Set<String> deleted, |
| @NotNull Set<String> changed, |
| @NotNull Map<String, String> properties, |
| @NotNull CommitInfo commitInfo); |
| |
| /** |
| * A node at {@code path} has been changed. |
| * @param path Path of the changed node. |
| * @param added Names of the added properties. |
| * @param deleted Names of the deleted properties. |
| * @param changed Names of the changed properties. |
| * @param properties Properties as specified in the constructor |
| * @param commitInfo commit info associated with this change. |
| */ |
| protected abstract void changed( |
| @NotNull String path, |
| @NotNull Set<String> added, |
| @NotNull Set<String> deleted, |
| @NotNull Set<String> changed, |
| @NotNull Map<String, String> properties, |
| @NotNull CommitInfo commitInfo); |
| |
| @Override |
| public void contentChanged(@NotNull NodeState root, @NotNull CommitInfo info) { |
| if (previousRoot != null) { |
| try { |
| long start = PERF_LOGGER.start(); |
| NamePathMapper namePathMapper = new NamePathMapperImpl( |
| new GlobalNameMapper(RootFactory.createReadOnlyRoot(root))); |
| |
| Set<String> oakPropertyNames = Sets.newHashSet(); |
| for (String name : propertyNames) { |
| String oakName = namePathMapper.getOakNameOrNull(name); |
| if (oakName == null) { |
| LOG.warn("Ignoring invalid property name: {}", name); |
| } else { |
| oakPropertyNames.add(oakName); |
| } |
| } |
| |
| NodeState before = previousRoot; |
| NodeState after = root; |
| EventHandler handler = new FilteredHandler( |
| VISIBLE_FILTER, |
| new NodeEventHandler("/", info, namePathMapper, oakPropertyNames)); |
| |
| String oakPath = namePathMapper.getOakPath(path); |
| if (oakPath == null) { |
| LOG.warn("Cannot listen for changes on invalid path: {}", path); |
| return; |
| } |
| |
| for (String oakName : PathUtils.elements(oakPath)) { |
| before = before.getChildNode(oakName); |
| after = after.getChildNode(oakName); |
| handler = handler.getChildHandler(oakName, before, after); |
| } |
| |
| EventGenerator generator = new EventGenerator(before, after, handler); |
| while (!generator.isDone()) { |
| generator.generate(); |
| } |
| PERF_LOGGER.end(start, 100, |
| "Generated events (before: {}, after: {})", |
| previousRoot, root); |
| } catch (Exception e) { |
| LOG.warn("Error while dispatching observation events", e); |
| } |
| } |
| |
| previousRoot = root; |
| } |
| |
| private enum EventType {ADDED, DELETED, CHANGED} |
| |
| private class NodeEventHandler extends DefaultEventHandler { |
| private final String path; |
| private final CommitInfo commitInfo; |
| private final NamePathMapper namePathMapper; |
| private final Set<String> propertyNames; |
| private final EventType eventType; |
| private final Set<String> added = Sets.newHashSet(); |
| private final Set<String> deleted = Sets.newHashSet(); |
| private final Set<String> changed = Sets.newHashSet(); |
| |
| public NodeEventHandler(String path, CommitInfo commitInfo, NamePathMapper namePathMapper, |
| Set<String> propertyNames) { |
| this.path = path; |
| this.commitInfo = commitInfo; |
| this.namePathMapper = namePathMapper; |
| this.propertyNames = propertyNames; |
| this.eventType = EventType.CHANGED; |
| } |
| |
| private NodeEventHandler(NodeEventHandler parent, String name, EventType eventType) { |
| this.path = "/".equals(parent.path) ? '/' + name : parent.path + '/' + name; |
| this.commitInfo = parent.commitInfo; |
| this.namePathMapper = parent.namePathMapper; |
| this.propertyNames = parent.propertyNames; |
| this.eventType = eventType; |
| } |
| |
| @Override |
| public void leave(NodeState before, NodeState after) { |
| switch (eventType) { |
| case ADDED: |
| added(namePathMapper.getJcrPath(path), added, deleted, changed, |
| collectProperties(after), commitInfo); |
| break; |
| case DELETED: |
| deleted(namePathMapper.getJcrPath(path), added, deleted, changed, |
| collectProperties(before), commitInfo); |
| break; |
| case CHANGED: |
| if (!added.isEmpty() || ! deleted.isEmpty() || !changed.isEmpty()) { |
| changed(namePathMapper.getJcrPath(path), added, deleted, changed, |
| collectProperties(after), commitInfo); |
| } |
| break; |
| } |
| } |
| |
| private Map<String, String> collectProperties(NodeState node) { |
| Map<String, String> properties = Maps.newHashMap(); |
| for (String name : propertyNames) { |
| PropertyState p = node.getProperty(name); |
| if (p != null && !p.isArray()) { |
| properties.put(name, p.getValue(Type.STRING)); |
| } |
| } |
| return properties; |
| } |
| |
| @Override |
| public EventHandler getChildHandler(String name, NodeState before, NodeState after) { |
| if (!before.exists()) { |
| return new NodeEventHandler(this, name, EventType.ADDED); |
| } else if (!after.exists()) { |
| return new NodeEventHandler(this, name, EventType.DELETED); |
| } else { |
| return new NodeEventHandler(this, name, EventType.CHANGED); |
| } |
| } |
| |
| @Override |
| public void propertyAdded(PropertyState after) { |
| added.add(namePathMapper.getJcrName(after.getName())); |
| } |
| |
| @Override |
| public void propertyChanged(PropertyState before, PropertyState after) { |
| changed.add(namePathMapper.getJcrName(after.getName())); |
| } |
| |
| @Override |
| public void propertyDeleted(PropertyState before) { |
| deleted.add(namePathMapper.getJcrName(before.getName())); |
| } |
| |
| } |
| } |