/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.api.vfs.impl.file.event.detectors;
import com.google.common.hash.Hashing;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.jsonrpc.commons.RequestTransmitter;
import org.eclipse.che.api.core.notification.EventService;
import org.eclipse.che.api.core.notification.EventSubscriber;
import org.eclipse.che.api.project.shared.dto.event.FileStateUpdateDto;
import org.eclipse.che.api.project.shared.dto.event.FileTrackingOperationDto;
import org.eclipse.che.api.project.shared.dto.event.FileTrackingOperationDto.Type;
import org.eclipse.che.api.vfs.Path;
import org.eclipse.che.api.vfs.VirtualFile;
import org.eclipse.che.api.vfs.VirtualFileSystemProvider;
import org.eclipse.che.api.vfs.watcher.FileWatcherManager;
import org.eclipse.che.api.vfs.watcher.FileWatcherUtils;
import org.slf4j.Logger;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Timer;
import java.util.TimerTask;
import java.util.function.Consumer;
import static com.google.common.io.Files.hash;
import static java.nio.charset.Charset.defaultCharset;
import static org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType.DELETED;
import static org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType.MODIFIED;
import static org.eclipse.che.dto.server.DtoFactory.newDto;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Receive a file tracking operation call from client. There are several type of such calls:
* <ul>
* <li>
* START/STOP - tells to start/stop tracking specific file
* </li>
* <li>
* SUSPEND/RESUME - tells to start/stop tracking all files registered for specific endpoint
* </li>
* <li>
* MOVE - tells that file that is being tracked should be moved (renamed)
* </li>
* </ul>
*
* @author Dmitry Kuleshov
*/
@Singleton
public class EditorFileTracker {
private static final Logger LOG = getLogger(EditorFileTracker.class);
private static final String OUTGOING_METHOD = "event:file-state-changed";
private final Map<String, String> hashRegistry = new HashMap<>();
private final Map<String, Integer> watchIdRegistry = new HashMap<>();
private final RequestTransmitter transmitter;
private final FileWatcherManager fileWatcherManager;
private final VirtualFileSystemProvider vfsProvider;
private File root;
private final EventService eventService;
private final EventSubscriber<FileTrackingOperationEvent> fileOperationEventSubscriber;
@Inject
public EditorFileTracker(@Named("che.user.workspaces.storage") File root,
FileWatcherManager fileWatcherManager,
RequestTransmitter transmitter,
VirtualFileSystemProvider vfsProvider,
EventService eventService) {
this.root = root;
this.fileWatcherManager = fileWatcherManager;
this.transmitter = transmitter;
this.vfsProvider = vfsProvider;
this.eventService = eventService;
fileOperationEventSubscriber = new EventSubscriber<FileTrackingOperationEvent>() {
@Override
public void onEvent(FileTrackingOperationEvent event) {
onFileTrackingOperationReceived(event.getEndpointId(), event.getFileTrackingOperation());
}
};
eventService.subscribe(fileOperationEventSubscriber);
}
private void onFileTrackingOperationReceived(String endpointId, FileTrackingOperationDto operation) {
Type type = operation.getType();
String path = operation.getPath();
String oldPath = operation.getOldPath();
switch (type) {
case START: {
String key = path + endpointId;
LOG.debug("Received file tracking operation START trigger key : {}", key);
if (watchIdRegistry.containsKey(key)) {
LOG.debug("Already registered {}", key);
return;
}
int id = fileWatcherManager.registerByPath(path,
getCreateConsumer(endpointId, path),
getModifyConsumer(endpointId, path),
getDeleteConsumer(endpointId, path));
watchIdRegistry.put(key, id);
break;
}
case STOP: {
LOG.debug("Received file tracking operation STOP trigger.");
Integer id = watchIdRegistry.remove(path + endpointId);
if (id != null) {
fileWatcherManager.unRegisterByPath(id);
}
break;
}
case SUSPEND: {
LOG.debug("Received file tracking operation SUSPEND trigger.");
fileWatcherManager.suspend();
break;
}
case RESUME: {
LOG.debug("Received file tracking operation RESUME trigger.");
fileWatcherManager.resume();
break;
}
case MOVE: {
LOG.debug("Received file tracking operation MOVE trigger.");
Integer oldId = watchIdRegistry.remove(oldPath + endpointId);
if (oldId != null) {
fileWatcherManager.unRegisterByPath(oldId);
}
int newId = fileWatcherManager.registerByPath(path,
getCreateConsumer(endpointId, path),
getModifyConsumer(endpointId, path),
getDeleteConsumer(endpointId, path));
watchIdRegistry.put(path + endpointId, newId);
break;
}
default: {
LOG.error("Received file tracking operation UNKNOWN trigger.");
break;
}
}
}
private Consumer<String> getCreateConsumer(String endpointId, String path) {
// for case when file is updated through recreation
return getModifyConsumer(endpointId, path);
}
private Consumer<String> getModifyConsumer(String endpointId, String path) {
return it -> {
String newHash = hashFile(path);
String oldHash = hashRegistry.getOrDefault(path + endpointId, null);
if (Objects.equals(newHash, oldHash)) {
return;
}
hashRegistry.put(path + endpointId, newHash);
FileStateUpdateDto params = newDto(FileStateUpdateDto.class).withPath(path).withType(MODIFIED).withHashCode(newHash);
transmitter.newRequest()
.endpointId(endpointId)
.methodName(OUTGOING_METHOD)
.paramsAsDto(params)
.sendAndSkipResult();
};
}
private Consumer<String> getDeleteConsumer(String endpointId, String path) {
return it -> new Timer().schedule(new TimerTask() {
@Override
public void run() {
if (!Files.exists(FileWatcherUtils.toNormalPath(root.toPath(), it))) {
FileStateUpdateDto params = newDto(FileStateUpdateDto.class).withPath(path).withType(DELETED);
transmitter.newRequest()
.endpointId(endpointId)
.methodName(OUTGOING_METHOD)
.paramsAsDto(params)
.sendAndSkipResult();
}
}
}, 1_000L);
}
private String hashFile(String path) {
try {
VirtualFile file = vfsProvider.getVirtualFileSystem().getRoot().getChild(Path.of(path));
return file == null ? Hashing.md5().hashString("", defaultCharset()).toString()
: hash(file.toIoFile(), Hashing.md5()).toString();
} catch (ServerException | IOException e) {
LOG.error("Error trying to read {} file and broadcast it", path, e);
}
return null;
}
@PreDestroy
private void unsubscribe() {
eventService.unsubscribe(fileOperationEventSubscriber);
}
}