blob: 0099a87be4328cc48bc53a6ad7565c18f0dc6369 [file] [log] [blame]
/*
* 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()));
}
}
}