blob: 88c9d6f7755a2a6ede3f88fc7bff2bb6d043dc24 [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.sling.fsprovider.internal;
import static org.apache.sling.api.SlingConstants.PROPERTY_PATH;
import static org.apache.sling.api.SlingConstants.PROPERTY_RESOURCE_TYPE;
import static org.apache.sling.api.SlingConstants.TOPIC_RESOURCE_ADDED;
import static org.apache.sling.api.SlingConstants.TOPIC_RESOURCE_CHANGED;
import static org.apache.sling.api.SlingConstants.TOPIC_RESOURCE_REMOVED;
import java.io.File;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.vault.util.PlatformNameFormat;
import org.apache.sling.fsprovider.internal.mapper.ContentFile;
import org.apache.sling.fsprovider.internal.mapper.FileResource;
import org.apache.sling.fsprovider.internal.parser.ContentElement;
import org.apache.sling.fsprovider.internal.parser.ContentElementImpl;
import org.apache.sling.fsprovider.internal.parser.ContentFileCache;
import org.osgi.service.event.EventAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class is a monitor for the file system
* that periodically checks for changes.
*/
public final class FileMonitor extends TimerTask {
private final Logger log = LoggerFactory.getLogger(this.getClass());
private final Timer timer = new Timer();
private boolean stop = false;
private boolean stopped = true;
private final Monitorable root;
private final FsResourceProvider provider;
private final FsMode fsMode;
private final ContentFileExtensions contentFileExtensions;
private final ContentFileCache contentFileCache;
/**
* Creates a new instance of this class.
* @param provider The resource provider.
* @param interval The interval between executions of the task, in milliseconds.
* @param fsMode FS mode
* @param contentFileExtensions Content file extensions
* @param contentFileCache Content file cache
*/
public FileMonitor(final FsResourceProvider provider, final long interval, FsMode fsMode,
final ContentFileExtensions contentFileExtensions, final ContentFileCache contentFileCache) {
this.provider = provider;
this.fsMode = fsMode;
this.contentFileExtensions = contentFileExtensions;
this.contentFileCache = contentFileCache;
File rootFile = this.provider.getRootFile();
if (fsMode == FsMode.FILEVAULT_XML) {
rootFile = new File(this.provider.getRootFile(), "." + PlatformNameFormat.getPlatformPath(this.provider.getProviderRoot()));
}
this.root = new Monitorable(this.provider.getProviderRoot(), rootFile, null);
createStatus(this.root, contentFileExtensions, contentFileCache);
log.debug("Starting file monitor for {} with an interval of {}ms", this.root.file, interval);
timer.schedule(this, 0, interval);
}
/**
* Stop periodically executing this task. If the task is currently executing it
* will never be run again after the current execution, otherwise it will simply
* never run (again).
*/
void stop() {
synchronized (timer) {
if (!stop) {
stop = true;
cancel();
timer.cancel();
}
boolean interrupted = false;
while (!stopped) {
try {
timer.wait();
}
catch (InterruptedException e) {
interrupted = true;
}
}
if (interrupted) {
Thread.currentThread().interrupt();
}
}
log.debug("Stopped file monitor for {}", this.root.file);
}
/**
* @see java.util.TimerTask#run()
*/
@Override
public void run() {
synchronized (timer) {
stopped = false;
if (stop) {
stopped = true;
timer.notifyAll();
return;
}
}
synchronized ( this ) {
try {
// if we don't have an event admin, we just skip the check
final EventAdmin reporter = this.provider.getEventAdmin();
if ( reporter != null ) {
this.check(this.root, reporter);
}
} catch (Exception e) {
// ignore this
}
}
synchronized (timer) {
stopped = true;
timer.notifyAll();
}
}
/**
* Check the monitorable
* @param monitorable The monitorable to check
* @param reporter The EventAdmin
*/
private void check(final Monitorable monitorable, final EventAdmin reporter) {
log.trace("Checking {}", monitorable.file);
// if the file is non existing, check if it has been readded
if ( monitorable.status instanceof NonExistingStatus ) {
if ( monitorable.file.exists() ) {
// new file and reset status
createStatus(monitorable, contentFileExtensions, contentFileCache);
sendEvents(monitorable, TOPIC_RESOURCE_ADDED, reporter);
final FileStatus fs = (FileStatus)monitorable.status;
if ( fs instanceof DirStatus ) {
final DirStatus ds = (DirStatus)fs;
// remove monitorables for new folder and update folder children to send events for directory contents
ds.children = new Monitorable[0];
checkDirStatusChildren(monitorable, reporter);
}
}
} else {
// check if the file has been removed
if ( !monitorable.file.exists() ) {
// removed file and update status
sendEvents(monitorable, TOPIC_RESOURCE_REMOVED, reporter);
monitorable.status = NonExistingStatus.SINGLETON;
contentFileCache.remove(transformPath(monitorable.path));
} else {
// check for changes
final FileStatus fs = (FileStatus)monitorable.status;
boolean changed = false;
if ( fs.lastModified < monitorable.file.lastModified() ) {
fs.lastModified = monitorable.file.lastModified();
// changed
sendEvents(monitorable, TOPIC_RESOURCE_CHANGED, reporter);
changed = true;
contentFileCache.remove(transformPath(monitorable.path));
}
if ( fs instanceof DirStatus ) {
// directory
final DirStatus ds = (DirStatus)fs;
for(int i=0; i<ds.children.length; i++) {
check(ds.children[i], reporter);
}
// if the dir changed we have to update
if ( changed ) {
// and now update
checkDirStatusChildren(monitorable, reporter);
}
}
}
}
}
private void checkDirStatusChildren(final Monitorable dirMonitorable, final EventAdmin reporter) {
final DirStatus ds = (DirStatus)dirMonitorable.status;
final File[] files = dirMonitorable.file.listFiles();
if (files != null) {
final Monitorable[] children = new Monitorable[files.length];
for (int i = 0; i < files.length; i++) {
// search in old list
for (int m = 0; m < ds.children.length; m++) {
if (ds.children[m].file.equals(files[i])) {
children[i] = ds.children[m];
break;
}
}
if (children[i] == null) {
children[i] = new Monitorable(dirMonitorable.path + '/' + files[i].getName(), files[i],
contentFileExtensions.getSuffix(files[i]));
children[i].status = NonExistingStatus.SINGLETON;
check(children[i], reporter);
}
}
ds.children = children;
} else {
ds.children = new Monitorable[0];
}
}
/**
* Send the event async via the event admin.
*/
private void sendEvents(final Monitorable monitorable, final String changeType, final EventAdmin reporter) {
if (log.isDebugEnabled()) {
log.debug("Detected change for resource {} : {}", transformPath(monitorable.path), changeType);
}
List<ResourceChange> changes = collectResourceChanges(monitorable, changeType);
for (ResourceChange change : changes) {
if (log.isTraceEnabled()) {
log.debug("Send change for resource {}: {}", transformPath(change.path), change.topic);
}
final Dictionary<String, String> properties = new Hashtable<>();
properties.put(PROPERTY_PATH, transformPath(change.path));
if (change.resourceType != null) {
properties.put(PROPERTY_RESOURCE_TYPE, change.resourceType);
}
reporter.postEvent(new org.osgi.service.event.Event(change.topic, properties));
}
}
/**
* Transform path for resource event.
* @param path Path
* @return Transformed path
*/
private String transformPath(String path) {
if (fsMode == FsMode.FILEVAULT_XML) {
return PlatformNameFormat.getRepositoryPath(path);
}
else {
return path;
}
}
private List<ResourceChange> collectResourceChanges(final Monitorable monitorable, final String changeType) {
List<ResourceChange> changes = new ArrayList<>();
if (monitorable.status instanceof ContentFileStatus) {
ContentFile contentFile = ((ContentFileStatus)monitorable.status).contentFile;
if (StringUtils.equals(changeType, TOPIC_RESOURCE_CHANGED)) {
ContentElement content = contentFile.getContent();
// we cannot easily report the diff of resource changes between two content files
// so we simulate a removal of the toplevel node and then add all nodes contained in the current content file again.
changes.add(buildContentResourceChange(TOPIC_RESOURCE_REMOVED, content, transformPath(monitorable.path)));
addContentResourceChanges(changes, TOPIC_RESOURCE_ADDED, content, transformPath(monitorable.path));
}
else {
addContentResourceChanges(changes, changeType, contentFile.getContent(), transformPath(monitorable.path));
}
}
else {
Map<String,Object> props = new HashMap<>();
props.put("sling:resourceType", monitorable.status instanceof FileStatus ?
FileResource.RESOURCE_TYPE_FILE : FileResource.RESOURCE_TYPE_FOLDER);
ContentElement content = new ContentElementImpl(null, props);
changes.add(buildContentResourceChange(changeType, content, transformPath(monitorable.path)));
}
return changes;
}
private void addContentResourceChanges(final List<ResourceChange> changes, final String changeType,
final ContentElement content, final String path) {
changes.add(buildContentResourceChange(changeType, content, path));
if (content != null) {
for (Map.Entry<String,ContentElement> entry : content.getChildren().entrySet()) {
String childPath = path + "/" + entry.getKey();
addContentResourceChanges(changes, changeType, entry.getValue(), childPath);
}
}
}
private ResourceChange buildContentResourceChange(final String changeType, final ContentElement content, final String path) {
ResourceChange change = new ResourceChange();
change.path = path;
change.resourceType = content != null ? (String)content.getProperties().get("sling:resourceType") : null;
change.topic = changeType;
return change;
}
/**
* Create a status object for the monitorable
*/
private static void createStatus(final Monitorable monitorable, ContentFileExtensions contentFileExtensions, ContentFileCache contentFileCache) {
if ( !monitorable.file.exists() ) {
monitorable.status = NonExistingStatus.SINGLETON;
} else if ( monitorable.file.isFile() ) {
if (contentFileExtensions.matchesSuffix(monitorable.file)) {
monitorable.status = new ContentFileStatus(monitorable.file,
new ContentFile(monitorable.file, monitorable.path, null, contentFileCache));
}
else {
monitorable.status = new FileStatus(monitorable.file);
}
} else {
monitorable.status = new DirStatus(monitorable.file, monitorable.path, contentFileExtensions, contentFileCache);
}
}
/** The monitorable to hold the resource path, the file and the status. */
private static final class Monitorable {
public final String path;
public final File file;
public Object status;
public Monitorable(final String path, final File file, String contentFileSuffix) {
this.file = file;
if (contentFileSuffix != null) {
this.path = StringUtils.substringBeforeLast(path, contentFileSuffix);
}
else {
this.path = path;
}
}
}
/** Status for files. */
private static class FileStatus {
public long lastModified;
public FileStatus(final File file) {
this.lastModified = file.lastModified();
}
}
/** Status for content files */
private static class ContentFileStatus extends FileStatus {
public final ContentFile contentFile;
public ContentFileStatus(final File file, final ContentFile contentFile) {
super(file);
this.contentFile = contentFile;
}
}
/** Status for directories. */
private static final class DirStatus extends FileStatus {
public Monitorable[] children;
public DirStatus(final File dir, final String path,
final ContentFileExtensions contentFileExtensions, final ContentFileCache contentFileCache) {
super(dir);
final File[] files = dir.listFiles();
if (files != null) {
this.children = new Monitorable[files.length];
for (int i = 0; i < files.length; i++) {
this.children[i] = new Monitorable(path + '/' + files[i].getName(), files[i],
contentFileExtensions.getSuffix(files[i]));
FileMonitor.createStatus(this.children[i], contentFileExtensions, contentFileCache);
}
} else {
this.children = new Monitorable[0];
}
}
}
/** Status for non existing files. */
private static final class NonExistingStatus {
public static NonExistingStatus SINGLETON = new NonExistingStatus();
}
static class ResourceChange {
public String path;
public String resourceType;
public String topic;
}
}